classifyre-cli 0.4.85__tar.gz → 0.4.87__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 (283) hide show
  1. classifyre_cli-0.4.87/.turbo/turbo-build.log +3 -0
  2. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/PKG-INFO +1 -1
  3. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/package.json +1 -1
  4. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/pyproject.toml +1 -1
  5. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/custom/runners/_gliner2.py +11 -1
  6. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/custom/runners/_text_classification.py +68 -1
  7. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/pii/detector.py +105 -9
  8. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/models/generated_detectors.py +4 -4
  9. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/outputs/base.py +8 -1
  10. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/outputs/factory.py +18 -4
  11. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/outputs/rest.py +17 -7
  12. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/sandbox/source.py +8 -3
  13. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/custom/test_transformer_runners.py +50 -0
  14. classifyre_cli-0.4.87/tests/detectors/pii/test_pii_chunking.py +327 -0
  15. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_outputs.py +50 -12
  16. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/uv.lock +2 -2
  17. classifyre_cli-0.4.85/.turbo/turbo-build.log +0 -3
  18. classifyre_cli-0.4.85/tests/detectors/pii/test_pii_chunking.py +0 -121
  19. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/.gitignore +0 -0
  20. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/.python-version +0 -0
  21. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/README.md +0 -0
  22. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/main.py +0 -0
  23. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/scripts/generate_models.py +0 -0
  24. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/__init__.py +0 -0
  25. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/config.py +0 -0
  26. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/__init__.py +0 -0
  27. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/base.py +0 -0
  28. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/broken_links/__init__.py +0 -0
  29. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/broken_links/detector.py +0 -0
  30. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/config.py +0 -0
  31. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/content/__init__.py +0 -0
  32. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/custom/__init__.py +0 -0
  33. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/custom/detector.py +0 -0
  34. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/custom/extractor.py +0 -0
  35. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/custom/runners/__init__.py +0 -0
  36. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/custom/runners/_base.py +0 -0
  37. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/custom/runners/_factory.py +0 -0
  38. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/custom/runners/_image_classification.py +0 -0
  39. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/custom/runners/_llm.py +0 -0
  40. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/custom/runners/_object_detection.py +0 -0
  41. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/custom/runners/_regex.py +0 -0
  42. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/custom/trainer.py +0 -0
  43. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/dependencies.py +0 -0
  44. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/engine_version.py +0 -0
  45. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/pii/__init__.py +0 -0
  46. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/secrets/__init__.py +0 -0
  47. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/secrets/detector.py +0 -0
  48. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/threat/__init__.py +0 -0
  49. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/threat/code_security_detector.py +0 -0
  50. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/threat/yara_detector.py +0 -0
  51. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/file_evaluation/__init__.py +0 -0
  52. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/file_evaluation/runner.py +0 -0
  53. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/main.py +0 -0
  54. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/models/generated_input.py +0 -0
  55. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/models/generated_single_asset_scan_results.py +0 -0
  56. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/outputs/__init__.py +0 -0
  57. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/outputs/console.py +0 -0
  58. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/outputs/file.py +0 -0
  59. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/pipeline/__init__.py +0 -0
  60. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/pipeline/content_provider.py +0 -0
  61. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/pipeline/detector_pipeline.py +0 -0
  62. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/pipeline/parsed_content_provider.py +0 -0
  63. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/pipeline/payload_window.py +0 -0
  64. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/pipeline/scan_cache.py +0 -0
  65. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/pipeline/text_artifact.py +0 -0
  66. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/pipeline/worker_pool.py +0 -0
  67. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/__init__.py +0 -0
  68. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/asset_metadata.py +0 -0
  69. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/atlassian_common.py +0 -0
  70. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/azure_blob_storage/__init__.py +0 -0
  71. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/azure_blob_storage/source.py +0 -0
  72. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/base.py +0 -0
  73. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/confluence/__init__.py +0 -0
  74. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/confluence/source.py +0 -0
  75. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/databricks/__init__.py +0 -0
  76. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/databricks/source.py +0 -0
  77. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/delta_lake/__init__.py +0 -0
  78. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/delta_lake/source.py +0 -0
  79. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/dependencies.py +0 -0
  80. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/dropbox/__init__.py +0 -0
  81. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/dropbox/auth.py +0 -0
  82. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/dropbox/source.py +0 -0
  83. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/elasticsearch/__init__.py +0 -0
  84. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/elasticsearch/source.py +0 -0
  85. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/email/__init__.py +0 -0
  86. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/email/source.py +0 -0
  87. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/google_cloud_storage/__init__.py +0 -0
  88. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/google_cloud_storage/source.py +0 -0
  89. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/google_workspace/__init__.py +0 -0
  90. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/google_workspace/source.py +0 -0
  91. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/hive/__init__.py +0 -0
  92. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/hive/source.py +0 -0
  93. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/hugging_face/__init__.py +0 -0
  94. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/hugging_face/source.py +0 -0
  95. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/iceberg/__init__.py +0 -0
  96. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/iceberg/source.py +0 -0
  97. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/jira/__init__.py +0 -0
  98. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/jira/source.py +0 -0
  99. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/kafka/__init__.py +0 -0
  100. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/kafka/rest.py +0 -0
  101. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/kafka/source.py +0 -0
  102. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/lakehouse_base.py +0 -0
  103. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/local_folder/__init__.py +0 -0
  104. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/local_folder/source.py +0 -0
  105. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/meilisearch/__init__.py +0 -0
  106. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/meilisearch/source.py +0 -0
  107. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/microsoft_365/__init__.py +0 -0
  108. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/microsoft_365/source.py +0 -0
  109. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/mongodb/__init__.py +0 -0
  110. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/mongodb/source.py +0 -0
  111. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/mssql/__init__.py +0 -0
  112. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/mssql/source.py +0 -0
  113. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/mysql/__init__.py +0 -0
  114. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/mysql/source.py +0 -0
  115. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/neo4j/__init__.py +0 -0
  116. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/neo4j/source.py +0 -0
  117. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/notion/__init__.py +0 -0
  118. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/notion/client.py +0 -0
  119. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/notion/source.py +0 -0
  120. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/object_storage/base.py +0 -0
  121. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/opensearch/__init__.py +0 -0
  122. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/opensearch/source.py +0 -0
  123. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/oracle/__init__.py +0 -0
  124. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/oracle/source.py +0 -0
  125. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/postgresql/__init__.py +0 -0
  126. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/postgresql/source.py +0 -0
  127. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/powerbi/__init__.py +0 -0
  128. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/powerbi/source.py +0 -0
  129. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/recipe_normalizer.py +0 -0
  130. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/reddit/__init__.py +0 -0
  131. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/reddit/source.py +0 -0
  132. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/s3_client.py +0 -0
  133. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/s3_compatible_storage/README.md +0 -0
  134. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/s3_compatible_storage/__init__.py +0 -0
  135. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/s3_compatible_storage/source.py +0 -0
  136. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/sandbox/__init__.py +0 -0
  137. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/search_engine_base.py +0 -0
  138. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/servicedesk/__init__.py +0 -0
  139. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/servicedesk/source.py +0 -0
  140. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/slack/__init__.py +0 -0
  141. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/slack/source.py +0 -0
  142. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/snowflake/__init__.py +0 -0
  143. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/snowflake/source.py +0 -0
  144. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/sqlite/__init__.py +0 -0
  145. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/sqlite/source.py +0 -0
  146. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/tableau/__init__.py +0 -0
  147. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/tableau/source.py +0 -0
  148. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/tabular_base.py +0 -0
  149. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/tabular_utils.py +0 -0
  150. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/wordpress/__init__.py +0 -0
  151. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/wordpress/source.py +0 -0
  152. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/youtube/__init__.py +0 -0
  153. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/youtube/source.py +0 -0
  154. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/telemetry.py +0 -0
  155. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/utils/__init__.py +0 -0
  156. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/utils/archive_extraction.py +0 -0
  157. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/utils/content_extraction.py +0 -0
  158. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/utils/dependency_groups.py +0 -0
  159. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/utils/embedded_files.py +0 -0
  160. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/utils/external_video.py +0 -0
  161. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/utils/file_metadata.py +0 -0
  162. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/utils/file_parser.py +0 -0
  163. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/utils/file_to_images.py +0 -0
  164. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/utils/hashing.py +0 -0
  165. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/utils/legacy_office.py +0 -0
  166. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/utils/payload.py +0 -0
  167. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/utils/range_reader.py +0 -0
  168. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/utils/resources.py +0 -0
  169. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/utils/transcription.py +0 -0
  170. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/utils/uv_sync.py +0 -0
  171. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/utils/validation.py +0 -0
  172. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/utils/video_processing.py +0 -0
  173. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/__init__.py +0 -0
  174. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/_lakehouse_fakes.py +0 -0
  175. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/conftest.py +0 -0
  176. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/__init__.py +0 -0
  177. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/broken_links/test_broken_links_detector.py +0 -0
  178. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/conftest.py +0 -0
  179. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/content/__init__.py +0 -0
  180. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/custom/__init__.py +0 -0
  181. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/custom/conftest.py +0 -0
  182. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/custom/test_gliner2_classification.py +0 -0
  183. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/custom/test_invoice_extraction.py +0 -0
  184. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/custom/test_llm_runner.py +0 -0
  185. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/custom/test_pipeline_integration.py +0 -0
  186. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/custom/test_regex_runner.py +0 -0
  187. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/custom/test_runner_hardening.py +0 -0
  188. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/pii/__init__.py +0 -0
  189. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/pii/conftest.py +0 -0
  190. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/pii/sample_invoice.pdf +0 -0
  191. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/pii/test_pii_detector.py +0 -0
  192. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/pii/test_pii_detector_extended.py +0 -0
  193. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/pii/test_pii_regional_entities.py +0 -0
  194. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/pii/test_pii_severity.py +0 -0
  195. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/secrets/__init__.py +0 -0
  196. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/secrets/test_secrets_detector.py +0 -0
  197. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/secrets/test_secrets_detector_extended.py +0 -0
  198. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/test_base_detector.py +0 -0
  199. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/test_custom_detector_examples_runtime.py +0 -0
  200. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/test_detector_catalog_commercial.py +0 -0
  201. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/test_detector_pipeline_types.py +0 -0
  202. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/test_detector_schema_examples.py +0 -0
  203. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/test_detector_types.py +0 -0
  204. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/test_phase2_detectors.py +0 -0
  205. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/test_registry.py +0 -0
  206. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/threat/__init__.py +0 -0
  207. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/threat/test_code_security_detector.py +0 -0
  208. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/threat/test_yara_detector.py +0 -0
  209. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/integration/test_wordpress_broken_links_detector.py +0 -0
  210. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/integration/test_wordpress_links_assets.py +0 -0
  211. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/pipeline/test_detector_outcomes.py +0 -0
  212. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/pipeline/test_detector_pipeline.py +0 -0
  213. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/pipeline/test_worker_pool.py +0 -0
  214. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_assets_metadata_catalog.py +0 -0
  215. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_azure_blob_storage_source.py +0 -0
  216. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_base_source_attachment.py +0 -0
  217. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_base_source_sampling.py +0 -0
  218. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_byte_valued_fields.py +0 -0
  219. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_config.py +0 -0
  220. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_confluence_source.py +0 -0
  221. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_custom_extractor.py +0 -0
  222. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_databricks_source.py +0 -0
  223. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_delta_lake_source.py +0 -0
  224. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_dependency_groups.py +0 -0
  225. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_dropbox_auth_command.py +0 -0
  226. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_dropbox_source.py +0 -0
  227. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_elasticsearch_source.py +0 -0
  228. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_email_source.py +0 -0
  229. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_file_evaluation_runner.py +0 -0
  230. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_google_cloud_storage_source.py +0 -0
  231. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_google_workspace_source.py +0 -0
  232. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_hashing.py +0 -0
  233. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_hive_source.py +0 -0
  234. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_hugging_face_source.py +0 -0
  235. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_iceberg_source.py +0 -0
  236. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_jira_source.py +0 -0
  237. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_kafka_source.py +0 -0
  238. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_local_folder_source.py +0 -0
  239. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_main.py +0 -0
  240. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_meilisearch_source.py +0 -0
  241. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_microsoft_365_source.py +0 -0
  242. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_mongodb_source.py +0 -0
  243. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_mssql_source.py +0 -0
  244. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_mysql_source.py +0 -0
  245. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_neo4j_source.py +0 -0
  246. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_notion_source.py +0 -0
  247. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_object_storage_download_efficiency.py +0 -0
  248. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_opensearch_source.py +0 -0
  249. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_oracle_source.py +0 -0
  250. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_payload_window.py +0 -0
  251. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_payload_window_parquet.py +0 -0
  252. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_postgresql_source.py +0 -0
  253. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_powerbi_source.py +0 -0
  254. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_range_reading.py +0 -0
  255. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_recipe_normalizer.py +0 -0
  256. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_reddit_source.py +0 -0
  257. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_s3_compatible_storage_source.py +0 -0
  258. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_sampling_automatic.py +0 -0
  259. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_sandbox_source.py +0 -0
  260. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_scan_cache.py +0 -0
  261. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_servicedesk_source.py +0 -0
  262. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_slack_source.py +0 -0
  263. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_snowflake_source.py +0 -0
  264. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_source_dependency_groups.py +0 -0
  265. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_source_strategy_conformance.py +0 -0
  266. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_sqlite_source.py +0 -0
  267. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_tableau_source.py +0 -0
  268. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_tabular_automatic_sampling.py +0 -0
  269. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_tabular_utils.py +0 -0
  270. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_uv_sync.py +0 -0
  271. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_wordpress_source.py +0 -0
  272. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_youtube_source.py +0 -0
  273. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_youtube_source_integration.py +0 -0
  274. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/utils/test_archive_extraction.py +0 -0
  275. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/utils/test_content_extraction.py +0 -0
  276. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/utils/test_embedded_files.py +0 -0
  277. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/utils/test_file_metadata.py +0 -0
  278. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/utils/test_file_parser.py +0 -0
  279. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/utils/test_file_to_images.py +0 -0
  280. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/utils/test_legacy_office.py +0 -0
  281. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/utils/test_payload.py +0 -0
  282. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/utils/test_transcription.py +0 -0
  283. {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/utils/test_video_processing.py +0 -0
@@ -0,0 +1,3 @@
1
+ $ uv sync
2
+ Resolved 307 packages in 231ms
3
+ Checked 51 packages in 3ms
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: classifyre-cli
3
- Version: 0.4.85
3
+ Version: 0.4.87
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.85",
3
+ "version": "0.4.87",
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.85"
3
+ version = "0.4.87"
4
4
  description = "Classifyre CLI — scan and classify unstructured data sources"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -189,7 +189,17 @@ class GLiNER2Runner(BaseRunner):
189
189
  self, model: Any, text: str, task_name: str, labels: list[str]
190
190
  ) -> dict[str, object]:
191
191
  """Classify text, chunking long inputs and keeping the max-confidence label."""
192
- chunks = _chunk_text(text, _CLS_CHUNK_SIZE, _CLS_CHUNK_OVERLAP)[:_MAX_CLS_CHUNKS]
192
+ all_chunks = _chunk_text(text, _CLS_CHUNK_SIZE, _CLS_CHUNK_OVERLAP)
193
+ chunks = all_chunks[:_MAX_CLS_CHUNKS]
194
+ if len(all_chunks) > _MAX_CLS_CHUNKS:
195
+ logger.warning(
196
+ "GLiNER2 classification for detector '%s': text of %d chars needs %d chunks, "
197
+ "capping at %d; the remainder was not classified",
198
+ self._detector_key,
199
+ len(text),
200
+ len(all_chunks),
201
+ _MAX_CLS_CHUNKS,
202
+ )
193
203
 
194
204
  best: dict[str, object] = {}
195
205
  for chunk in chunks:
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
+ from collections.abc import Iterator
6
7
  from typing import Any
7
8
 
8
9
  from ....models.generated_detectors import Severity, TextClassificationPipelineSchema
@@ -13,6 +14,22 @@ from ._base import _TEXT_CONTENT_TYPES, BaseRunner, _resolve_pipeline_severity
13
14
  logger = logging.getLogger(__name__)
14
15
 
15
16
 
17
+ # A transformer pipeline called with truncation=True silently keeps only the
18
+ # first model_max_length tokens, so without chunking a long document is
19
+ # classified on its opening paragraph alone -- no error, no log, and everything
20
+ # after it is never looked at. When chunk_size is not configured we derive a
21
+ # window from the model's own token limit instead.
22
+ # Deliberate under-estimate: too small a window only costs compute, too large
23
+ # a one hands the tail back to truncation.
24
+ _CHARS_PER_TOKEN = 3
25
+ _DEFAULT_MODEL_MAX_TOKENS = 512
26
+ # model_max_length is a huge sentinel on tokenizers that declare no limit.
27
+ _MODEL_MAX_TOKENS_SENTINEL = 100_000
28
+ _AUTO_CHUNK_OVERLAP = 128
29
+ # Bound per-asset CPU cost; hitting this is logged, never silent.
30
+ _MAX_AUTO_CHUNKS = 200
31
+
32
+
16
33
  def _chunk_text(text: str, chunk_size: int | None, chunk_overlap: int) -> list[str]:
17
34
  """Split text into chunks. Returns [text] when chunk_size is not set."""
18
35
  if not chunk_size:
@@ -21,6 +38,55 @@ def _chunk_text(text: str, chunk_size: int | None, chunk_overlap: int) -> list[s
21
38
  return [text[i : i + chunk_size] for i in range(0, len(text), step)]
22
39
 
23
40
 
41
+ def _model_chunk_chars(pipe: Any, max_length: int | None) -> int:
42
+ """Characters that comfortably fit one forward pass of *pipe*."""
43
+ tokens = max_length
44
+ if tokens is None:
45
+ tokenizer_limit = getattr(getattr(pipe, "tokenizer", None), "model_max_length", None)
46
+ if isinstance(tokenizer_limit, int) and 0 < tokenizer_limit < _MODEL_MAX_TOKENS_SENTINEL:
47
+ tokens = tokenizer_limit
48
+ if not isinstance(tokens, int) or tokens <= 0:
49
+ tokens = _DEFAULT_MODEL_MAX_TOKENS
50
+ return max(256, tokens * _CHARS_PER_TOKEN)
51
+
52
+
53
+ def _iter_analysis_chunks(
54
+ text: str,
55
+ chunk_size: int | None,
56
+ chunk_overlap: int,
57
+ auto_chunk_chars: int,
58
+ ) -> Iterator[str]:
59
+ """Yield the windows to classify, one resident at a time.
60
+
61
+ An explicit chunk_size wins. Otherwise the text is passed through whole
62
+ unless it would overflow the model window, in which case it is auto-chunked
63
+ so the tail is classified rather than truncated away.
64
+ """
65
+ if chunk_size:
66
+ step = max(1, chunk_size - chunk_overlap)
67
+ for start in range(0, len(text), step):
68
+ yield text[start : start + chunk_size]
69
+ return
70
+
71
+ if len(text) <= auto_chunk_chars:
72
+ yield text
73
+ return
74
+
75
+ overlap = min(_AUTO_CHUNK_OVERLAP, auto_chunk_chars // 4)
76
+ step = max(1, auto_chunk_chars - overlap)
77
+ for index, start in enumerate(range(0, len(text), step)):
78
+ if index >= _MAX_AUTO_CHUNKS:
79
+ logger.warning(
80
+ "text_classification: text of %d chars exceeds %d auto-chunks of %d; "
81
+ "the remainder was not classified (set chunk_size to control this)",
82
+ len(text),
83
+ _MAX_AUTO_CHUNKS,
84
+ auto_chunk_chars,
85
+ )
86
+ return
87
+ yield text[start : start + auto_chunk_chars]
88
+
89
+
24
90
  class TextClassificationRunner(BaseRunner):
25
91
  """Text classification via a single HuggingFace text-classification pipeline."""
26
92
 
@@ -94,8 +160,9 @@ class TextClassificationRunner(BaseRunner):
94
160
  default_severity = schema.severity if schema.severity is not None else Severity.info
95
161
 
96
162
  best_scores: dict[str, float] = {}
163
+ auto_chunk_chars = _model_chunk_chars(pipe, max_length)
97
164
  try:
98
- for chunk in _chunk_text(text, chunk_size, chunk_overlap):
165
+ for chunk in _iter_analysis_chunks(text, chunk_size, chunk_overlap, auto_chunk_chars):
99
166
  call_kwargs: dict[str, Any] = {"truncation": True}
100
167
  if max_length is not None:
101
168
  call_kwargs["max_length"] = max_length
@@ -1,12 +1,14 @@
1
1
  """PII detector powered by Microsoft Presidio."""
2
2
 
3
3
  import asyncio
4
+ import bisect
4
5
  import importlib
5
6
  import logging
6
7
  import re
7
8
  import subprocess
8
9
  import sys
9
10
  import warnings
11
+ from collections.abc import Iterator
10
12
  from dataclasses import dataclass
11
13
  from typing import Any, ClassVar
12
14
 
@@ -239,6 +241,17 @@ class PIIDetector(BaseDetector):
239
241
  # Per-cell analysis at scale causes O(rowsxcolumns) Presidio calls per page.
240
242
  _TABULAR_CELL_LIMIT: ClassVar[int] = 200
241
243
 
244
+ # spaCy's own ceiling when nlp.max_length is not overridden.
245
+ _SPACY_DEFAULT_MAX_LENGTH: ClassVar[int] = 1_000_000
246
+
247
+ # Window used when a page exceeds the spaCy ceiling and no chunk_size is
248
+ # configured. The parser/NER need roughly 1GB of scratch memory per 100k
249
+ # characters, so 200k caps peak usage near 2GB no matter how large the page
250
+ # is -- whereas simply raising nlp.max_length scales memory with file size
251
+ # (a 1.4M-char page would need ~14GB).
252
+ _AUTO_CHUNK_CHARS: ClassVar[int] = 200_000
253
+ _AUTO_CHUNK_OVERLAP: ClassVar[int] = 512
254
+
242
255
  def __init__(self, config: DetectorConfig | None = None) -> None:
243
256
  super().__init__(config)
244
257
  self._cfg: PIIDetectorConfig = (
@@ -659,10 +672,40 @@ class PIIDetector(BaseDetector):
659
672
  # ------------------------------------------------------------------
660
673
 
661
674
  def _analyze_content(self, content: str, *, entities: list[str] | None = None) -> list[Any]:
675
+ """Analyze *content*, splitting it first if it would overflow spaCy.
676
+
677
+ This is the single chokepoint every caller goes through -- full-text
678
+ pages, individual tabular cells, and pre-chunked windows alike -- so the
679
+ oversize guard lives here rather than at any one call site. A page that
680
+ arrives already chunked passes straight through.
681
+ """
662
682
  if self.analyzer is None:
663
683
  if self._init_error is not None:
664
684
  raise self._init_error
665
685
  return []
686
+ if len(content) > self._spacy_char_limit():
687
+ return self._analyze_oversized(content, entities=entities)
688
+ return self._analyze_once(content, entities=entities)
689
+
690
+ def _analyze_oversized(self, content: str, *, entities: list[str] | None) -> list[Any]:
691
+ """Analyze text past the spaCy ceiling window by window, merged into one result set.
692
+
693
+ Results are re-based onto absolute offsets in *content* and deduped
694
+ across the overlap, keeping the highest-scoring hit for each span.
695
+ """
696
+ merged: dict[tuple[str, int, int], Any] = {}
697
+ for chunk, offset in self._auto_chunks(content):
698
+ for result in self._analyze_once(chunk, entities=entities):
699
+ result.start += offset
700
+ result.end += offset
701
+ key = (result.entity_type, result.start, result.end)
702
+ existing = merged.get(key)
703
+ if existing is None or result.score > existing.score:
704
+ merged[key] = result
705
+ return sorted(merged.values(), key=lambda r: (r.start, r.end))
706
+
707
+ def _analyze_once(self, content: str, *, entities: list[str] | None = None) -> list[Any]:
708
+ """One Presidio pass over text already known to fit the spaCy window."""
666
709
  try:
667
710
  return self.analyzer.analyze(text=content, language="en", entities=entities)
668
711
  except ModuleNotFoundError as exc:
@@ -983,14 +1026,64 @@ class PIIDetector(BaseDetector):
983
1026
  # Offloading to a thread keeps the loop alive and allows I/O to proceed.
984
1027
  return await asyncio.to_thread(self._detect_sync, content)
985
1028
 
986
- def _chunk_text(self, text: str) -> list[tuple[str, int]]:
987
- """Return (chunk, offset) pairs. When chunk_size is null returns the full text at offset 0."""
1029
+ def _spacy_char_limit(self) -> int:
1030
+ """Largest text spaCy will accept in a single call, honouring max_length."""
1031
+ return _unwrap_int(getattr(self._cfg, "max_length", None)) or self._SPACY_DEFAULT_MAX_LENGTH
1032
+
1033
+ def _iter_chunks(self, text: str) -> Iterator[tuple[str, int]]:
1034
+ """Yield (chunk, offset) pairs, one chunk resident at a time.
1035
+
1036
+ With chunk_size configured the text is split on that fixed window. With
1037
+ chunk_size null the text is passed through whole -- unless it exceeds
1038
+ the spaCy ceiling, in which case it is auto-chunked instead of raising
1039
+ E088 and losing every finding on the page.
1040
+ """
988
1041
  chunk_size = _unwrap_int(getattr(self._cfg, "chunk_size", None))
989
- if not chunk_size:
990
- return [(text, 0)]
991
- overlap = _unwrap_int(getattr(self._cfg, "chunk_overlap", None)) or 0
992
- step = max(1, chunk_size - overlap)
993
- return [(text[i : i + chunk_size], i) for i in range(0, len(text), step)]
1042
+ if chunk_size:
1043
+ overlap = _unwrap_int(getattr(self._cfg, "chunk_overlap", None)) or 0
1044
+ step = max(1, chunk_size - overlap)
1045
+ for start in range(0, len(text), step):
1046
+ yield (text[start : start + chunk_size], start)
1047
+ return
1048
+
1049
+ if len(text) <= self._spacy_char_limit():
1050
+ yield (text, 0)
1051
+ return
1052
+
1053
+ yield from self._auto_chunks(text)
1054
+
1055
+ def _auto_chunks(self, text: str) -> Iterator[tuple[str, int]]:
1056
+ """Split an oversized page into spaCy-sized windows on whitespace boundaries."""
1057
+ window = min(self._AUTO_CHUNK_CHARS, self._spacy_char_limit())
1058
+ overlap = min(self._AUTO_CHUNK_OVERLAP, window // 4)
1059
+ total = len(text)
1060
+ logger.info(
1061
+ "Page of %d chars exceeds the spaCy limit (%d); auto-chunking at %d chars "
1062
+ "(set chunk_size to control this explicitly)",
1063
+ total,
1064
+ self._spacy_char_limit(),
1065
+ window,
1066
+ )
1067
+
1068
+ start = 0
1069
+ while start < total:
1070
+ end = min(start + window, total)
1071
+ if end < total:
1072
+ # Prefer a line/word boundary inside the trailing overlap zone so
1073
+ # entities are not cut in half; the overlap re-covers the seam.
1074
+ split = text.rfind("\n", end - overlap, end)
1075
+ if split == -1:
1076
+ split = text.rfind(" ", end - overlap, end)
1077
+ if split > start:
1078
+ end = split + 1
1079
+ yield (text[start:end], start)
1080
+ if end >= total:
1081
+ return
1082
+ start = max(start + 1, end - overlap)
1083
+
1084
+ def _chunk_text(self, text: str) -> list[tuple[str, int]]:
1085
+ """Materialized :meth:`_iter_chunks` (kept for callers that need a list)."""
1086
+ return list(self._iter_chunks(text))
994
1087
 
995
1088
  def _detect_sync(self, content: str) -> list[DetectionResult]:
996
1089
  tabular_results = self._detect_tabular_content(content)
@@ -1004,8 +1097,11 @@ class PIIDetector(BaseDetector):
1004
1097
  threshold = self._cfg.confidence_threshold or 0.7
1005
1098
  results: list[DetectionResult] = []
1006
1099
  seen: set[tuple[str, int, int]] = set()
1100
+ # Precomputed once: content[:start].count("\n") per finding is O(n) each
1101
+ # and copies the prefix, which is quadratic on multi-megabyte pages.
1102
+ newline_offsets = [m.start() for m in re.finditer("\n", content)]
1007
1103
 
1008
- for chunk, offset in self._chunk_text(content):
1104
+ for chunk, offset in self._iter_chunks(content):
1009
1105
  for result in self._analyze_content(chunk, entities=entities):
1010
1106
  if not self._is_entity_enabled(result.entity_type):
1011
1107
  continue
@@ -1017,7 +1113,7 @@ class PIIDetector(BaseDetector):
1017
1113
  continue
1018
1114
  seen.add(dedup_key)
1019
1115
 
1020
- line_number = content[:abs_start].count("\n") + 1
1116
+ line_number = bisect.bisect_left(newline_offsets, abs_start) + 1
1021
1117
  matched_content = content[abs_start:abs_end]
1022
1118
 
1023
1119
  detection = self._build_detection_result(
@@ -432,7 +432,7 @@ class SecretsDetectorConfig(DetectorConfig):
432
432
  class MaxLength(RootModel[int]):
433
433
  root: int = Field(
434
434
  None,
435
- description="Override spaCy's nlp.max_length (default 1,000,000 chars). Set higher than your longest expected input to avoid the E088 error. Prefer chunk_size for very large texts.",
435
+ description="Override spaCy's nlp.max_length (default 1,000,000 chars). Raising it makes memory scale with input size (~1GB of scratch per 100,000 chars), so prefer chunk_size for very large texts. Oversized pages are auto-chunked rather than failing, so this rarely needs setting.",
436
436
  ge=1,
437
437
  )
438
438
 
@@ -440,7 +440,7 @@ class MaxLength(RootModel[int]):
440
440
  class ChunkSize(RootModel[int]):
441
441
  root: int = Field(
442
442
  None,
443
- description='Split text into chunks of this many characters before analysis. Findings from all chunks are merged with corrected offsets. When null the full text is passed as-is (subject to max_length).',
443
+ description='Split text into chunks of this many characters before analysis. Findings from all chunks are merged with corrected offsets. When null the full text is passed as-is, unless it exceeds the spaCy limit, in which case it is auto-chunked at 200,000 characters on whitespace boundaries.',
444
444
  ge=1,
445
445
  )
446
446
 
@@ -483,11 +483,11 @@ class PIIDetectorConfig(DetectorConfig):
483
483
  )
484
484
  max_length: MaxLength | None = Field(
485
485
  None,
486
- description="Override spaCy's nlp.max_length (default 1,000,000 chars). Set higher than your longest expected input to avoid the E088 error. Prefer chunk_size for very large texts.",
486
+ description="Override spaCy's nlp.max_length (default 1,000,000 chars). Raising it makes memory scale with input size (~1GB of scratch per 100,000 chars), so prefer chunk_size for very large texts. Oversized pages are auto-chunked rather than failing, so this rarely needs setting.",
487
487
  )
488
488
  chunk_size: ChunkSize | None = Field(
489
489
  None,
490
- description='Split text into chunks of this many characters before analysis. Findings from all chunks are merged with corrected offsets. When null the full text is passed as-is (subject to max_length).',
490
+ description='Split text into chunks of this many characters before analysis. Findings from all chunks are merged with corrected offsets. When null the full text is passed as-is, unless it exceeds the spaCy limit, in which case it is auto-chunked at 200,000 characters on whitespace boundaries.',
491
491
  )
492
492
  chunk_overlap: ChunkOverlap | None = Field(
493
493
  0,
@@ -7,6 +7,12 @@ from pydantic import BaseModel
7
7
 
8
8
  OutputType = Literal["rest", "file", "console"]
9
9
 
10
+ # How long to wait for the API to answer a write-back. Generous enough that a
11
+ # bulk ingest of a large batch finishes well inside it — the old 30 s cut those
12
+ # off mid-flight and made urllib3 re-upload the same body — but finite, so a
13
+ # wedged API fails the run instead of hanging it forever.
14
+ DEFAULT_REST_TIMEOUT_SEC = 300
15
+
10
16
 
11
17
  @dataclass(frozen=True)
12
18
  class OutputRuntimeContext:
@@ -24,7 +30,8 @@ class OutputSettings:
24
30
  runner_id: str | None
25
31
  managed_runner: bool
26
32
  rest_url: str | None = None
27
- rest_timeout_sec: int = 30
33
+ # Read timeout in seconds; None disables it. See resolve_output_settings.
34
+ rest_timeout_sec: int | None = DEFAULT_REST_TIMEOUT_SEC
28
35
  file_path: str | None = None
29
36
 
30
37
 
@@ -4,7 +4,13 @@ import argparse
4
4
  import os
5
5
  from typing import Any, cast
6
6
 
7
- from .base import OutputRuntimeContext, OutputSettings, OutputSink, OutputType
7
+ from .base import (
8
+ DEFAULT_REST_TIMEOUT_SEC,
9
+ OutputRuntimeContext,
10
+ OutputSettings,
11
+ OutputSink,
12
+ OutputType,
13
+ )
8
14
  from .console import ConsoleOutputSink
9
15
  from .file import FileOutputSink
10
16
  from .rest import RestOutputSink
@@ -96,9 +102,17 @@ def resolve_output_settings(
96
102
  )
97
103
  rest_url = str(rest_url_value) if rest_url_value is not None else None
98
104
 
99
- rest_timeout_sec = _parse_int(_coalesce(env_rest_timeout, 30), 30)
100
- if rest_timeout_sec < 1:
101
- rest_timeout_sec = 30
105
+ # 5 minutes by default (see DEFAULT_REST_TIMEOUT_SEC): long enough that a
106
+ # bulk ingest of a large batch is never cut off mid-flight, short enough
107
+ # that a wedged API surfaces as a failure. Override with
108
+ # CLASSIFYRE_OUTPUT_REST_TIMEOUT_SEC; 0 or negative disables the read
109
+ # timeout entirely.
110
+ rest_timeout_sec: int | None = _parse_int(
111
+ _coalesce(env_rest_timeout, DEFAULT_REST_TIMEOUT_SEC),
112
+ DEFAULT_REST_TIMEOUT_SEC,
113
+ )
114
+ if rest_timeout_sec is not None and rest_timeout_sec < 1:
115
+ rest_timeout_sec = None
102
116
 
103
117
  file_path_value = _coalesce(
104
118
  getattr(args, "output_file_path", None),
@@ -55,9 +55,12 @@ class _JitteredRetry(Retry):
55
55
  # What we retry and why:
56
56
  # connect=8 — pod restarted / not yet ready (RemoteDisconnected, ConnectionReset,
57
57
  # ConnectTimeout). Request never reached the application.
58
- # read=8 — API is under load and slow to respond (ReadTimeout). Safe to retry
59
- # because all endpoints are idempotent (bulk ingest is upsert-based,
60
- # status/findings updates are set-operations).
58
+ # read=8 — API is under load and slow to respond (ReadTimeout), i.e. it did
59
+ # not answer within DEFAULT_REST_TIMEOUT_SEC. Safe to retry because
60
+ # all endpoints are idempotent (bulk ingest is upsert-based,
61
+ # status/findings updates are set-operations) — but note that a
62
+ # retry re-uploads the whole body, which is why that budget is
63
+ # generous rather than tight.
61
64
  # status=8 — transient HTTP errors from an overloaded or restarting API:
62
65
  # 408 Request Timeout - API-level timeout
63
66
  # 429 Too Many Requests - rate-limited / backpressure
@@ -76,8 +79,7 @@ class _JitteredRetry(Retry):
76
79
  # attempt 7 → ~60 s (capped)
77
80
  # attempt 8 → ~60 s (capped)
78
81
  # Total extra wait: ~182 s (~3 min) — covers extended load spikes on a
79
- # single-node VPS before event-loop pressure drops. Worst-case a single
80
- # call costs 8 * 120 s + 182 s = ~18 min, acceptable for long-running scans.
82
+ # single-node VPS before event-loop pressure drops.
81
83
  #
82
84
  # POST and PATCH are explicitly allowed: without this urllib3 only retries
83
85
  # idempotent methods (GET/HEAD) by default.
@@ -161,6 +163,12 @@ class ExternalRunnerResponse(BaseModel):
161
163
  source_id: str = Field(validation_alias="sourceId")
162
164
 
163
165
 
166
+ # Applied to connection establishment only, kept short so a refused or
167
+ # unreachable API fails fast and is retried by _RETRY_POLICY. How long the API
168
+ # may then take to answer is a separate budget (timeout_sec, 5 min by default).
169
+ _CONNECT_TIMEOUT_SEC = 30
170
+
171
+
164
172
  class RestOutputSink:
165
173
  output_type: OutputType = "rest"
166
174
 
@@ -169,7 +177,7 @@ class RestOutputSink:
169
177
  context: OutputRuntimeContext,
170
178
  *,
171
179
  base_url: str,
172
- timeout_sec: int,
180
+ timeout_sec: int | None,
173
181
  ):
174
182
  self.context = context
175
183
  self.batch_size = context.batch_size
@@ -443,7 +451,9 @@ class RestOutputSink:
443
451
  method=method,
444
452
  url=url,
445
453
  json=payload,
446
- timeout=self.timeout_sec,
454
+ # (connect, read): the read budget is the API's whole processing
455
+ # time, not just the first byte. None waits indefinitely.
456
+ timeout=(_CONNECT_TIMEOUT_SEC, self.timeout_sec),
447
457
  )
448
458
 
449
459
  if response.status_code >= 400:
@@ -10,6 +10,7 @@ from urllib.parse import quote
10
10
  import requests
11
11
 
12
12
  from ...models.generated_input import SandboxInput
13
+ from ...outputs.base import DEFAULT_REST_TIMEOUT_SEC
13
14
  from ..object_storage.base import ObjectRef, ObjectStorageSourceBase
14
15
 
15
16
  logger = logging.getLogger(__name__)
@@ -48,15 +49,19 @@ class SandboxSource(ObjectStorageSourceBase):
48
49
  return default
49
50
 
50
51
  def _max_object_bytes(self) -> int:
51
- # Keep the extractor aligned with the API's per-upload limit instead
52
- # of the object-storage connector family's conservative 5 MiB default.
52
+ # Spool threshold (RAM before spilling to disk), not a refusal ceiling —
53
+ # uploads are no longer size-capped. Raised above the object-storage
54
+ # family's conservative 5 MiB default because these files are local.
53
55
  return 50 * 1024 * 1024
54
56
 
55
57
  def _request(self, method: str, path: str, *, stream: bool = False) -> requests.Response:
56
58
  response = self._session.request(
57
59
  method,
58
60
  f"{self._api_url}{path}",
59
- timeout=120,
61
+ # (connect, read). The read budget applies per socket read, so 5
62
+ # minutes is generous for a streamed download of any size; the old
63
+ # flat 120 s applied to connect and read alike.
64
+ timeout=(30, DEFAULT_REST_TIMEOUT_SEC),
60
65
  stream=stream,
61
66
  headers={"Connection": "close"},
62
67
  )
@@ -89,6 +89,56 @@ async def test_text_classification_runner_applies_severity_map() -> None:
89
89
  assert findings[0].severity == Severity.high
90
90
 
91
91
 
92
+ @pytest.mark.asyncio
93
+ async def test_text_classification_short_text_is_a_single_call() -> None:
94
+ runner = _make_text_cls_runner([{"label": "SPAM", "score": 0.95}])
95
+
96
+ runner.detect("Win a free iPhone now!", "text/plain")
97
+
98
+ assert runner._pipe.call_count == 1
99
+
100
+
101
+ @pytest.mark.asyncio
102
+ async def test_text_classification_long_text_is_chunked_not_truncated() -> None:
103
+ # A pipeline called with truncation=True keeps only the first ~512 tokens,
104
+ # so an unchunked long document was classified on its opening lines alone
105
+ # and the rest was silently discarded.
106
+ runner = _make_text_cls_runner([{"label": "SPAM", "score": 0.95}])
107
+ text = "spam and eggs " * 5_000 # 70k chars, far past any model window
108
+
109
+ runner.detect(text, "text/plain")
110
+
111
+ assert runner._pipe.call_count > 1
112
+ seen = "".join(call.args[0] for call in runner._pipe.call_args_list)
113
+ # Overlapping windows, so the concatenation is longer than the input -- the
114
+ # point is that no part of it went unseen.
115
+ assert len(seen) >= len(text)
116
+ assert all(len(call.args[0]) <= 4_000 for call in runner._pipe.call_args_list)
117
+
118
+
119
+ @pytest.mark.asyncio
120
+ async def test_text_classification_chunk_cap_is_bounded() -> None:
121
+ from src.detectors.custom.runners._text_classification import _MAX_AUTO_CHUNKS
122
+
123
+ runner = _make_text_cls_runner([{"label": "SPAM", "score": 0.95}])
124
+
125
+ runner.detect("spam and eggs " * 200_000, "text/plain")
126
+
127
+ assert runner._pipe.call_count <= _MAX_AUTO_CHUNKS
128
+
129
+
130
+ @pytest.mark.asyncio
131
+ async def test_text_classification_scores_are_max_across_chunks() -> None:
132
+ runner = _make_text_cls_runner([])
133
+ runner._pipe = MagicMock(
134
+ side_effect=[[{"label": "SPAM", "score": s}] for s in (0.1, 0.99, 0.2)] * 200
135
+ )
136
+
137
+ findings = runner.detect("spam and eggs " * 400, "text/plain")
138
+
139
+ assert findings[0].confidence == pytest.approx(0.99)
140
+
141
+
92
142
  def test_text_classification_run_raises_not_implemented() -> None:
93
143
  runner = _make_text_cls_runner([])
94
144
  with pytest.raises(NotImplementedError):