classifyre-cli 0.4.85__tar.gz → 0.4.86__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 (281) hide show
  1. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/.turbo/turbo-build.log +1 -1
  2. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/PKG-INFO +1 -1
  3. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/package.json +1 -1
  4. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/pyproject.toml +1 -1
  5. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/custom/runners/_gliner2.py +11 -1
  6. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/custom/runners/_text_classification.py +68 -1
  7. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/pii/detector.py +75 -9
  8. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/models/generated_detectors.py +4 -4
  9. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/custom/test_transformer_runners.py +50 -0
  10. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/pii/test_pii_chunking.py +78 -0
  11. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/uv.lock +2 -2
  12. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/.gitignore +0 -0
  13. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/.python-version +0 -0
  14. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/README.md +0 -0
  15. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/main.py +0 -0
  16. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/scripts/generate_models.py +0 -0
  17. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/__init__.py +0 -0
  18. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/config.py +0 -0
  19. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/__init__.py +0 -0
  20. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/base.py +0 -0
  21. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/broken_links/__init__.py +0 -0
  22. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/broken_links/detector.py +0 -0
  23. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/config.py +0 -0
  24. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/content/__init__.py +0 -0
  25. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/custom/__init__.py +0 -0
  26. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/custom/detector.py +0 -0
  27. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/custom/extractor.py +0 -0
  28. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/custom/runners/__init__.py +0 -0
  29. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/custom/runners/_base.py +0 -0
  30. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/custom/runners/_factory.py +0 -0
  31. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/custom/runners/_image_classification.py +0 -0
  32. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/custom/runners/_llm.py +0 -0
  33. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/custom/runners/_object_detection.py +0 -0
  34. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/custom/runners/_regex.py +0 -0
  35. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/custom/trainer.py +0 -0
  36. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/dependencies.py +0 -0
  37. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/engine_version.py +0 -0
  38. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/pii/__init__.py +0 -0
  39. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/secrets/__init__.py +0 -0
  40. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/secrets/detector.py +0 -0
  41. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/threat/__init__.py +0 -0
  42. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/threat/code_security_detector.py +0 -0
  43. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/threat/yara_detector.py +0 -0
  44. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/file_evaluation/__init__.py +0 -0
  45. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/file_evaluation/runner.py +0 -0
  46. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/main.py +0 -0
  47. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/models/generated_input.py +0 -0
  48. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/models/generated_single_asset_scan_results.py +0 -0
  49. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/outputs/__init__.py +0 -0
  50. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/outputs/base.py +0 -0
  51. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/outputs/console.py +0 -0
  52. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/outputs/factory.py +0 -0
  53. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/outputs/file.py +0 -0
  54. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/outputs/rest.py +0 -0
  55. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/pipeline/__init__.py +0 -0
  56. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/pipeline/content_provider.py +0 -0
  57. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/pipeline/detector_pipeline.py +0 -0
  58. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/pipeline/parsed_content_provider.py +0 -0
  59. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/pipeline/payload_window.py +0 -0
  60. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/pipeline/scan_cache.py +0 -0
  61. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/pipeline/text_artifact.py +0 -0
  62. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/pipeline/worker_pool.py +0 -0
  63. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/__init__.py +0 -0
  64. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/asset_metadata.py +0 -0
  65. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/atlassian_common.py +0 -0
  66. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/azure_blob_storage/__init__.py +0 -0
  67. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/azure_blob_storage/source.py +0 -0
  68. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/base.py +0 -0
  69. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/confluence/__init__.py +0 -0
  70. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/confluence/source.py +0 -0
  71. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/databricks/__init__.py +0 -0
  72. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/databricks/source.py +0 -0
  73. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/delta_lake/__init__.py +0 -0
  74. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/delta_lake/source.py +0 -0
  75. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/dependencies.py +0 -0
  76. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/dropbox/__init__.py +0 -0
  77. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/dropbox/auth.py +0 -0
  78. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/dropbox/source.py +0 -0
  79. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/elasticsearch/__init__.py +0 -0
  80. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/elasticsearch/source.py +0 -0
  81. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/email/__init__.py +0 -0
  82. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/email/source.py +0 -0
  83. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/google_cloud_storage/__init__.py +0 -0
  84. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/google_cloud_storage/source.py +0 -0
  85. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/google_workspace/__init__.py +0 -0
  86. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/google_workspace/source.py +0 -0
  87. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/hive/__init__.py +0 -0
  88. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/hive/source.py +0 -0
  89. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/hugging_face/__init__.py +0 -0
  90. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/hugging_face/source.py +0 -0
  91. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/iceberg/__init__.py +0 -0
  92. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/iceberg/source.py +0 -0
  93. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/jira/__init__.py +0 -0
  94. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/jira/source.py +0 -0
  95. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/kafka/__init__.py +0 -0
  96. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/kafka/rest.py +0 -0
  97. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/kafka/source.py +0 -0
  98. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/lakehouse_base.py +0 -0
  99. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/local_folder/__init__.py +0 -0
  100. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/local_folder/source.py +0 -0
  101. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/meilisearch/__init__.py +0 -0
  102. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/meilisearch/source.py +0 -0
  103. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/microsoft_365/__init__.py +0 -0
  104. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/microsoft_365/source.py +0 -0
  105. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/mongodb/__init__.py +0 -0
  106. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/mongodb/source.py +0 -0
  107. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/mssql/__init__.py +0 -0
  108. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/mssql/source.py +0 -0
  109. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/mysql/__init__.py +0 -0
  110. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/mysql/source.py +0 -0
  111. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/neo4j/__init__.py +0 -0
  112. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/neo4j/source.py +0 -0
  113. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/notion/__init__.py +0 -0
  114. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/notion/client.py +0 -0
  115. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/notion/source.py +0 -0
  116. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/object_storage/base.py +0 -0
  117. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/opensearch/__init__.py +0 -0
  118. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/opensearch/source.py +0 -0
  119. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/oracle/__init__.py +0 -0
  120. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/oracle/source.py +0 -0
  121. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/postgresql/__init__.py +0 -0
  122. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/postgresql/source.py +0 -0
  123. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/powerbi/__init__.py +0 -0
  124. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/powerbi/source.py +0 -0
  125. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/recipe_normalizer.py +0 -0
  126. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/reddit/__init__.py +0 -0
  127. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/reddit/source.py +0 -0
  128. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/s3_client.py +0 -0
  129. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/s3_compatible_storage/README.md +0 -0
  130. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/s3_compatible_storage/__init__.py +0 -0
  131. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/s3_compatible_storage/source.py +0 -0
  132. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/sandbox/__init__.py +0 -0
  133. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/sandbox/source.py +0 -0
  134. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/search_engine_base.py +0 -0
  135. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/servicedesk/__init__.py +0 -0
  136. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/servicedesk/source.py +0 -0
  137. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/slack/__init__.py +0 -0
  138. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/slack/source.py +0 -0
  139. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/snowflake/__init__.py +0 -0
  140. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/snowflake/source.py +0 -0
  141. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/sqlite/__init__.py +0 -0
  142. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/sqlite/source.py +0 -0
  143. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/tableau/__init__.py +0 -0
  144. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/tableau/source.py +0 -0
  145. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/tabular_base.py +0 -0
  146. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/tabular_utils.py +0 -0
  147. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/wordpress/__init__.py +0 -0
  148. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/wordpress/source.py +0 -0
  149. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/youtube/__init__.py +0 -0
  150. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/youtube/source.py +0 -0
  151. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/telemetry.py +0 -0
  152. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/utils/__init__.py +0 -0
  153. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/utils/archive_extraction.py +0 -0
  154. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/utils/content_extraction.py +0 -0
  155. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/utils/dependency_groups.py +0 -0
  156. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/utils/embedded_files.py +0 -0
  157. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/utils/external_video.py +0 -0
  158. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/utils/file_metadata.py +0 -0
  159. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/utils/file_parser.py +0 -0
  160. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/utils/file_to_images.py +0 -0
  161. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/utils/hashing.py +0 -0
  162. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/utils/legacy_office.py +0 -0
  163. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/utils/payload.py +0 -0
  164. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/utils/range_reader.py +0 -0
  165. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/utils/resources.py +0 -0
  166. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/utils/transcription.py +0 -0
  167. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/utils/uv_sync.py +0 -0
  168. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/utils/validation.py +0 -0
  169. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/utils/video_processing.py +0 -0
  170. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/__init__.py +0 -0
  171. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/_lakehouse_fakes.py +0 -0
  172. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/conftest.py +0 -0
  173. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/__init__.py +0 -0
  174. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/broken_links/test_broken_links_detector.py +0 -0
  175. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/conftest.py +0 -0
  176. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/content/__init__.py +0 -0
  177. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/custom/__init__.py +0 -0
  178. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/custom/conftest.py +0 -0
  179. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/custom/test_gliner2_classification.py +0 -0
  180. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/custom/test_invoice_extraction.py +0 -0
  181. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/custom/test_llm_runner.py +0 -0
  182. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/custom/test_pipeline_integration.py +0 -0
  183. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/custom/test_regex_runner.py +0 -0
  184. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/custom/test_runner_hardening.py +0 -0
  185. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/pii/__init__.py +0 -0
  186. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/pii/conftest.py +0 -0
  187. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/pii/sample_invoice.pdf +0 -0
  188. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/pii/test_pii_detector.py +0 -0
  189. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/pii/test_pii_detector_extended.py +0 -0
  190. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/pii/test_pii_regional_entities.py +0 -0
  191. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/pii/test_pii_severity.py +0 -0
  192. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/secrets/__init__.py +0 -0
  193. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/secrets/test_secrets_detector.py +0 -0
  194. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/secrets/test_secrets_detector_extended.py +0 -0
  195. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/test_base_detector.py +0 -0
  196. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/test_custom_detector_examples_runtime.py +0 -0
  197. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/test_detector_catalog_commercial.py +0 -0
  198. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/test_detector_pipeline_types.py +0 -0
  199. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/test_detector_schema_examples.py +0 -0
  200. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/test_detector_types.py +0 -0
  201. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/test_phase2_detectors.py +0 -0
  202. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/test_registry.py +0 -0
  203. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/threat/__init__.py +0 -0
  204. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/threat/test_code_security_detector.py +0 -0
  205. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/threat/test_yara_detector.py +0 -0
  206. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/integration/test_wordpress_broken_links_detector.py +0 -0
  207. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/integration/test_wordpress_links_assets.py +0 -0
  208. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/pipeline/test_detector_outcomes.py +0 -0
  209. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/pipeline/test_detector_pipeline.py +0 -0
  210. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/pipeline/test_worker_pool.py +0 -0
  211. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_assets_metadata_catalog.py +0 -0
  212. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_azure_blob_storage_source.py +0 -0
  213. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_base_source_attachment.py +0 -0
  214. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_base_source_sampling.py +0 -0
  215. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_byte_valued_fields.py +0 -0
  216. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_config.py +0 -0
  217. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_confluence_source.py +0 -0
  218. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_custom_extractor.py +0 -0
  219. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_databricks_source.py +0 -0
  220. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_delta_lake_source.py +0 -0
  221. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_dependency_groups.py +0 -0
  222. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_dropbox_auth_command.py +0 -0
  223. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_dropbox_source.py +0 -0
  224. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_elasticsearch_source.py +0 -0
  225. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_email_source.py +0 -0
  226. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_file_evaluation_runner.py +0 -0
  227. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_google_cloud_storage_source.py +0 -0
  228. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_google_workspace_source.py +0 -0
  229. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_hashing.py +0 -0
  230. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_hive_source.py +0 -0
  231. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_hugging_face_source.py +0 -0
  232. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_iceberg_source.py +0 -0
  233. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_jira_source.py +0 -0
  234. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_kafka_source.py +0 -0
  235. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_local_folder_source.py +0 -0
  236. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_main.py +0 -0
  237. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_meilisearch_source.py +0 -0
  238. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_microsoft_365_source.py +0 -0
  239. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_mongodb_source.py +0 -0
  240. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_mssql_source.py +0 -0
  241. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_mysql_source.py +0 -0
  242. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_neo4j_source.py +0 -0
  243. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_notion_source.py +0 -0
  244. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_object_storage_download_efficiency.py +0 -0
  245. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_opensearch_source.py +0 -0
  246. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_oracle_source.py +0 -0
  247. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_outputs.py +0 -0
  248. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_payload_window.py +0 -0
  249. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_payload_window_parquet.py +0 -0
  250. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_postgresql_source.py +0 -0
  251. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_powerbi_source.py +0 -0
  252. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_range_reading.py +0 -0
  253. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_recipe_normalizer.py +0 -0
  254. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_reddit_source.py +0 -0
  255. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_s3_compatible_storage_source.py +0 -0
  256. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_sampling_automatic.py +0 -0
  257. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_sandbox_source.py +0 -0
  258. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_scan_cache.py +0 -0
  259. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_servicedesk_source.py +0 -0
  260. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_slack_source.py +0 -0
  261. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_snowflake_source.py +0 -0
  262. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_source_dependency_groups.py +0 -0
  263. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_source_strategy_conformance.py +0 -0
  264. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_sqlite_source.py +0 -0
  265. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_tableau_source.py +0 -0
  266. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_tabular_automatic_sampling.py +0 -0
  267. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_tabular_utils.py +0 -0
  268. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_uv_sync.py +0 -0
  269. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_wordpress_source.py +0 -0
  270. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_youtube_source.py +0 -0
  271. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_youtube_source_integration.py +0 -0
  272. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/utils/test_archive_extraction.py +0 -0
  273. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/utils/test_content_extraction.py +0 -0
  274. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/utils/test_embedded_files.py +0 -0
  275. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/utils/test_file_metadata.py +0 -0
  276. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/utils/test_file_parser.py +0 -0
  277. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/utils/test_file_to_images.py +0 -0
  278. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/utils/test_legacy_office.py +0 -0
  279. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/utils/test_payload.py +0 -0
  280. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/utils/test_transcription.py +0 -0
  281. {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/utils/test_video_processing.py +0 -0
@@ -1,3 +1,3 @@
1
1
  $ uv sync
2
- Resolved 307 packages in 272ms
2
+ Resolved 307 packages in 247ms
3
3
  Checked 51 packages in 1ms
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: classifyre-cli
3
- Version: 0.4.85
3
+ Version: 0.4.86
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.86",
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.86"
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 = (
@@ -983,14 +996,64 @@ class PIIDetector(BaseDetector):
983
996
  # Offloading to a thread keeps the loop alive and allows I/O to proceed.
984
997
  return await asyncio.to_thread(self._detect_sync, content)
985
998
 
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."""
999
+ def _spacy_char_limit(self) -> int:
1000
+ """Largest text spaCy will accept in a single call, honouring max_length."""
1001
+ return _unwrap_int(getattr(self._cfg, "max_length", None)) or self._SPACY_DEFAULT_MAX_LENGTH
1002
+
1003
+ def _iter_chunks(self, text: str) -> Iterator[tuple[str, int]]:
1004
+ """Yield (chunk, offset) pairs, one chunk resident at a time.
1005
+
1006
+ With chunk_size configured the text is split on that fixed window. With
1007
+ chunk_size null the text is passed through whole -- unless it exceeds
1008
+ the spaCy ceiling, in which case it is auto-chunked instead of raising
1009
+ E088 and losing every finding on the page.
1010
+ """
988
1011
  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)]
1012
+ if chunk_size:
1013
+ overlap = _unwrap_int(getattr(self._cfg, "chunk_overlap", None)) or 0
1014
+ step = max(1, chunk_size - overlap)
1015
+ for start in range(0, len(text), step):
1016
+ yield (text[start : start + chunk_size], start)
1017
+ return
1018
+
1019
+ if len(text) <= self._spacy_char_limit():
1020
+ yield (text, 0)
1021
+ return
1022
+
1023
+ yield from self._auto_chunks(text)
1024
+
1025
+ def _auto_chunks(self, text: str) -> Iterator[tuple[str, int]]:
1026
+ """Split an oversized page into spaCy-sized windows on whitespace boundaries."""
1027
+ window = min(self._AUTO_CHUNK_CHARS, self._spacy_char_limit())
1028
+ overlap = min(self._AUTO_CHUNK_OVERLAP, window // 4)
1029
+ total = len(text)
1030
+ logger.info(
1031
+ "Page of %d chars exceeds the spaCy limit (%d); auto-chunking at %d chars "
1032
+ "(set chunk_size to control this explicitly)",
1033
+ total,
1034
+ self._spacy_char_limit(),
1035
+ window,
1036
+ )
1037
+
1038
+ start = 0
1039
+ while start < total:
1040
+ end = min(start + window, total)
1041
+ if end < total:
1042
+ # Prefer a line/word boundary inside the trailing overlap zone so
1043
+ # entities are not cut in half; the overlap re-covers the seam.
1044
+ split = text.rfind("\n", end - overlap, end)
1045
+ if split == -1:
1046
+ split = text.rfind(" ", end - overlap, end)
1047
+ if split > start:
1048
+ end = split + 1
1049
+ yield (text[start:end], start)
1050
+ if end >= total:
1051
+ return
1052
+ start = max(start + 1, end - overlap)
1053
+
1054
+ def _chunk_text(self, text: str) -> list[tuple[str, int]]:
1055
+ """Materialized :meth:`_iter_chunks` (kept for callers that need a list)."""
1056
+ return list(self._iter_chunks(text))
994
1057
 
995
1058
  def _detect_sync(self, content: str) -> list[DetectionResult]:
996
1059
  tabular_results = self._detect_tabular_content(content)
@@ -1004,8 +1067,11 @@ class PIIDetector(BaseDetector):
1004
1067
  threshold = self._cfg.confidence_threshold or 0.7
1005
1068
  results: list[DetectionResult] = []
1006
1069
  seen: set[tuple[str, int, int]] = set()
1070
+ # Precomputed once: content[:start].count("\n") per finding is O(n) each
1071
+ # and copies the prefix, which is quadratic on multi-megabyte pages.
1072
+ newline_offsets = [m.start() for m in re.finditer("\n", content)]
1007
1073
 
1008
- for chunk, offset in self._chunk_text(content):
1074
+ for chunk, offset in self._iter_chunks(content):
1009
1075
  for result in self._analyze_content(chunk, entities=entities):
1010
1076
  if not self._is_entity_enabled(result.entity_type):
1011
1077
  continue
@@ -1017,7 +1083,7 @@ class PIIDetector(BaseDetector):
1017
1083
  continue
1018
1084
  seen.add(dedup_key)
1019
1085
 
1020
- line_number = content[:abs_start].count("\n") + 1
1086
+ line_number = bisect.bisect_left(newline_offsets, abs_start) + 1
1021
1087
  matched_content = content[abs_start:abs_end]
1022
1088
 
1023
1089
  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,
@@ -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):
@@ -10,6 +10,8 @@ produced nothing for the whole corpus.
10
10
  These tests need no Presidio: they exercise the config plumbing directly.
11
11
  """
12
12
 
13
+ import itertools
14
+
13
15
  from src.detectors.pii.detector import PIIDetector, _unwrap_int
14
16
  from src.models.generated_detectors import ChunkOverlap, ChunkSize, MaxLength
15
17
 
@@ -119,3 +121,79 @@ class TestMaxLength:
119
121
 
120
122
  def test_unset_max_length_is_none(self):
121
123
  assert _unwrap_int(None) is None
124
+
125
+ def test_limit_defaults_to_spacy_default(self):
126
+ detector = _detector_with(_Cfg())
127
+
128
+ assert detector._spacy_char_limit() == PIIDetector._SPACY_DEFAULT_MAX_LENGTH
129
+
130
+ def test_limit_follows_configured_max_length(self):
131
+ detector = _detector_with(_Cfg(max_length=MaxLength(root=2_000_000)))
132
+
133
+ assert detector._spacy_char_limit() == 2_000_000
134
+
135
+
136
+ class TestAutoChunking:
137
+ """A page longer than spaCy's ceiling used to raise E088 inside Presidio,
138
+ which the detector swallowed -- so every finding on that page was lost.
139
+ Oversized pages are now split into bounded windows instead."""
140
+
141
+ def _oversized(self, chars: int) -> str:
142
+ # Word-shaped text so boundary snapping has somewhere to cut.
143
+ return ("lorem ipsum dolor sit amet " * ((chars // 27) + 1))[:chars]
144
+
145
+ def test_text_under_the_limit_is_not_chunked(self):
146
+ detector = _detector_with(_Cfg())
147
+
148
+ assert detector._chunk_text("some text") == [("some text", 0)]
149
+
150
+ def test_oversized_text_is_split_without_config(self):
151
+ detector = _detector_with(_Cfg())
152
+ text = self._oversized(PIIDetector._SPACY_DEFAULT_MAX_LENGTH + 500_000)
153
+
154
+ chunks = detector._chunk_text(text)
155
+
156
+ assert len(chunks) > 1
157
+ assert all(len(chunk) <= PIIDetector._AUTO_CHUNK_CHARS for chunk, _ in chunks)
158
+
159
+ def test_chunks_cover_the_whole_text_at_correct_offsets(self):
160
+ detector = _detector_with(_Cfg())
161
+ text = self._oversized(PIIDetector._SPACY_DEFAULT_MAX_LENGTH + 12_345)
162
+
163
+ chunks = detector._chunk_text(text)
164
+
165
+ # Every chunk must sit at the offset it claims, or finding positions
166
+ # reported back to the API point at the wrong bytes.
167
+ assert all(text[offset : offset + len(chunk)] == chunk for chunk, offset in chunks)
168
+ assert chunks[0][1] == 0
169
+ last_chunk, last_offset = chunks[-1]
170
+ assert last_offset + len(last_chunk) == len(text)
171
+ # No gaps between consecutive windows.
172
+ for (chunk, offset), (_, next_offset) in itertools.pairwise(chunks):
173
+ assert next_offset <= offset + len(chunk)
174
+
175
+ def test_a_lower_max_length_tightens_the_window(self):
176
+ detector = _detector_with(_Cfg(max_length=MaxLength(root=50_000)))
177
+ text = self._oversized(120_000)
178
+
179
+ chunks = detector._chunk_text(text)
180
+
181
+ assert all(len(chunk) <= 50_000 for chunk, _ in chunks)
182
+
183
+ def test_explicit_chunk_size_still_wins_over_auto(self):
184
+ detector = _detector_with(_Cfg(chunk_size=ChunkSize(root=10), chunk_overlap=None))
185
+ text = self._oversized(PIIDetector._SPACY_DEFAULT_MAX_LENGTH + 1)
186
+
187
+ first, offset = next(iter(detector._iter_chunks(text)))
188
+
189
+ assert offset == 0
190
+ assert len(first) == 10
191
+
192
+ def test_text_without_whitespace_still_terminates(self):
193
+ detector = _detector_with(_Cfg(max_length=MaxLength(root=1_000)))
194
+ text = "a" * 5_000
195
+
196
+ chunks = detector._chunk_text(text)
197
+
198
+ assert chunks[-1][1] + len(chunks[-1][0]) == len(text)
199
+ assert all(len(chunk) <= 1_000 for chunk, _ in chunks)
@@ -830,7 +830,7 @@ wheels = [
830
830
 
831
831
  [[package]]
832
832
  name = "classifyre-cli"
833
- version = "0.4.85"
833
+ version = "0.4.86"
834
834
  source = { editable = "." }
835
835
  dependencies = [
836
836
  { name = "beautifulsoup4" },
@@ -1369,7 +1369,7 @@ youtube = [
1369
1369
 
1370
1370
  [[package]]
1371
1371
  name = "classifyre-schemas"
1372
- version = "0.4.85"
1372
+ version = "0.4.86"
1373
1373
  source = { editable = "../../packages/schemas" }
1374
1374
  dependencies = [
1375
1375
  { name = "fastjsonschema" },
File without changes