classifyre-cli 0.4.86__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 (282) hide show
  1. classifyre_cli-0.4.87/.turbo/turbo-build.log +3 -0
  2. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/PKG-INFO +1 -1
  3. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/package.json +1 -1
  4. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/pyproject.toml +1 -1
  5. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/detectors/pii/detector.py +30 -0
  6. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/outputs/base.py +8 -1
  7. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/outputs/factory.py +18 -4
  8. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/outputs/rest.py +17 -7
  9. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/sandbox/source.py +8 -3
  10. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/detectors/pii/test_pii_chunking.py +128 -0
  11. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_outputs.py +50 -12
  12. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/uv.lock +2 -2
  13. classifyre_cli-0.4.86/.turbo/turbo-build.log +0 -3
  14. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/.gitignore +0 -0
  15. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/.python-version +0 -0
  16. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/README.md +0 -0
  17. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/main.py +0 -0
  18. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/scripts/generate_models.py +0 -0
  19. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/__init__.py +0 -0
  20. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/config.py +0 -0
  21. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/detectors/__init__.py +0 -0
  22. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/detectors/base.py +0 -0
  23. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/detectors/broken_links/__init__.py +0 -0
  24. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/detectors/broken_links/detector.py +0 -0
  25. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/detectors/config.py +0 -0
  26. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/detectors/content/__init__.py +0 -0
  27. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/detectors/custom/__init__.py +0 -0
  28. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/detectors/custom/detector.py +0 -0
  29. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/detectors/custom/extractor.py +0 -0
  30. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/detectors/custom/runners/__init__.py +0 -0
  31. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/detectors/custom/runners/_base.py +0 -0
  32. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/detectors/custom/runners/_factory.py +0 -0
  33. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/detectors/custom/runners/_gliner2.py +0 -0
  34. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/detectors/custom/runners/_image_classification.py +0 -0
  35. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/detectors/custom/runners/_llm.py +0 -0
  36. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/detectors/custom/runners/_object_detection.py +0 -0
  37. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/detectors/custom/runners/_regex.py +0 -0
  38. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/detectors/custom/runners/_text_classification.py +0 -0
  39. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/detectors/custom/trainer.py +0 -0
  40. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/detectors/dependencies.py +0 -0
  41. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/detectors/engine_version.py +0 -0
  42. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/detectors/pii/__init__.py +0 -0
  43. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/detectors/secrets/__init__.py +0 -0
  44. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/detectors/secrets/detector.py +0 -0
  45. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/detectors/threat/__init__.py +0 -0
  46. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/detectors/threat/code_security_detector.py +0 -0
  47. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/detectors/threat/yara_detector.py +0 -0
  48. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/file_evaluation/__init__.py +0 -0
  49. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/file_evaluation/runner.py +0 -0
  50. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/main.py +0 -0
  51. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/models/generated_detectors.py +0 -0
  52. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/models/generated_input.py +0 -0
  53. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/models/generated_single_asset_scan_results.py +0 -0
  54. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/outputs/__init__.py +0 -0
  55. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/outputs/console.py +0 -0
  56. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/outputs/file.py +0 -0
  57. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/pipeline/__init__.py +0 -0
  58. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/pipeline/content_provider.py +0 -0
  59. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/pipeline/detector_pipeline.py +0 -0
  60. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/pipeline/parsed_content_provider.py +0 -0
  61. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/pipeline/payload_window.py +0 -0
  62. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/pipeline/scan_cache.py +0 -0
  63. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/pipeline/text_artifact.py +0 -0
  64. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/pipeline/worker_pool.py +0 -0
  65. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/__init__.py +0 -0
  66. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/asset_metadata.py +0 -0
  67. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/atlassian_common.py +0 -0
  68. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/azure_blob_storage/__init__.py +0 -0
  69. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/azure_blob_storage/source.py +0 -0
  70. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/base.py +0 -0
  71. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/confluence/__init__.py +0 -0
  72. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/confluence/source.py +0 -0
  73. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/databricks/__init__.py +0 -0
  74. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/databricks/source.py +0 -0
  75. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/delta_lake/__init__.py +0 -0
  76. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/delta_lake/source.py +0 -0
  77. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/dependencies.py +0 -0
  78. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/dropbox/__init__.py +0 -0
  79. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/dropbox/auth.py +0 -0
  80. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/dropbox/source.py +0 -0
  81. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/elasticsearch/__init__.py +0 -0
  82. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/elasticsearch/source.py +0 -0
  83. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/email/__init__.py +0 -0
  84. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/email/source.py +0 -0
  85. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/google_cloud_storage/__init__.py +0 -0
  86. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/google_cloud_storage/source.py +0 -0
  87. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/google_workspace/__init__.py +0 -0
  88. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/google_workspace/source.py +0 -0
  89. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/hive/__init__.py +0 -0
  90. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/hive/source.py +0 -0
  91. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/hugging_face/__init__.py +0 -0
  92. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/hugging_face/source.py +0 -0
  93. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/iceberg/__init__.py +0 -0
  94. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/iceberg/source.py +0 -0
  95. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/jira/__init__.py +0 -0
  96. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/jira/source.py +0 -0
  97. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/kafka/__init__.py +0 -0
  98. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/kafka/rest.py +0 -0
  99. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/kafka/source.py +0 -0
  100. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/lakehouse_base.py +0 -0
  101. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/local_folder/__init__.py +0 -0
  102. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/local_folder/source.py +0 -0
  103. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/meilisearch/__init__.py +0 -0
  104. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/meilisearch/source.py +0 -0
  105. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/microsoft_365/__init__.py +0 -0
  106. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/microsoft_365/source.py +0 -0
  107. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/mongodb/__init__.py +0 -0
  108. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/mongodb/source.py +0 -0
  109. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/mssql/__init__.py +0 -0
  110. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/mssql/source.py +0 -0
  111. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/mysql/__init__.py +0 -0
  112. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/mysql/source.py +0 -0
  113. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/neo4j/__init__.py +0 -0
  114. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/neo4j/source.py +0 -0
  115. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/notion/__init__.py +0 -0
  116. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/notion/client.py +0 -0
  117. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/notion/source.py +0 -0
  118. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/object_storage/base.py +0 -0
  119. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/opensearch/__init__.py +0 -0
  120. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/opensearch/source.py +0 -0
  121. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/oracle/__init__.py +0 -0
  122. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/oracle/source.py +0 -0
  123. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/postgresql/__init__.py +0 -0
  124. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/postgresql/source.py +0 -0
  125. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/powerbi/__init__.py +0 -0
  126. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/powerbi/source.py +0 -0
  127. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/recipe_normalizer.py +0 -0
  128. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/reddit/__init__.py +0 -0
  129. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/reddit/source.py +0 -0
  130. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/s3_client.py +0 -0
  131. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/s3_compatible_storage/README.md +0 -0
  132. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/s3_compatible_storage/__init__.py +0 -0
  133. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/s3_compatible_storage/source.py +0 -0
  134. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/sandbox/__init__.py +0 -0
  135. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/search_engine_base.py +0 -0
  136. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/servicedesk/__init__.py +0 -0
  137. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/servicedesk/source.py +0 -0
  138. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/slack/__init__.py +0 -0
  139. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/slack/source.py +0 -0
  140. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/snowflake/__init__.py +0 -0
  141. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/snowflake/source.py +0 -0
  142. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/sqlite/__init__.py +0 -0
  143. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/sqlite/source.py +0 -0
  144. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/tableau/__init__.py +0 -0
  145. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/tableau/source.py +0 -0
  146. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/tabular_base.py +0 -0
  147. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/tabular_utils.py +0 -0
  148. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/wordpress/__init__.py +0 -0
  149. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/wordpress/source.py +0 -0
  150. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/youtube/__init__.py +0 -0
  151. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/sources/youtube/source.py +0 -0
  152. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/telemetry.py +0 -0
  153. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/utils/__init__.py +0 -0
  154. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/utils/archive_extraction.py +0 -0
  155. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/utils/content_extraction.py +0 -0
  156. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/utils/dependency_groups.py +0 -0
  157. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/utils/embedded_files.py +0 -0
  158. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/utils/external_video.py +0 -0
  159. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/utils/file_metadata.py +0 -0
  160. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/utils/file_parser.py +0 -0
  161. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/utils/file_to_images.py +0 -0
  162. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/utils/hashing.py +0 -0
  163. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/utils/legacy_office.py +0 -0
  164. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/utils/payload.py +0 -0
  165. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/utils/range_reader.py +0 -0
  166. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/utils/resources.py +0 -0
  167. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/utils/transcription.py +0 -0
  168. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/utils/uv_sync.py +0 -0
  169. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/utils/validation.py +0 -0
  170. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/src/utils/video_processing.py +0 -0
  171. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/__init__.py +0 -0
  172. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/_lakehouse_fakes.py +0 -0
  173. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/conftest.py +0 -0
  174. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/detectors/__init__.py +0 -0
  175. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/detectors/broken_links/test_broken_links_detector.py +0 -0
  176. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/detectors/conftest.py +0 -0
  177. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/detectors/content/__init__.py +0 -0
  178. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/detectors/custom/__init__.py +0 -0
  179. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/detectors/custom/conftest.py +0 -0
  180. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/detectors/custom/test_gliner2_classification.py +0 -0
  181. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/detectors/custom/test_invoice_extraction.py +0 -0
  182. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/detectors/custom/test_llm_runner.py +0 -0
  183. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/detectors/custom/test_pipeline_integration.py +0 -0
  184. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/detectors/custom/test_regex_runner.py +0 -0
  185. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/detectors/custom/test_runner_hardening.py +0 -0
  186. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/detectors/custom/test_transformer_runners.py +0 -0
  187. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/detectors/pii/__init__.py +0 -0
  188. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/detectors/pii/conftest.py +0 -0
  189. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/detectors/pii/sample_invoice.pdf +0 -0
  190. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/detectors/pii/test_pii_detector.py +0 -0
  191. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/detectors/pii/test_pii_detector_extended.py +0 -0
  192. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/detectors/pii/test_pii_regional_entities.py +0 -0
  193. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/detectors/pii/test_pii_severity.py +0 -0
  194. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/detectors/secrets/__init__.py +0 -0
  195. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/detectors/secrets/test_secrets_detector.py +0 -0
  196. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/detectors/secrets/test_secrets_detector_extended.py +0 -0
  197. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/detectors/test_base_detector.py +0 -0
  198. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/detectors/test_custom_detector_examples_runtime.py +0 -0
  199. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/detectors/test_detector_catalog_commercial.py +0 -0
  200. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/detectors/test_detector_pipeline_types.py +0 -0
  201. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/detectors/test_detector_schema_examples.py +0 -0
  202. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/detectors/test_detector_types.py +0 -0
  203. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/detectors/test_phase2_detectors.py +0 -0
  204. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/detectors/test_registry.py +0 -0
  205. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/detectors/threat/__init__.py +0 -0
  206. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/detectors/threat/test_code_security_detector.py +0 -0
  207. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/detectors/threat/test_yara_detector.py +0 -0
  208. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/integration/test_wordpress_broken_links_detector.py +0 -0
  209. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/integration/test_wordpress_links_assets.py +0 -0
  210. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/pipeline/test_detector_outcomes.py +0 -0
  211. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/pipeline/test_detector_pipeline.py +0 -0
  212. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/pipeline/test_worker_pool.py +0 -0
  213. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_assets_metadata_catalog.py +0 -0
  214. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_azure_blob_storage_source.py +0 -0
  215. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_base_source_attachment.py +0 -0
  216. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_base_source_sampling.py +0 -0
  217. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_byte_valued_fields.py +0 -0
  218. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_config.py +0 -0
  219. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_confluence_source.py +0 -0
  220. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_custom_extractor.py +0 -0
  221. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_databricks_source.py +0 -0
  222. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_delta_lake_source.py +0 -0
  223. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_dependency_groups.py +0 -0
  224. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_dropbox_auth_command.py +0 -0
  225. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_dropbox_source.py +0 -0
  226. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_elasticsearch_source.py +0 -0
  227. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_email_source.py +0 -0
  228. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_file_evaluation_runner.py +0 -0
  229. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_google_cloud_storage_source.py +0 -0
  230. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_google_workspace_source.py +0 -0
  231. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_hashing.py +0 -0
  232. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_hive_source.py +0 -0
  233. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_hugging_face_source.py +0 -0
  234. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_iceberg_source.py +0 -0
  235. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_jira_source.py +0 -0
  236. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_kafka_source.py +0 -0
  237. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_local_folder_source.py +0 -0
  238. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_main.py +0 -0
  239. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_meilisearch_source.py +0 -0
  240. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_microsoft_365_source.py +0 -0
  241. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_mongodb_source.py +0 -0
  242. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_mssql_source.py +0 -0
  243. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_mysql_source.py +0 -0
  244. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_neo4j_source.py +0 -0
  245. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_notion_source.py +0 -0
  246. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_object_storage_download_efficiency.py +0 -0
  247. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_opensearch_source.py +0 -0
  248. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_oracle_source.py +0 -0
  249. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_payload_window.py +0 -0
  250. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_payload_window_parquet.py +0 -0
  251. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_postgresql_source.py +0 -0
  252. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_powerbi_source.py +0 -0
  253. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_range_reading.py +0 -0
  254. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_recipe_normalizer.py +0 -0
  255. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_reddit_source.py +0 -0
  256. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_s3_compatible_storage_source.py +0 -0
  257. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_sampling_automatic.py +0 -0
  258. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_sandbox_source.py +0 -0
  259. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_scan_cache.py +0 -0
  260. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_servicedesk_source.py +0 -0
  261. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_slack_source.py +0 -0
  262. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_snowflake_source.py +0 -0
  263. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_source_dependency_groups.py +0 -0
  264. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_source_strategy_conformance.py +0 -0
  265. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_sqlite_source.py +0 -0
  266. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_tableau_source.py +0 -0
  267. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_tabular_automatic_sampling.py +0 -0
  268. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_tabular_utils.py +0 -0
  269. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_uv_sync.py +0 -0
  270. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_wordpress_source.py +0 -0
  271. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_youtube_source.py +0 -0
  272. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/test_youtube_source_integration.py +0 -0
  273. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/utils/test_archive_extraction.py +0 -0
  274. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/utils/test_content_extraction.py +0 -0
  275. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/utils/test_embedded_files.py +0 -0
  276. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/utils/test_file_metadata.py +0 -0
  277. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/utils/test_file_parser.py +0 -0
  278. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/utils/test_file_to_images.py +0 -0
  279. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/utils/test_legacy_office.py +0 -0
  280. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/utils/test_payload.py +0 -0
  281. {classifyre_cli-0.4.86 → classifyre_cli-0.4.87}/tests/utils/test_transcription.py +0 -0
  282. {classifyre_cli-0.4.86 → 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.86
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.86",
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.86"
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"
@@ -672,10 +672,40 @@ class PIIDetector(BaseDetector):
672
672
  # ------------------------------------------------------------------
673
673
 
674
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
+ """
675
682
  if self.analyzer is None:
676
683
  if self._init_error is not None:
677
684
  raise self._init_error
678
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."""
679
709
  try:
680
710
  return self.analyzer.analyze(text=content, language="en", entities=entities)
681
711
  except ModuleNotFoundError as exc:
@@ -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
  )
@@ -32,6 +32,51 @@ class _Cfg:
32
32
  self.max_length = max_length
33
33
 
34
34
 
35
+ class _FakeResult:
36
+ """Shape of a Presidio RecognizerResult, as far as the detector uses it."""
37
+
38
+ def __init__(self, entity_type, start, end, score):
39
+ self.entity_type = entity_type
40
+ self.start = start
41
+ self.end = end
42
+ self.score = score
43
+ self.recognition_metadata = None
44
+
45
+
46
+ class _FakeAnalyzer:
47
+ """Finds every occurrence of a needle and reports chunk-relative offsets.
48
+
49
+ Real Presidio raises E088 above nlp.max_length; this records what it was
50
+ handed so the tests can assert nothing was skipped and offsets came back
51
+ absolute.
52
+ """
53
+
54
+ NEEDLE = "alice@example.com"
55
+
56
+ def __init__(self, score=0.9):
57
+ self.calls: list[str] = []
58
+ self._score = score
59
+
60
+ def analyze(self, *, text, language, entities=None):
61
+ self.calls.append(text)
62
+ results = []
63
+ start = text.find(self.NEEDLE)
64
+ while start != -1:
65
+ results.append(
66
+ _FakeResult("EMAIL_ADDRESS", start, start + len(self.NEEDLE), self._score)
67
+ )
68
+ start = text.find(self.NEEDLE, start + 1)
69
+ return results
70
+
71
+
72
+ def _detector_with_analyzer(analyzer, cfg=None) -> PIIDetector:
73
+ detector = _detector_with(cfg or _Cfg())
74
+ detector.analyzer = analyzer
75
+ detector._init_error = None
76
+ detector._supported_entities_cache = frozenset({"EMAIL_ADDRESS"})
77
+ return detector
78
+
79
+
35
80
  class TestUnwrapInt:
36
81
  def test_unwraps_root_model(self):
37
82
  assert _unwrap_int(ChunkSize(root=500)) == 500
@@ -197,3 +242,86 @@ class TestAutoChunking:
197
242
 
198
243
  assert chunks[-1][1] + len(chunks[-1][0]) == len(text)
199
244
  assert all(len(chunk) <= 1_000 for chunk, _ in chunks)
245
+
246
+
247
+ class TestAnalyzeContentGuard:
248
+ """_analyze_content is the chokepoint every caller shares. Guarding only the
249
+ full-text loop left the tabular per-cell path calling Presidio with the raw
250
+ cell, so one multi-megabyte text column still raised E088 -- swallowed, so
251
+ the page reported zero findings."""
252
+
253
+ def _text_with_needles(self, length: int, positions: list[int]) -> str:
254
+ buf = list("x" * length)
255
+ for pos in positions:
256
+ buf[pos : pos + len(_FakeAnalyzer.NEEDLE)] = _FakeAnalyzer.NEEDLE
257
+ return "".join(buf)
258
+
259
+ def test_short_text_is_a_single_pass(self):
260
+ analyzer = _FakeAnalyzer()
261
+ detector = _detector_with_analyzer(analyzer)
262
+
263
+ detector._analyze_content("contact alice@example.com today")
264
+
265
+ assert len(analyzer.calls) == 1
266
+
267
+ def test_oversized_text_is_split_before_reaching_presidio(self):
268
+ analyzer = _FakeAnalyzer()
269
+ detector = _detector_with_analyzer(analyzer)
270
+ text = self._text_with_needles(2_023_954, [1_500_000])
271
+
272
+ detector._analyze_content(text)
273
+
274
+ assert len(analyzer.calls) > 1
275
+ limit = PIIDetector._SPACY_DEFAULT_MAX_LENGTH
276
+ assert all(len(call) <= limit for call in analyzer.calls)
277
+
278
+ def test_offsets_come_back_absolute(self):
279
+ analyzer = _FakeAnalyzer()
280
+ detector = _detector_with_analyzer(analyzer)
281
+ text = self._text_with_needles(2_023_954, [10, 640_000, 1_500_000, 2_000_000])
282
+
283
+ results = detector._analyze_content(text)
284
+
285
+ # A finding past the first window is exactly what the old code lost.
286
+ assert [r.start for r in results] == [10, 640_000, 1_500_000, 2_000_000]
287
+ assert all(text[r.start : r.end] == _FakeAnalyzer.NEEDLE for r in results)
288
+
289
+ def test_overlap_does_not_duplicate_findings(self):
290
+ analyzer = _FakeAnalyzer()
291
+ detector = _detector_with_analyzer(analyzer)
292
+ window = PIIDetector._AUTO_CHUNK_CHARS
293
+ # Sitting inside the overlap zone, this one is seen by two windows.
294
+ seam = window - (PIIDetector._AUTO_CHUNK_OVERLAP // 2)
295
+ text = self._text_with_needles(1_200_000, [seam])
296
+
297
+ results = detector._analyze_content(text)
298
+
299
+ assert len(results) == 1
300
+ assert results[0].start == seam
301
+
302
+ def test_oversized_tabular_cell_is_analyzed_not_dropped(self):
303
+ analyzer = _FakeAnalyzer()
304
+ detector = _detector_with_analyzer(analyzer)
305
+ cell_value = self._text_with_needles(2_023_954, [1_800_000])
306
+
307
+ results = detector._analyze_structured_cell(
308
+ cell_value, allowed_entity_types={"EMAIL_ADDRESS"}
309
+ )
310
+
311
+ assert [r.start for r in results] == [1_800_000]
312
+ assert cell_value[results[0].start : results[0].end] == _FakeAnalyzer.NEEDLE
313
+
314
+ def test_chunk_size_larger_than_the_spacy_limit_is_still_safe(self):
315
+ # An explicit chunk_size above the ceiling used to hand Presidio an
316
+ # oversized window just the same.
317
+ analyzer = _FakeAnalyzer()
318
+ detector = _detector_with_analyzer(
319
+ analyzer, _Cfg(chunk_size=ChunkSize(root=1_500_000), chunk_overlap=ChunkOverlap(root=0))
320
+ )
321
+ text = self._text_with_needles(2_023_954, [1_400_000])
322
+
323
+ for chunk, _offset in detector._iter_chunks(text):
324
+ detector._analyze_content(chunk)
325
+
326
+ limit = PIIDetector._SPACY_DEFAULT_MAX_LENGTH
327
+ assert all(len(call) <= limit for call in analyzer.calls)
@@ -7,7 +7,7 @@ from typing import Any
7
7
 
8
8
  import pytest
9
9
 
10
- from src.outputs.base import OutputRuntimeContext
10
+ from src.outputs.base import DEFAULT_REST_TIMEOUT_SEC, OutputRuntimeContext
11
11
  from src.outputs.console import ConsoleOutputSink
12
12
  from src.outputs.factory import resolve_output_settings
13
13
  from src.outputs.file import FileOutputSink
@@ -87,6 +87,32 @@ def test_output_resolution_rest_uses_api_url_env_fallback(
87
87
  assert settings.rest_url == "http://api-env.example:8000"
88
88
 
89
89
 
90
+ def test_output_resolution_uses_the_five_minute_read_timeout_by_default() -> None:
91
+ settings = resolve_output_settings(_args(source_id="source-1"))
92
+
93
+ assert settings.rest_timeout_sec == DEFAULT_REST_TIMEOUT_SEC == 300
94
+
95
+
96
+ def test_output_resolution_env_overrides_the_read_timeout(
97
+ monkeypatch: pytest.MonkeyPatch,
98
+ ) -> None:
99
+ monkeypatch.setenv("CLASSIFYRE_OUTPUT_REST_TIMEOUT_SEC", "45")
100
+
101
+ settings = resolve_output_settings(_args(source_id="source-1"))
102
+
103
+ assert settings.rest_timeout_sec == 45
104
+
105
+
106
+ def test_output_resolution_treats_non_positive_read_timeout_as_unlimited(
107
+ monkeypatch: pytest.MonkeyPatch,
108
+ ) -> None:
109
+ monkeypatch.setenv("CLASSIFYRE_OUTPUT_REST_TIMEOUT_SEC", "0")
110
+
111
+ settings = resolve_output_settings(_args(source_id="source-1"))
112
+
113
+ assert settings.rest_timeout_sec is None
114
+
115
+
90
116
  def test_output_resolution_defaults_to_console_without_source_id() -> None:
91
117
  settings = resolve_output_settings(_args())
92
118
 
@@ -167,7 +193,7 @@ class _FakeSession:
167
193
  method: str,
168
194
  url: str,
169
195
  json: dict[str, Any] | None,
170
- timeout: int,
196
+ timeout: tuple[float, float | None],
171
197
  ) -> _FakeResponse:
172
198
  self.calls.append(
173
199
  {
@@ -199,7 +225,9 @@ async def test_rest_output_managed_runner_flow(monkeypatch: pytest.MonkeyPatch)
199
225
  managed_runner=True,
200
226
  batch_size=20,
201
227
  )
202
- sink = RestOutputSink(context, base_url="http://localhost:8000", timeout_sec=30)
228
+ sink = RestOutputSink(
229
+ context, base_url="http://localhost:8000", timeout_sec=DEFAULT_REST_TIMEOUT_SEC
230
+ )
203
231
 
204
232
  await sink.start()
205
233
  await sink.emit_batch([{"hash": "h1"}])
@@ -231,7 +259,7 @@ async def test_rest_output_keeps_namespace_base_for_discovery_and_finding_batche
231
259
  sink = RestOutputSink(
232
260
  context,
233
261
  base_url="http://api.internal/prefix/namespace-id/",
234
- timeout_sec=30,
262
+ timeout_sec=DEFAULT_REST_TIMEOUT_SEC,
235
263
  )
236
264
 
237
265
  await sink.register_discovered_assets(["hash-1", "hash-2"])
@@ -267,7 +295,7 @@ async def test_register_discovered_assets_returns_a_bare_list_without_payload_cu
267
295
  sink = RestOutputSink(
268
296
  OutputRuntimeContext(source_id="s", runner_id="r", managed_runner=True, batch_size=20),
269
297
  base_url="http://localhost:8000",
270
- timeout_sec=30,
298
+ timeout_sec=DEFAULT_REST_TIMEOUT_SEC,
271
299
  )
272
300
 
273
301
  result = await sink.register_discovered_assets(["a", "b"], include_scan_cache=True)
@@ -297,7 +325,7 @@ async def test_register_discovered_assets_collects_payload_cursors(
297
325
  sink = RestOutputSink(
298
326
  OutputRuntimeContext(source_id="s", runner_id="r", managed_runner=True, batch_size=20),
299
327
  base_url="http://localhost:8000",
300
- timeout_sec=30,
328
+ timeout_sec=DEFAULT_REST_TIMEOUT_SEC,
301
329
  )
302
330
 
303
331
  cache, cursors = await sink.register_discovered_assets(
@@ -320,7 +348,7 @@ async def test_asset_payload_carries_the_payload_cursor(
320
348
  sink = RestOutputSink(
321
349
  OutputRuntimeContext(source_id="s", runner_id="r", managed_runner=True, batch_size=20),
322
350
  base_url="http://localhost:8000",
323
- timeout_sec=30,
351
+ timeout_sec=DEFAULT_REST_TIMEOUT_SEC,
324
352
  )
325
353
 
326
354
  cursor = {"v": 1, "offset": 300, "exhausted": False, "checksum": "c1"}
@@ -342,7 +370,9 @@ async def test_rest_output_finalize_includes_sampling_cursor(
342
370
  managed_runner=True,
343
371
  batch_size=20,
344
372
  )
345
- sink = RestOutputSink(context, base_url="http://localhost:8000", timeout_sec=30)
373
+ sink = RestOutputSink(
374
+ context, base_url="http://localhost:8000", timeout_sec=DEFAULT_REST_TIMEOUT_SEC
375
+ )
346
376
 
347
377
  cursor = {"tables": {"db_#_users": {"pk": [42]}}}
348
378
  sink.set_sampling_cursor(cursor)
@@ -373,7 +403,9 @@ async def test_rest_output_omits_null_fields_in_nested_payload(
373
403
  managed_runner=True,
374
404
  batch_size=20,
375
405
  )
376
- sink = RestOutputSink(context, base_url="http://localhost:8000", timeout_sec=30)
406
+ sink = RestOutputSink(
407
+ context, base_url="http://localhost:8000", timeout_sec=DEFAULT_REST_TIMEOUT_SEC
408
+ )
377
409
 
378
410
  await sink.start()
379
411
  await sink.emit_batch(
@@ -425,7 +457,9 @@ async def test_rest_output_auto_creates_external_runner(monkeypatch: pytest.Monk
425
457
  managed_runner=False,
426
458
  batch_size=20,
427
459
  )
428
- sink = RestOutputSink(context, base_url="http://localhost:8000", timeout_sec=30)
460
+ sink = RestOutputSink(
461
+ context, base_url="http://localhost:8000", timeout_sec=DEFAULT_REST_TIMEOUT_SEC
462
+ )
429
463
 
430
464
  await sink.start()
431
465
  await sink.emit_batch([{"hash": "h1"}])
@@ -447,7 +481,9 @@ async def test_rest_output_fail_marks_runner_error(monkeypatch: pytest.MonkeyPat
447
481
  managed_runner=False,
448
482
  batch_size=20,
449
483
  )
450
- sink = RestOutputSink(context, base_url="http://localhost:8000", timeout_sec=30)
484
+ sink = RestOutputSink(
485
+ context, base_url="http://localhost:8000", timeout_sec=DEFAULT_REST_TIMEOUT_SEC
486
+ )
451
487
 
452
488
  await sink.start()
453
489
  await sink.fail(RuntimeError("boom"))
@@ -470,7 +506,9 @@ async def test_rest_output_fail_marks_managed_runner_error(
470
506
  managed_runner=True,
471
507
  batch_size=20,
472
508
  )
473
- sink = RestOutputSink(context, base_url="http://localhost:8000", timeout_sec=30)
509
+ sink = RestOutputSink(
510
+ context, base_url="http://localhost:8000", timeout_sec=DEFAULT_REST_TIMEOUT_SEC
511
+ )
474
512
 
475
513
  await sink.start()
476
514
  await sink.fail(RuntimeError("boom"))
@@ -830,7 +830,7 @@ wheels = [
830
830
 
831
831
  [[package]]
832
832
  name = "classifyre-cli"
833
- version = "0.4.86"
833
+ version = "0.4.87"
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.86"
1372
+ version = "0.4.87"
1373
1373
  source = { editable = "../../packages/schemas" }
1374
1374
  dependencies = [
1375
1375
  { name = "fastjsonschema" },
@@ -1,3 +0,0 @@
1
- $ uv sync
2
- Resolved 307 packages in 247ms
3
- Checked 51 packages in 1ms
File without changes