classifyre-cli 0.4.48__tar.gz → 0.4.50__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 (235) hide show
  1. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/.turbo/turbo-build.log +1 -1
  2. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/PKG-INFO +1 -1
  3. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/package.json +1 -1
  4. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/pyproject.toml +1 -1
  5. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/custom/runners/_base.py +21 -2
  6. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/custom/runners/_feature_extraction.py +42 -15
  7. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/custom/runners/_gliner2.py +64 -16
  8. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/custom/runners/_image_classification.py +45 -19
  9. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/custom/runners/_llm.py +5 -0
  10. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/custom/runners/_object_detection.py +49 -20
  11. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/custom/runners/_regex.py +14 -0
  12. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/custom/runners/_text_classification.py +41 -16
  13. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/main.py +4 -0
  14. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/pipeline/worker_pool.py +54 -3
  15. classifyre_cli-0.4.50/tests/detectors/custom/test_runner_hardening.py +220 -0
  16. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/uv.lock +41 -46
  17. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/.gitignore +0 -0
  18. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/.python-version +0 -0
  19. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/README.md +0 -0
  20. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/main.py +0 -0
  21. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/scripts/generate_models.py +0 -0
  22. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/__init__.py +0 -0
  23. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/config.py +0 -0
  24. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/__init__.py +0 -0
  25. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/base.py +0 -0
  26. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/broken_links/__init__.py +0 -0
  27. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/broken_links/detector.py +0 -0
  28. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/config.py +0 -0
  29. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/content/__init__.py +0 -0
  30. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/custom/__init__.py +0 -0
  31. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/custom/detector.py +0 -0
  32. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/custom/extractor.py +0 -0
  33. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/custom/runners/__init__.py +0 -0
  34. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/custom/runners/_factory.py +0 -0
  35. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/custom/trainer.py +0 -0
  36. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/dependencies.py +0 -0
  37. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/pii/__init__.py +0 -0
  38. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/pii/detector.py +0 -0
  39. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/secrets/__init__.py +0 -0
  40. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/secrets/detector.py +0 -0
  41. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/threat/__init__.py +0 -0
  42. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/threat/code_security_detector.py +0 -0
  43. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/threat/yara_detector.py +0 -0
  44. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/models/generated_detectors.py +0 -0
  45. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/models/generated_input.py +0 -0
  46. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/models/generated_single_asset_scan_results.py +0 -0
  47. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/outputs/__init__.py +0 -0
  48. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/outputs/base.py +0 -0
  49. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/outputs/console.py +0 -0
  50. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/outputs/factory.py +0 -0
  51. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/outputs/file.py +0 -0
  52. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/outputs/rest.py +0 -0
  53. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/pipeline/__init__.py +0 -0
  54. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/pipeline/content_provider.py +0 -0
  55. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/pipeline/detector_pipeline.py +0 -0
  56. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/pipeline/parsed_content_provider.py +0 -0
  57. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sandbox/__init__.py +0 -0
  58. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sandbox/runner.py +0 -0
  59. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/__init__.py +0 -0
  60. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/asset_metadata.py +0 -0
  61. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/atlassian_common.py +0 -0
  62. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/azure_blob_storage/__init__.py +0 -0
  63. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/azure_blob_storage/source.py +0 -0
  64. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/base.py +0 -0
  65. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/confluence/__init__.py +0 -0
  66. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/confluence/source.py +0 -0
  67. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/databricks/__init__.py +0 -0
  68. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/databricks/source.py +0 -0
  69. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/delta_lake/__init__.py +0 -0
  70. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/delta_lake/source.py +0 -0
  71. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/dependencies.py +0 -0
  72. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/elasticsearch/__init__.py +0 -0
  73. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/elasticsearch/source.py +0 -0
  74. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/email/__init__.py +0 -0
  75. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/email/source.py +0 -0
  76. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/google_cloud_storage/__init__.py +0 -0
  77. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/google_cloud_storage/source.py +0 -0
  78. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/hive/__init__.py +0 -0
  79. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/hive/source.py +0 -0
  80. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/iceberg/__init__.py +0 -0
  81. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/iceberg/source.py +0 -0
  82. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/jira/__init__.py +0 -0
  83. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/jira/source.py +0 -0
  84. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/kafka/__init__.py +0 -0
  85. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/kafka/source.py +0 -0
  86. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/lakehouse_base.py +0 -0
  87. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/local_folder/__init__.py +0 -0
  88. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/local_folder/source.py +0 -0
  89. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/meilisearch/__init__.py +0 -0
  90. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/meilisearch/source.py +0 -0
  91. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/mongodb/__init__.py +0 -0
  92. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/mongodb/source.py +0 -0
  93. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/mssql/__init__.py +0 -0
  94. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/mssql/source.py +0 -0
  95. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/mysql/__init__.py +0 -0
  96. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/mysql/source.py +0 -0
  97. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/neo4j/__init__.py +0 -0
  98. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/neo4j/source.py +0 -0
  99. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/notion/__init__.py +0 -0
  100. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/notion/client.py +0 -0
  101. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/notion/source.py +0 -0
  102. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/object_storage/base.py +0 -0
  103. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/opensearch/__init__.py +0 -0
  104. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/opensearch/source.py +0 -0
  105. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/oracle/__init__.py +0 -0
  106. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/oracle/source.py +0 -0
  107. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/postgresql/__init__.py +0 -0
  108. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/postgresql/source.py +0 -0
  109. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/powerbi/__init__.py +0 -0
  110. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/powerbi/source.py +0 -0
  111. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/recipe_normalizer.py +0 -0
  112. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/s3_client.py +0 -0
  113. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/s3_compatible_storage/README.md +0 -0
  114. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/s3_compatible_storage/__init__.py +0 -0
  115. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/s3_compatible_storage/source.py +0 -0
  116. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/search_engine_base.py +0 -0
  117. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/servicedesk/__init__.py +0 -0
  118. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/servicedesk/source.py +0 -0
  119. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/slack/__init__.py +0 -0
  120. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/slack/source.py +0 -0
  121. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/snowflake/__init__.py +0 -0
  122. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/snowflake/source.py +0 -0
  123. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/sqlite/__init__.py +0 -0
  124. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/sqlite/source.py +0 -0
  125. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/tableau/__init__.py +0 -0
  126. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/tableau/source.py +0 -0
  127. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/tabular_base.py +0 -0
  128. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/tabular_utils.py +0 -0
  129. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/wordpress/__init__.py +0 -0
  130. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/wordpress/source.py +0 -0
  131. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/youtube/__init__.py +0 -0
  132. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/youtube/source.py +0 -0
  133. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/telemetry.py +0 -0
  134. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/utils/__init__.py +0 -0
  135. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/utils/content_extraction.py +0 -0
  136. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/utils/dependency_groups.py +0 -0
  137. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/utils/embedded_images.py +0 -0
  138. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/utils/file_metadata.py +0 -0
  139. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/utils/file_parser.py +0 -0
  140. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/utils/file_to_images.py +0 -0
  141. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/utils/hashing.py +0 -0
  142. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/utils/resources.py +0 -0
  143. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/utils/transcription.py +0 -0
  144. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/utils/uv_sync.py +0 -0
  145. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/utils/validation.py +0 -0
  146. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/__init__.py +0 -0
  147. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/_lakehouse_fakes.py +0 -0
  148. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/conftest.py +0 -0
  149. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/__init__.py +0 -0
  150. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/broken_links/test_broken_links_detector.py +0 -0
  151. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/conftest.py +0 -0
  152. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/content/__init__.py +0 -0
  153. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/custom/__init__.py +0 -0
  154. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/custom/conftest.py +0 -0
  155. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/custom/test_invoice_extraction.py +0 -0
  156. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/custom/test_llm_runner.py +0 -0
  157. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/custom/test_pipeline_integration.py +0 -0
  158. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/custom/test_regex_runner.py +0 -0
  159. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/custom/test_transformer_runners.py +0 -0
  160. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/pii/__init__.py +0 -0
  161. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/pii/conftest.py +0 -0
  162. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/pii/sample_invoice.pdf +0 -0
  163. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/pii/test_pii_detector.py +0 -0
  164. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/pii/test_pii_detector_extended.py +0 -0
  165. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/secrets/__init__.py +0 -0
  166. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/secrets/test_secrets_detector.py +0 -0
  167. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/secrets/test_secrets_detector_extended.py +0 -0
  168. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/test_base_detector.py +0 -0
  169. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/test_custom_detector_examples_runtime.py +0 -0
  170. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/test_detector_catalog_commercial.py +0 -0
  171. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/test_detector_pipeline_types.py +0 -0
  172. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/test_detector_schema_examples.py +0 -0
  173. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/test_detector_types.py +0 -0
  174. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/test_phase2_detectors.py +0 -0
  175. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/test_registry.py +0 -0
  176. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/threat/__init__.py +0 -0
  177. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/threat/test_code_security_detector.py +0 -0
  178. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/threat/test_yara_detector.py +0 -0
  179. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/integration/test_wordpress_broken_links_detector.py +0 -0
  180. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/integration/test_wordpress_links_assets.py +0 -0
  181. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/pipeline/test_detector_pipeline.py +0 -0
  182. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/pipeline/test_worker_pool.py +0 -0
  183. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_assets_metadata_catalog.py +0 -0
  184. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_azure_blob_storage_source.py +0 -0
  185. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_base_source_attachment.py +0 -0
  186. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_base_source_sampling.py +0 -0
  187. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_config.py +0 -0
  188. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_confluence_source.py +0 -0
  189. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_custom_extractor.py +0 -0
  190. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_databricks_source.py +0 -0
  191. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_delta_lake_source.py +0 -0
  192. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_dependency_groups.py +0 -0
  193. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_elasticsearch_source.py +0 -0
  194. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_email_source.py +0 -0
  195. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_google_cloud_storage_source.py +0 -0
  196. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_hashing.py +0 -0
  197. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_hive_source.py +0 -0
  198. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_iceberg_source.py +0 -0
  199. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_jira_source.py +0 -0
  200. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_kafka_source.py +0 -0
  201. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_local_folder_source.py +0 -0
  202. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_meilisearch_source.py +0 -0
  203. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_mongodb_source.py +0 -0
  204. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_mssql_source.py +0 -0
  205. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_mysql_source.py +0 -0
  206. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_neo4j_source.py +0 -0
  207. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_notion_source.py +0 -0
  208. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_opensearch_source.py +0 -0
  209. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_oracle_source.py +0 -0
  210. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_outputs.py +0 -0
  211. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_postgresql_source.py +0 -0
  212. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_powerbi_source.py +0 -0
  213. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_recipe_normalizer.py +0 -0
  214. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_s3_compatible_storage_source.py +0 -0
  215. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_sampling_automatic.py +0 -0
  216. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_sandbox_runner.py +0 -0
  217. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_servicedesk_source.py +0 -0
  218. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_slack_source.py +0 -0
  219. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_snowflake_source.py +0 -0
  220. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_source_dependency_groups.py +0 -0
  221. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_source_strategy_conformance.py +0 -0
  222. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_sqlite_source.py +0 -0
  223. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_tableau_source.py +0 -0
  224. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_tabular_automatic_sampling.py +0 -0
  225. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_tabular_utils.py +0 -0
  226. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_uv_sync.py +0 -0
  227. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_wordpress_source.py +0 -0
  228. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_youtube_source.py +0 -0
  229. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_youtube_source_integration.py +0 -0
  230. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/utils/test_content_extraction.py +0 -0
  231. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/utils/test_embedded_images.py +0 -0
  232. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/utils/test_file_metadata.py +0 -0
  233. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/utils/test_file_parser.py +0 -0
  234. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/utils/test_file_to_images.py +0 -0
  235. {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/utils/test_transcription.py +0 -0
@@ -1,3 +1,3 @@
1
1
  $ uv sync
2
- Resolved 276 packages in 141ms
2
+ Resolved 276 packages in 178ms
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.48
3
+ Version: 0.4.50
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.48",
3
+ "version": "0.4.50",
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.48"
3
+ version = "0.4.50"
4
4
  description = "Classifyre CLI — scan and classify unstructured data sources"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -47,6 +47,24 @@ _IMAGE_INPUT_CONTENT_TYPES = [*_IMAGE_CONTENT_TYPES, "application/pdf"]
47
47
 
48
48
  logger = logging.getLogger(__name__)
49
49
 
50
+ # Cap per-label entity spans embedded in each finding's metadata. Every finding
51
+ # carries a copy of the pipeline result (the API builds extraction rows from it),
52
+ # so an uncapped regex with thousands of matches would blow up the scan payload
53
+ # quadratically.
54
+ _MAX_EMBEDDED_SPANS_PER_LABEL = 25
55
+
56
+
57
+ def _slim_pipeline_result(result: PipelineResult) -> dict[str, Any]:
58
+ """model_dump with entity span lists capped for embedding in finding metadata."""
59
+ dump = result.model_dump()
60
+ entities = dump.get("entities")
61
+ if isinstance(entities, dict):
62
+ for label, spans in entities.items():
63
+ if isinstance(spans, list) and len(spans) > _MAX_EMBEDDED_SPANS_PER_LABEL:
64
+ entities[label] = spans[:_MAX_EMBEDDED_SPANS_PER_LABEL]
65
+ dump.setdefault("metadata", {})[f"truncated_spans:{label}"] = len(spans)
66
+ return dump
67
+
50
68
 
51
69
  def _load_input_images(content: bytes, content_type: str, pil: Any) -> list[tuple[int, Any]]:
52
70
  """Return ``(page_index, PIL.Image)`` tuples for an image or renderable file.
@@ -139,6 +157,7 @@ class BaseRunner(ABC):
139
157
  def _result_to_findings(self, text: str, result: PipelineResult) -> list[DetectionResult]:
140
158
  findings: list[DetectionResult] = []
141
159
  runner_type = result.metadata.get("runner", "GLINER2")
160
+ pipeline_result_dump = _slim_pipeline_result(result)
142
161
 
143
162
  for label, spans in result.entities.items():
144
163
  for span in spans:
@@ -162,7 +181,7 @@ class BaseRunner(ABC):
162
181
  meta: dict[str, Any] = {
163
182
  "runner": runner_type,
164
183
  "entity_label": label,
165
- "pipeline_result": result.model_dump(),
184
+ "pipeline_result": pipeline_result_dump,
166
185
  }
167
186
  if "groups" in span:
168
187
  meta["capture_groups"] = span["groups"]
@@ -197,7 +216,7 @@ class BaseRunner(ABC):
197
216
  "runner": runner_type,
198
217
  "task": task,
199
218
  "label": label,
200
- "pipeline_result": result.model_dump(),
219
+ "pipeline_result": pipeline_result_dump,
201
220
  },
202
221
  )
203
222
  )
@@ -60,20 +60,44 @@ class FeatureExtractionRunner(BaseRunner):
60
60
  self._schema = schema
61
61
  self._detector_key = detector_key
62
62
  self._detector_name = detector_name
63
- ensure_torch("feature_extraction", ["custom", "detectors"])
64
- transformers = require_module("transformers", "feature_extraction", ["custom", "detectors"])
65
- truncation = schema.truncation if schema.truncation is not None else True
66
- tokenizer_kwargs: dict[str, Any] = {"truncation": truncation}
67
- if schema.max_length is not None:
68
- tokenizer_kwargs["max_length"] = schema.max_length
69
- pipeline_kwargs: dict[str, Any] = {
70
- "model": schema.model,
71
- "device": schema.device or "cpu",
72
- "tokenizer_kwargs": tokenizer_kwargs,
73
- }
74
- if schema.model_revision:
75
- pipeline_kwargs["revision"] = schema.model_revision
76
- self._pipe: Any = transformers.pipeline("feature-extraction", **pipeline_kwargs)
63
+ # Model loading is deferred to first detect() so the parent process
64
+ # (which only routes when a worker pool is active) never pays the
65
+ # torch import + model memory cost.
66
+ self._pipe: Any | None = None
67
+ self._load_error: str | None = None
68
+
69
+ def _ensure_pipeline(self) -> Any | None:
70
+ if self._pipe is not None:
71
+ return self._pipe
72
+ if self._load_error is not None:
73
+ return None
74
+ schema = self._schema
75
+ try:
76
+ ensure_torch("feature_extraction", ["custom", "detectors"])
77
+ transformers = require_module(
78
+ "transformers", "feature_extraction", ["custom", "detectors"]
79
+ )
80
+ truncation = schema.truncation if schema.truncation is not None else True
81
+ tokenizer_kwargs: dict[str, Any] = {"truncation": truncation}
82
+ if schema.max_length is not None:
83
+ tokenizer_kwargs["max_length"] = schema.max_length
84
+ pipeline_kwargs: dict[str, Any] = {
85
+ "model": schema.model,
86
+ "device": schema.device or "cpu",
87
+ "tokenizer_kwargs": tokenizer_kwargs,
88
+ }
89
+ if schema.model_revision:
90
+ pipeline_kwargs["revision"] = schema.model_revision
91
+ self._pipe = transformers.pipeline("feature-extraction", **pipeline_kwargs)
92
+ return self._pipe
93
+ except Exception as exc:
94
+ # Raise on the first failure so the scan records one structured
95
+ # error; later assets skip quietly via the cached _load_error.
96
+ self._load_error = str(exc)
97
+ raise RuntimeError(
98
+ f"feature_extraction model '{schema.model}' failed to load for "
99
+ f"detector '{self._detector_key}': {exc}"
100
+ ) from exc
77
101
 
78
102
  def run(self, text: str) -> None: # type: ignore[override] # pragma: no cover
79
103
  raise NotImplementedError("FeatureExtractionRunner uses detect() directly")
@@ -86,6 +110,9 @@ class FeatureExtractionRunner(BaseRunner):
86
110
  text = content.strip()
87
111
  if not text:
88
112
  return []
113
+ pipe = self._ensure_pipeline()
114
+ if pipe is None:
115
+ return []
89
116
 
90
117
  schema = self._schema
91
118
  pooling = str(schema.pooling_strategy or "mean")
@@ -96,7 +123,7 @@ class FeatureExtractionRunner(BaseRunner):
96
123
  results: list[DetectionResult] = []
97
124
  try:
98
125
  for chunk, offset in _chunk_text_with_offsets(text, chunk_size, chunk_overlap):
99
- raw: list[list[list[float]]] = self._pipe(chunk) or []
126
+ raw: list[list[list[float]]] = pipe(chunk) or []
100
127
  if not raw or not raw[0]:
101
128
  continue
102
129
  embedding = _pool_hidden(raw[0], pooling, normalize)
@@ -18,9 +18,20 @@ from ....models.generated_detectors import (
18
18
  )
19
19
  from ...dependencies import MissingDependencyError, require_module
20
20
  from ._base import _DEFAULT_GLINER2_MODEL, BaseRunner
21
+ from ._text_classification import _chunk_text
21
22
 
22
23
  logger = logging.getLogger(__name__)
23
24
 
25
+ # GLiNER2's encoder window is a few hundred tokens; anything past it is silently
26
+ # dropped by a plain extract_entities call. Above this char threshold we use the
27
+ # library's extract_entities_long (chunked) API when available, and run
28
+ # classification per-chunk with max-confidence aggregation.
29
+ _LONG_TEXT_CHAR_THRESHOLD = 3000
30
+ _CLS_CHUNK_SIZE = 3000
31
+ _CLS_CHUNK_OVERLAP = 200
32
+ # Bound per-asset CPU cost on pathological inputs.
33
+ _MAX_CLS_CHUNKS = 50
34
+
24
35
 
25
36
  class GLiNER2Runner(BaseRunner):
26
37
  """Execute a GLiNER2 pipeline: single-model pass for entities + classification.
@@ -38,6 +49,7 @@ class GLiNER2Runner(BaseRunner):
38
49
  self._detector_key = detector_key
39
50
  self._detector_name = detector_name
40
51
  self._model: Any | None = None
52
+ self._load_error: str | None = None
41
53
  self._setfit_models: dict[str, Any] | None = None
42
54
  self._setfit_labels: dict[str, list[str]] = {}
43
55
  self._artifact_dir: Path | None = None
@@ -70,21 +82,15 @@ class GLiNER2Runner(BaseRunner):
70
82
 
71
83
  try:
72
84
  if entity_schema:
73
- raw = model.extract_entities(
74
- text,
75
- entity_schema,
76
- threshold=0.0,
77
- include_confidence=True,
78
- include_spans=True,
79
- )
85
+ raw = self._extract_entities(model, text, entity_schema)
80
86
  raw_entities = _normalise_entity_output(raw, text)
81
87
 
82
88
  for task_name, labels in classification_tasks.items():
83
89
  setfit = self._get_setfit_model(task_name)
84
90
  if setfit is not None:
85
- raw_cls = self._run_setfit(setfit, task_name, text)
91
+ raw_cls: Any = self._run_setfit(setfit, task_name, text)
86
92
  else:
87
- raw_cls = model.classify(text, labels, threshold=0.0)
93
+ raw_cls = self._classify_chunked(model, text, labels)
88
94
  raw_classification[task_name] = _normalise_classification_output(raw_cls)
89
95
 
90
96
  except Exception as exc: # pragma: no cover - runtime specific
@@ -113,9 +119,50 @@ class GLiNER2Runner(BaseRunner):
113
119
  },
114
120
  )
115
121
 
122
+ def _extract_entities(self, model: Any, text: str, entity_schema: dict[str, str]) -> Any:
123
+ """Extract entities, using the library's chunked long-document API for large texts."""
124
+ kwargs: dict[str, Any] = {
125
+ "threshold": 0.0,
126
+ "include_confidence": True,
127
+ "include_spans": True,
128
+ }
129
+ extract_long = getattr(model, "extract_entities_long", None)
130
+ if extract_long is not None and len(text) > _LONG_TEXT_CHAR_THRESHOLD:
131
+ try:
132
+ return extract_long(text, entity_schema, **kwargs)
133
+ except TypeError:
134
+ logger.debug(
135
+ "extract_entities_long signature mismatch for detector '%s', "
136
+ "falling back to extract_entities",
137
+ self._detector_key,
138
+ )
139
+ return model.extract_entities(text, entity_schema, **kwargs)
140
+
141
+ def _classify_chunked(self, model: Any, text: str, labels: list[str]) -> dict[str, object]:
142
+ """Classify text, chunking long inputs and keeping the max-confidence label."""
143
+ chunks = _chunk_text(text, _CLS_CHUNK_SIZE, _CLS_CHUNK_OVERLAP)[:_MAX_CLS_CHUNKS]
144
+ if len(chunks) == 1:
145
+ return _normalise_classification_output(model.classify(text, labels, threshold=0.0))
146
+
147
+ best: dict[str, object] = {}
148
+ for chunk in chunks:
149
+ outcome = _normalise_classification_output(model.classify(chunk, labels, threshold=0.0))
150
+ if outcome and float(outcome.get("confidence", 0.0)) > float(
151
+ best.get("confidence", -1.0)
152
+ ):
153
+ best = outcome
154
+ return best
155
+
116
156
  def _load_model(self) -> Any | None:
117
157
  if self._model is not None:
118
158
  return self._model
159
+ if self._load_error is not None:
160
+ logger.debug(
161
+ "GLiNER2 model previously failed to load for detector '%s': %s",
162
+ self._detector_key,
163
+ self._load_error,
164
+ )
165
+ return None
119
166
 
120
167
  if self._artifact_dir is not None:
121
168
  gliner_path = self._artifact_dir / "gliner2"
@@ -153,13 +200,14 @@ class GLiNER2Runner(BaseRunner):
153
200
  except MissingDependencyError:
154
201
  raise
155
202
  except Exception as exc: # pragma: no cover - environment specific
156
- logger.warning(
157
- "Failed to load GLiNER2 model '%s' for detector '%s': %s",
158
- model_name,
159
- self._detector_key,
160
- exc,
161
- )
162
- return None
203
+ # Raise on the first failure so the scan records one structured error
204
+ # instead of silently reporting zero findings; later assets skip
205
+ # quietly via the cached _load_error above.
206
+ self._load_error = str(exc)
207
+ raise RuntimeError(
208
+ f"GLiNER2 model '{model_name}' failed to load for detector "
209
+ f"'{self._detector_key}': {exc}"
210
+ ) from exc
163
211
 
164
212
  def _get_setfit_model(self, task_name: str) -> Any | None:
165
213
  """Return the SetFit model for task_name if a trained artifact exists."""
@@ -31,24 +31,46 @@ class ImageClassificationRunner(BaseRunner):
31
31
  self._schema = schema
32
32
  self._detector_key = detector_key
33
33
  self._detector_name = detector_name
34
- ensure_torch("image_classification", ["custom", "detectors"])
35
- transformers = require_module(
36
- "transformers", "image_classification", ["custom", "detectors"]
37
- )
38
- self._pil = require_module("PIL.Image", "image_classification", ["custom", "detectors"])
39
- model_id = schema.model or _DEFAULT_IMAGE_CLASSIFICATION_MODEL
40
- pipeline_kwargs: dict[str, Any] = {
41
- "model": model_id,
42
- "device": schema.device or "cpu",
43
- }
44
- if schema.model_revision:
45
- pipeline_kwargs["revision"] = schema.model_revision
46
- if schema.top_k is not None:
47
- pipeline_kwargs["top_k"] = schema.top_k
48
- if schema.function_to_apply is not None:
49
- pipeline_kwargs["function_to_apply"] = str(schema.function_to_apply)
50
- self._pipe: Any = transformers.pipeline("image-classification", **pipeline_kwargs)
51
- self._model_id = model_id
34
+ self._model_id = schema.model or _DEFAULT_IMAGE_CLASSIFICATION_MODEL
35
+ # Model loading is deferred to first detect() so the parent process
36
+ # (which only routes when a worker pool is active) never pays the
37
+ # torch import + model memory cost.
38
+ self._pipe: Any | None = None
39
+ self._pil: Any | None = None
40
+ self._load_error: str | None = None
41
+
42
+ def _ensure_pipeline(self) -> Any | None:
43
+ if self._pipe is not None:
44
+ return self._pipe
45
+ if self._load_error is not None:
46
+ return None
47
+ schema = self._schema
48
+ try:
49
+ ensure_torch("image_classification", ["custom", "detectors"])
50
+ transformers = require_module(
51
+ "transformers", "image_classification", ["custom", "detectors"]
52
+ )
53
+ self._pil = require_module("PIL.Image", "image_classification", ["custom", "detectors"])
54
+ pipeline_kwargs: dict[str, Any] = {
55
+ "model": self._model_id,
56
+ "device": schema.device or "cpu",
57
+ }
58
+ if schema.model_revision:
59
+ pipeline_kwargs["revision"] = schema.model_revision
60
+ if schema.top_k is not None:
61
+ pipeline_kwargs["top_k"] = schema.top_k
62
+ if schema.function_to_apply is not None:
63
+ pipeline_kwargs["function_to_apply"] = str(schema.function_to_apply)
64
+ self._pipe = transformers.pipeline("image-classification", **pipeline_kwargs)
65
+ return self._pipe
66
+ except Exception as exc:
67
+ # Raise on the first failure so the scan records one structured
68
+ # error; later assets skip quietly via the cached _load_error.
69
+ self._load_error = str(exc)
70
+ raise RuntimeError(
71
+ f"image_classification model '{self._model_id}' failed to load for "
72
+ f"detector '{self._detector_key}': {exc}"
73
+ ) from exc
52
74
 
53
75
  def run(self, text: str) -> None: # type: ignore[override] # pragma: no cover
54
76
  raise NotImplementedError("ImageClassificationRunner uses detect() directly")
@@ -58,6 +80,10 @@ class ImageClassificationRunner(BaseRunner):
58
80
  logger.warning("image_classification: received string content, expected bytes")
59
81
  return []
60
82
 
83
+ pipe = self._ensure_pipeline()
84
+ if pipe is None:
85
+ return []
86
+
61
87
  # image/* opens directly; PDFs are rasterised to one image per page.
62
88
  images = _load_input_images(content, content_type, self._pil)
63
89
  if not images:
@@ -69,7 +95,7 @@ class ImageClassificationRunner(BaseRunner):
69
95
  results: list[DetectionResult] = []
70
96
  for page_index, image in images:
71
97
  try:
72
- predictions: list[dict[str, Any]] = self._pipe(image) or []
98
+ predictions: list[dict[str, Any]] = pipe(image) or []
73
99
  for pred in predictions:
74
100
  label: str = pred.get("label", "unknown")
75
101
  score: float = float(pred.get("score", 0.0))
@@ -39,6 +39,10 @@ _VISION_CONTENT_TYPES = [*_IMAGE_CONTENT_TYPES, "application/pdf"]
39
39
  # token cost and request size for multi-page PDFs.
40
40
  _MAX_VISION_IMAGES = 20
41
41
 
42
+ # Hard request timeout so a hung provider endpoint can never stall a pool
43
+ # worker for litellm's ~10-minute default. Overridable per deployment.
44
+ _COMPLETION_TIMEOUT_SECONDS = float(os.environ.get("CLASSIFYRE_LLM_TIMEOUT_SECONDS", "90"))
45
+
42
46
 
43
47
  class LLMRunner(BaseRunner):
44
48
  """AI detector — sends content to a configured LLM provider for classification + extraction."""
@@ -136,6 +140,7 @@ class LLMRunner(BaseRunner):
136
140
  max_tokens=self._max_tokens(),
137
141
  messages=messages,
138
142
  response_format={"type": "json_object"},
143
+ timeout=_COMPLETION_TIMEOUT_SECONDS,
139
144
  )
140
145
  raw = response.choices[0].message.content or "{}"
141
146
  parsed = self._parse_json(raw)
@@ -30,26 +30,51 @@ class ObjectDetectionRunner(BaseRunner):
30
30
  self._schema = schema
31
31
  self._detector_key = detector_key
32
32
  self._detector_name = detector_name
33
- ensure_torch("object_detection", ["custom", "detectors"])
34
- transformers = require_module("transformers", "object_detection", ["custom", "detectors"])
35
- self._pil = require_module("PIL.Image", "object_detection", ["custom", "detectors"])
36
- pipeline_kwargs: dict[str, Any] = {
37
- "model": schema.model,
38
- "device": schema.device or "cpu",
39
- }
40
- if schema.model_revision:
41
- pipeline_kwargs["revision"] = schema.model_revision
42
- nms = getattr(schema.nms_threshold, "root", schema.nms_threshold)
43
- if nms is not None:
44
- pipeline_kwargs["threshold"] = nms
33
+ # Model loading is deferred to first detect() so the parent process
34
+ # (which only routes when a worker pool is active) never pays the
35
+ # torch import + model memory cost.
36
+ self._pipe: Any | None = None
37
+ self._pil: Any | None = None
38
+ self._load_error: str | None = None
39
+
40
+ def _ensure_pipeline(self) -> Any | None:
41
+ if self._pipe is not None:
42
+ return self._pipe
43
+ if self._load_error is not None:
44
+ return None
45
+ schema = self._schema
45
46
  try:
46
- self._pipe: Any = transformers.pipeline("object-detection", **pipeline_kwargs)
47
- except ImportError as exc:
48
- raise MissingDependencyError(
49
- "object_detection",
50
- ["custom", "detectors"],
51
- ["custom", "detectors"],
52
- f"ObjectDetectionRunner requires additional dependencies: {exc}",
47
+ ensure_torch("object_detection", ["custom", "detectors"])
48
+ transformers = require_module(
49
+ "transformers", "object_detection", ["custom", "detectors"]
50
+ )
51
+ self._pil = require_module("PIL.Image", "object_detection", ["custom", "detectors"])
52
+ pipeline_kwargs: dict[str, Any] = {
53
+ "model": schema.model,
54
+ "device": schema.device or "cpu",
55
+ }
56
+ if schema.model_revision:
57
+ pipeline_kwargs["revision"] = schema.model_revision
58
+ nms = getattr(schema.nms_threshold, "root", schema.nms_threshold)
59
+ if nms is not None:
60
+ pipeline_kwargs["threshold"] = nms
61
+ try:
62
+ self._pipe = transformers.pipeline("object-detection", **pipeline_kwargs)
63
+ except ImportError as exc:
64
+ raise MissingDependencyError(
65
+ "object_detection",
66
+ ["custom", "detectors"],
67
+ ["custom", "detectors"],
68
+ f"ObjectDetectionRunner requires additional dependencies: {exc}",
69
+ ) from exc
70
+ return self._pipe
71
+ except Exception as exc:
72
+ # Raise on the first failure so the scan records one structured
73
+ # error; later assets skip quietly via the cached _load_error.
74
+ self._load_error = str(exc)
75
+ raise RuntimeError(
76
+ f"object_detection model '{schema.model}' failed to load for "
77
+ f"detector '{self._detector_key}': {exc}"
53
78
  ) from exc
54
79
 
55
80
  def run(self, text: str) -> None: # type: ignore[override] # pragma: no cover
@@ -60,6 +85,10 @@ class ObjectDetectionRunner(BaseRunner):
60
85
  logger.warning("object_detection: received string content, expected bytes")
61
86
  return []
62
87
 
88
+ pipe = self._ensure_pipeline()
89
+ if pipe is None:
90
+ return []
91
+
63
92
  # image/* opens directly; PDFs are rasterised to one image per page.
64
93
  images = _load_input_images(content, content_type, self._pil)
65
94
  if not images:
@@ -71,7 +100,7 @@ class ObjectDetectionRunner(BaseRunner):
71
100
  results: list[DetectionResult] = []
72
101
  for page_index, image in images:
73
102
  try:
74
- detections: list[dict[str, Any]] = self._pipe(image) or []
103
+ detections: list[dict[str, Any]] = pipe(image) or []
75
104
  for det in detections:
76
105
  label: str = det.get("label", "unknown")
77
106
  score: float = float(det.get("score", 0.0))
@@ -18,6 +18,11 @@ from ._base import BaseRunner
18
18
 
19
19
  logger = logging.getLogger(__name__)
20
20
 
21
+ # Hard per-pattern span cap: a pathological pattern on a large document would
22
+ # otherwise materialise millions of spans before the detector-level
23
+ # max_findings cap applies.
24
+ _MAX_SPANS_PER_PATTERN = 1000
25
+
21
26
 
22
27
  def _load_regex_engine() -> tuple[Any, bool]:
23
28
  """Try to load google-re2, fall back to stdlib re."""
@@ -130,6 +135,15 @@ class RegexRunner(BaseRunner):
130
135
  span["groups"] = match.groups()
131
136
 
132
137
  spans.append(span)
138
+ if len(spans) >= _MAX_SPANS_PER_PATTERN:
139
+ logger.warning(
140
+ "Pattern '%s' in detector '%s' hit the %d-span cap; "
141
+ "remaining matches dropped",
142
+ name,
143
+ self._detector_key,
144
+ _MAX_SPANS_PER_PATTERN,
145
+ )
146
+ break
133
147
  if spans:
134
148
  entities[name] = spans
135
149
 
@@ -33,21 +33,43 @@ class TextClassificationRunner(BaseRunner):
33
33
  self._schema = schema
34
34
  self._detector_key = detector_key
35
35
  self._detector_name = detector_name
36
- ensure_torch("text_classification", ["custom", "detectors"])
37
- transformers = require_module(
38
- "transformers", "text_classification", ["custom", "detectors"]
39
- )
40
- pipeline_kwargs: dict[str, Any] = {
41
- "model": schema.model,
42
- "device": schema.device or "cpu",
43
- }
44
- if schema.model_revision:
45
- pipeline_kwargs["revision"] = schema.model_revision
46
- if schema.top_k is not None:
47
- pipeline_kwargs["top_k"] = schema.top_k
48
- if schema.function_to_apply is not None:
49
- pipeline_kwargs["function_to_apply"] = str(schema.function_to_apply)
50
- self._pipe: Any = transformers.pipeline("text-classification", **pipeline_kwargs)
36
+ # Model loading is deferred to first detect() so the parent process
37
+ # (which only routes when a worker pool is active) never pays the
38
+ # torch import + model memory cost.
39
+ self._pipe: Any | None = None
40
+ self._load_error: str | None = None
41
+
42
+ def _ensure_pipeline(self) -> Any | None:
43
+ if self._pipe is not None:
44
+ return self._pipe
45
+ if self._load_error is not None:
46
+ return None
47
+ schema = self._schema
48
+ try:
49
+ ensure_torch("text_classification", ["custom", "detectors"])
50
+ transformers = require_module(
51
+ "transformers", "text_classification", ["custom", "detectors"]
52
+ )
53
+ pipeline_kwargs: dict[str, Any] = {
54
+ "model": schema.model,
55
+ "device": schema.device or "cpu",
56
+ }
57
+ if schema.model_revision:
58
+ pipeline_kwargs["revision"] = schema.model_revision
59
+ if schema.top_k is not None:
60
+ pipeline_kwargs["top_k"] = schema.top_k
61
+ if schema.function_to_apply is not None:
62
+ pipeline_kwargs["function_to_apply"] = str(schema.function_to_apply)
63
+ self._pipe = transformers.pipeline("text-classification", **pipeline_kwargs)
64
+ return self._pipe
65
+ except Exception as exc:
66
+ # Raise on the first failure so the scan records one structured
67
+ # error; later assets skip quietly via the cached _load_error.
68
+ self._load_error = str(exc)
69
+ raise RuntimeError(
70
+ f"text_classification model '{schema.model}' failed to load for "
71
+ f"detector '{self._detector_key}': {exc}"
72
+ ) from exc
51
73
 
52
74
  def run(self, text: str) -> None: # type: ignore[override] # pragma: no cover
53
75
  raise NotImplementedError("TextClassificationRunner uses detect() directly")
@@ -60,6 +82,9 @@ class TextClassificationRunner(BaseRunner):
60
82
  text = content.strip()
61
83
  if not text:
62
84
  return []
85
+ pipe = self._ensure_pipeline()
86
+ if pipe is None:
87
+ return []
63
88
 
64
89
  schema = self._schema
65
90
  chunk_size: int | None = getattr(schema.chunk_size, "root", schema.chunk_size)
@@ -74,7 +99,7 @@ class TextClassificationRunner(BaseRunner):
74
99
  call_kwargs: dict[str, Any] = {"truncation": True}
75
100
  if max_length is not None:
76
101
  call_kwargs["max_length"] = max_length
77
- raw = self._pipe(chunk, **call_kwargs) or []
102
+ raw = pipe(chunk, **call_kwargs) or []
78
103
  preds: list[dict[str, Any]] = raw[0] if raw and isinstance(raw[0], list) else raw
79
104
  for pred in preds:
80
105
  label: str = pred.get("label", "unknown")
@@ -182,10 +182,14 @@ async def run_command_async(args: argparse.Namespace, recipe: dict[str, Any]) ->
182
182
  from .pipeline.worker_pool import (
183
183
  DetectorWorkerPool,
184
184
  compute_pool_workers,
185
+ recipe_has_ml_custom_detectors,
185
186
  )
186
187
 
188
+ # ML-backed custom detectors load a model copy per worker
189
+ # process; budget ~2GB/worker for those recipes instead of 1GB.
187
190
  pool_workers = compute_pool_workers(
188
191
  override=args.max_pool_workers,
192
+ per_worker_mb=2048 if recipe_has_ml_custom_detectors(recipe) else 1024,
189
193
  )
190
194
  worker_pool: DetectorWorkerPool | None = None
191
195