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.
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/.turbo/turbo-build.log +1 -1
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/PKG-INFO +1 -1
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/package.json +1 -1
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/pyproject.toml +1 -1
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/custom/runners/_base.py +21 -2
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/custom/runners/_feature_extraction.py +42 -15
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/custom/runners/_gliner2.py +64 -16
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/custom/runners/_image_classification.py +45 -19
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/custom/runners/_llm.py +5 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/custom/runners/_object_detection.py +49 -20
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/custom/runners/_regex.py +14 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/custom/runners/_text_classification.py +41 -16
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/main.py +4 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/pipeline/worker_pool.py +54 -3
- classifyre_cli-0.4.50/tests/detectors/custom/test_runner_hardening.py +220 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/uv.lock +41 -46
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/.gitignore +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/.python-version +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/README.md +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/main.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/scripts/generate_models.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/config.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/base.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/broken_links/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/broken_links/detector.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/config.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/content/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/custom/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/custom/detector.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/custom/extractor.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/custom/runners/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/custom/runners/_factory.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/custom/trainer.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/dependencies.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/pii/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/pii/detector.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/secrets/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/secrets/detector.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/threat/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/threat/code_security_detector.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/threat/yara_detector.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/models/generated_detectors.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/models/generated_input.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/models/generated_single_asset_scan_results.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/outputs/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/outputs/base.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/outputs/console.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/outputs/factory.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/outputs/file.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/outputs/rest.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/pipeline/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/pipeline/content_provider.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/pipeline/detector_pipeline.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/pipeline/parsed_content_provider.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sandbox/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sandbox/runner.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/asset_metadata.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/atlassian_common.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/azure_blob_storage/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/azure_blob_storage/source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/base.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/confluence/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/confluence/source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/databricks/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/databricks/source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/delta_lake/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/delta_lake/source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/dependencies.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/elasticsearch/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/elasticsearch/source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/email/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/email/source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/google_cloud_storage/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/google_cloud_storage/source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/hive/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/hive/source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/iceberg/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/iceberg/source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/jira/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/jira/source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/kafka/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/kafka/source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/lakehouse_base.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/local_folder/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/local_folder/source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/meilisearch/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/meilisearch/source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/mongodb/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/mongodb/source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/mssql/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/mssql/source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/mysql/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/mysql/source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/neo4j/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/neo4j/source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/notion/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/notion/client.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/notion/source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/object_storage/base.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/opensearch/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/opensearch/source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/oracle/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/oracle/source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/postgresql/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/postgresql/source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/powerbi/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/powerbi/source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/recipe_normalizer.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/s3_client.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/s3_compatible_storage/README.md +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/s3_compatible_storage/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/s3_compatible_storage/source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/search_engine_base.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/servicedesk/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/servicedesk/source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/slack/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/slack/source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/snowflake/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/snowflake/source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/sqlite/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/sqlite/source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/tableau/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/tableau/source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/tabular_base.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/tabular_utils.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/wordpress/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/wordpress/source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/youtube/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/sources/youtube/source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/telemetry.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/utils/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/utils/content_extraction.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/utils/dependency_groups.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/utils/embedded_images.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/utils/file_metadata.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/utils/file_parser.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/utils/file_to_images.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/utils/hashing.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/utils/resources.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/utils/transcription.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/utils/uv_sync.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/utils/validation.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/_lakehouse_fakes.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/conftest.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/broken_links/test_broken_links_detector.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/conftest.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/content/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/custom/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/custom/conftest.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/custom/test_invoice_extraction.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/custom/test_llm_runner.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/custom/test_pipeline_integration.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/custom/test_regex_runner.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/custom/test_transformer_runners.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/pii/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/pii/conftest.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/pii/sample_invoice.pdf +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/pii/test_pii_detector.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/pii/test_pii_detector_extended.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/secrets/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/secrets/test_secrets_detector.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/secrets/test_secrets_detector_extended.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/test_base_detector.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/test_custom_detector_examples_runtime.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/test_detector_catalog_commercial.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/test_detector_pipeline_types.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/test_detector_schema_examples.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/test_detector_types.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/test_phase2_detectors.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/test_registry.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/threat/__init__.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/threat/test_code_security_detector.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/detectors/threat/test_yara_detector.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/integration/test_wordpress_broken_links_detector.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/integration/test_wordpress_links_assets.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/pipeline/test_detector_pipeline.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/pipeline/test_worker_pool.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_assets_metadata_catalog.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_azure_blob_storage_source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_base_source_attachment.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_base_source_sampling.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_config.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_confluence_source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_custom_extractor.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_databricks_source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_delta_lake_source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_dependency_groups.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_elasticsearch_source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_email_source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_google_cloud_storage_source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_hashing.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_hive_source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_iceberg_source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_jira_source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_kafka_source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_local_folder_source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_meilisearch_source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_mongodb_source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_mssql_source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_mysql_source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_neo4j_source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_notion_source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_opensearch_source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_oracle_source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_outputs.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_postgresql_source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_powerbi_source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_recipe_normalizer.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_s3_compatible_storage_source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_sampling_automatic.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_sandbox_runner.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_servicedesk_source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_slack_source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_snowflake_source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_source_dependency_groups.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_source_strategy_conformance.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_sqlite_source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_tableau_source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_tabular_automatic_sampling.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_tabular_utils.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_uv_sync.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_wordpress_source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_youtube_source.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/test_youtube_source_integration.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/utils/test_content_extraction.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/utils/test_embedded_images.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/utils/test_file_metadata.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/utils/test_file_parser.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/utils/test_file_to_images.py +0 -0
- {classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/tests/utils/test_transcription.py +0 -0
|
@@ -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":
|
|
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":
|
|
219
|
+
"pipeline_result": pipeline_result_dump,
|
|
201
220
|
},
|
|
202
221
|
)
|
|
203
222
|
)
|
{classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/custom/runners/_feature_extraction.py
RENAMED
|
@@ -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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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]]] =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
self.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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]] =
|
|
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)
|
{classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/custom/runners/_object_detection.py
RENAMED
|
@@ -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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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]] =
|
|
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
|
|
{classifyre_cli-0.4.48 → classifyre_cli-0.4.50}/src/detectors/custom/runners/_text_classification.py
RENAMED
|
@@ -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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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 =
|
|
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
|
|