classifyre-cli 0.4.60__tar.gz → 0.4.62__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.60 → classifyre_cli-0.4.62}/.turbo/turbo-build.log +1 -1
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/PKG-INFO +1 -1
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/package.json +1 -1
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/pyproject.toml +1 -1
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/custom/runners/_llm.py +100 -12
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/models/generated_input.py +136 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/pipeline/detector_pipeline.py +9 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/custom/test_llm_runner.py +58 -1
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/pipeline/test_detector_pipeline.py +38 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/uv.lock +103 -103
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/.gitignore +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/.python-version +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/README.md +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/main.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/scripts/generate_models.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/config.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/base.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/broken_links/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/broken_links/detector.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/config.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/content/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/custom/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/custom/detector.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/custom/extractor.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/custom/runners/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/custom/runners/_base.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/custom/runners/_factory.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/custom/runners/_gliner2.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/custom/runners/_image_classification.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/custom/runners/_object_detection.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/custom/runners/_regex.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/custom/runners/_text_classification.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/custom/trainer.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/dependencies.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/pii/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/pii/detector.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/secrets/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/secrets/detector.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/threat/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/threat/code_security_detector.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/threat/yara_detector.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/file_evaluation/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/file_evaluation/runner.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/main.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/models/generated_detectors.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/models/generated_single_asset_scan_results.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/outputs/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/outputs/base.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/outputs/console.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/outputs/factory.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/outputs/file.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/outputs/rest.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/pipeline/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/pipeline/content_provider.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/pipeline/parsed_content_provider.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/pipeline/text_artifact.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/pipeline/worker_pool.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/asset_metadata.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/atlassian_common.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/azure_blob_storage/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/azure_blob_storage/source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/base.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/confluence/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/confluence/source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/databricks/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/databricks/source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/delta_lake/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/delta_lake/source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/dependencies.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/elasticsearch/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/elasticsearch/source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/email/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/email/source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/google_cloud_storage/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/google_cloud_storage/source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/google_workspace/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/google_workspace/source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/hive/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/hive/source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/iceberg/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/iceberg/source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/jira/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/jira/source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/kafka/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/kafka/source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/lakehouse_base.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/local_folder/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/local_folder/source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/meilisearch/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/meilisearch/source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/microsoft_365/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/microsoft_365/source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/mongodb/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/mongodb/source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/mssql/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/mssql/source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/mysql/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/mysql/source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/neo4j/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/neo4j/source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/notion/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/notion/client.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/notion/source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/object_storage/base.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/opensearch/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/opensearch/source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/oracle/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/oracle/source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/postgresql/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/postgresql/source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/powerbi/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/powerbi/source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/recipe_normalizer.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/s3_client.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/s3_compatible_storage/README.md +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/s3_compatible_storage/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/s3_compatible_storage/source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/sandbox/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/sandbox/source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/search_engine_base.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/servicedesk/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/servicedesk/source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/slack/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/slack/source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/snowflake/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/snowflake/source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/sqlite/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/sqlite/source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/tableau/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/tableau/source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/tabular_base.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/tabular_utils.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/wordpress/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/wordpress/source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/youtube/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/youtube/source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/telemetry.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/utils/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/utils/archive_extraction.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/utils/content_extraction.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/utils/dependency_groups.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/utils/embedded_images.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/utils/file_metadata.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/utils/file_parser.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/utils/file_to_images.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/utils/hashing.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/utils/legacy_office.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/utils/resources.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/utils/transcription.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/utils/uv_sync.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/utils/validation.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/utils/video_processing.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/_lakehouse_fakes.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/conftest.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/broken_links/test_broken_links_detector.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/conftest.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/content/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/custom/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/custom/conftest.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/custom/test_gliner2_classification.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/custom/test_invoice_extraction.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/custom/test_pipeline_integration.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/custom/test_regex_runner.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/custom/test_runner_hardening.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/custom/test_transformer_runners.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/pii/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/pii/conftest.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/pii/sample_invoice.pdf +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/pii/test_pii_chunking.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/pii/test_pii_detector.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/pii/test_pii_detector_extended.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/pii/test_pii_severity.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/secrets/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/secrets/test_secrets_detector.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/secrets/test_secrets_detector_extended.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/test_base_detector.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/test_custom_detector_examples_runtime.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/test_detector_catalog_commercial.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/test_detector_pipeline_types.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/test_detector_schema_examples.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/test_detector_types.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/test_phase2_detectors.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/test_registry.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/threat/__init__.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/threat/test_code_security_detector.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/threat/test_yara_detector.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/integration/test_wordpress_broken_links_detector.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/integration/test_wordpress_links_assets.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/pipeline/test_detector_outcomes.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/pipeline/test_worker_pool.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_assets_metadata_catalog.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_azure_blob_storage_source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_base_source_attachment.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_base_source_sampling.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_config.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_confluence_source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_custom_extractor.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_databricks_source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_delta_lake_source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_dependency_groups.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_elasticsearch_source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_email_source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_file_evaluation_runner.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_google_cloud_storage_source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_google_workspace_source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_hashing.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_hive_source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_iceberg_source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_jira_source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_kafka_source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_local_folder_source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_main.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_meilisearch_source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_microsoft_365_source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_mongodb_source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_mssql_source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_mysql_source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_neo4j_source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_notion_source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_opensearch_source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_oracle_source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_outputs.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_postgresql_source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_powerbi_source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_recipe_normalizer.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_s3_compatible_storage_source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_sampling_automatic.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_sandbox_source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_servicedesk_source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_slack_source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_snowflake_source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_source_dependency_groups.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_source_strategy_conformance.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_sqlite_source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_tableau_source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_tabular_automatic_sampling.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_tabular_utils.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_uv_sync.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_wordpress_source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_youtube_source.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_youtube_source_integration.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/utils/test_archive_extraction.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/utils/test_content_extraction.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/utils/test_embedded_images.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/utils/test_file_metadata.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/utils/test_file_parser.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/utils/test_file_to_images.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/utils/test_legacy_office.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/utils/test_transcription.py +0 -0
- {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/utils/test_video_processing.py +0 -0
|
@@ -6,6 +6,8 @@ import base64
|
|
|
6
6
|
import json
|
|
7
7
|
import logging
|
|
8
8
|
import os
|
|
9
|
+
import random
|
|
10
|
+
import time
|
|
9
11
|
from datetime import UTC, datetime
|
|
10
12
|
from typing import Any
|
|
11
13
|
|
|
@@ -43,6 +45,23 @@ _MAX_VISION_IMAGES = 20
|
|
|
43
45
|
# worker for litellm's ~10-minute default. Overridable per deployment.
|
|
44
46
|
_COMPLETION_TIMEOUT_SECONDS = float(os.environ.get("CLASSIFYRE_LLM_TIMEOUT_SECONDS", "90"))
|
|
45
47
|
|
|
48
|
+
# Retry budget for transient provider failures (429 rate limits, 5xx, timeouts).
|
|
49
|
+
# Exponential backoff with jitter, honouring Retry-After when the provider
|
|
50
|
+
# sends one, so a saturated endpoint is backed off from instead of hammered.
|
|
51
|
+
_MAX_COMPLETION_ATTEMPTS = max(1, int(os.environ.get("CLASSIFYRE_LLM_MAX_ATTEMPTS", "4")))
|
|
52
|
+
_RETRY_BACKOFF_BASE_SECONDS = float(os.environ.get("CLASSIFYRE_LLM_RETRY_BASE_SECONDS", "2"))
|
|
53
|
+
_RETRY_BACKOFF_MAX_SECONDS = 60.0
|
|
54
|
+
_RETRYABLE_STATUS_CODES = {408, 429, 500, 502, 503, 504}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class LLMCompletionError(RuntimeError):
|
|
58
|
+
"""Provider call failed after retries.
|
|
59
|
+
|
|
60
|
+
Carries only a string message so it survives pickling across the detector
|
|
61
|
+
worker-pool process boundary; the pipeline records the detector's outcome
|
|
62
|
+
as ERROR instead of a silent empty result.
|
|
63
|
+
"""
|
|
64
|
+
|
|
46
65
|
|
|
47
66
|
class LLMRunner(BaseRunner):
|
|
48
67
|
"""AI detector — sends content to a configured LLM provider for classification + extraction."""
|
|
@@ -130,18 +149,8 @@ class LLMRunner(BaseRunner):
|
|
|
130
149
|
*,
|
|
131
150
|
vision_pages: int | None = None,
|
|
132
151
|
) -> list[DetectionResult]:
|
|
133
|
-
schema = self._schema
|
|
134
152
|
try:
|
|
135
|
-
response = self.
|
|
136
|
-
model=self._model_string(),
|
|
137
|
-
api_key=self._runtime.api_key,
|
|
138
|
-
api_base=self._runtime.base_url or None,
|
|
139
|
-
temperature=schema.temperature if schema.temperature is not None else 0.0,
|
|
140
|
-
max_tokens=self._max_tokens(),
|
|
141
|
-
messages=messages,
|
|
142
|
-
response_format={"type": "json_object"},
|
|
143
|
-
timeout=_COMPLETION_TIMEOUT_SECONDS,
|
|
144
|
-
)
|
|
153
|
+
response = self._complete_with_backoff(messages)
|
|
145
154
|
raw = response.choices[0].message.content or "{}"
|
|
146
155
|
parsed = self._parse_json(raw)
|
|
147
156
|
except Exception as exc:
|
|
@@ -152,10 +161,70 @@ class LLMRunner(BaseRunner):
|
|
|
152
161
|
exc,
|
|
153
162
|
exc_info=True,
|
|
154
163
|
)
|
|
155
|
-
|
|
164
|
+
# Propagate instead of returning [] — an empty result here is
|
|
165
|
+
# indistinguishable from "genuinely found nothing" and would let
|
|
166
|
+
# the pipeline record this detector OK on an asset it never saw.
|
|
167
|
+
raise LLMCompletionError(
|
|
168
|
+
f"LLM provider call failed for detector '{self._detector_key}' "
|
|
169
|
+
f"(model={self._runtime.model}): {exc}"
|
|
170
|
+
) from None
|
|
156
171
|
|
|
157
172
|
return self._results_from_payload(snippet, parsed, vision_pages=vision_pages)
|
|
158
173
|
|
|
174
|
+
def _complete_with_backoff(self, messages: list[dict[str, Any]]) -> Any:
|
|
175
|
+
schema = self._schema
|
|
176
|
+
for attempt in range(1, _MAX_COMPLETION_ATTEMPTS + 1):
|
|
177
|
+
try:
|
|
178
|
+
return self._litellm.completion(
|
|
179
|
+
model=self._model_string(),
|
|
180
|
+
api_key=self._runtime.api_key,
|
|
181
|
+
api_base=self._runtime.base_url or None,
|
|
182
|
+
temperature=schema.temperature if schema.temperature is not None else 0.0,
|
|
183
|
+
max_tokens=self._max_tokens(),
|
|
184
|
+
messages=messages,
|
|
185
|
+
response_format={"type": "json_object"},
|
|
186
|
+
timeout=_COMPLETION_TIMEOUT_SECONDS,
|
|
187
|
+
)
|
|
188
|
+
except Exception as exc:
|
|
189
|
+
if attempt >= _MAX_COMPLETION_ATTEMPTS or not self._is_retryable(exc):
|
|
190
|
+
raise
|
|
191
|
+
delay = min(
|
|
192
|
+
_RETRY_BACKOFF_MAX_SECONDS,
|
|
193
|
+
_RETRY_BACKOFF_BASE_SECONDS * (2 ** (attempt - 1)),
|
|
194
|
+
)
|
|
195
|
+
retry_after = _retry_after_seconds(exc)
|
|
196
|
+
if retry_after is not None:
|
|
197
|
+
delay = min(_RETRY_BACKOFF_MAX_SECONDS, max(delay, retry_after))
|
|
198
|
+
delay *= 0.75 + random.random() * 0.5 # ±25% jitter
|
|
199
|
+
logger.warning(
|
|
200
|
+
"llm provider transient failure (detector=%s, model=%s, "
|
|
201
|
+
"attempt %d/%d), retrying in %.1fs: %s",
|
|
202
|
+
self._detector_key,
|
|
203
|
+
self._runtime.model,
|
|
204
|
+
attempt,
|
|
205
|
+
_MAX_COMPLETION_ATTEMPTS,
|
|
206
|
+
delay,
|
|
207
|
+
exc,
|
|
208
|
+
)
|
|
209
|
+
time.sleep(delay)
|
|
210
|
+
raise LLMCompletionError("unreachable") # pragma: no cover
|
|
211
|
+
|
|
212
|
+
def _is_retryable(self, exc: Exception) -> bool:
|
|
213
|
+
retryable_types = tuple(
|
|
214
|
+
t
|
|
215
|
+
for t in (
|
|
216
|
+
getattr(self._litellm, "RateLimitError", None),
|
|
217
|
+
getattr(self._litellm, "Timeout", None),
|
|
218
|
+
getattr(self._litellm, "ServiceUnavailableError", None),
|
|
219
|
+
getattr(self._litellm, "InternalServerError", None),
|
|
220
|
+
getattr(self._litellm, "APIConnectionError", None),
|
|
221
|
+
)
|
|
222
|
+
if isinstance(t, type)
|
|
223
|
+
)
|
|
224
|
+
if retryable_types and isinstance(exc, retryable_types):
|
|
225
|
+
return True
|
|
226
|
+
return getattr(exc, "status_code", None) in _RETRYABLE_STATUS_CODES
|
|
227
|
+
|
|
159
228
|
def _vision_enabled(self) -> bool:
|
|
160
229
|
return bool(getattr(self._runtime, "supports_vision", False))
|
|
161
230
|
|
|
@@ -298,3 +367,22 @@ class LLMRunner(BaseRunner):
|
|
|
298
367
|
@staticmethod
|
|
299
368
|
def _coerce_fields(raw: Any) -> dict[str, Any]:
|
|
300
369
|
return {str(k): v for k, v in raw.items()} if isinstance(raw, dict) else {}
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _retry_after_seconds(exc: Exception) -> float | None:
|
|
373
|
+
"""Best-effort extraction of a Retry-After hint from a provider exception."""
|
|
374
|
+
response = getattr(exc, "response", None)
|
|
375
|
+
headers = getattr(response, "headers", None)
|
|
376
|
+
if headers is None:
|
|
377
|
+
return None
|
|
378
|
+
try:
|
|
379
|
+
raw = headers.get("retry-after")
|
|
380
|
+
except Exception:
|
|
381
|
+
return None
|
|
382
|
+
if raw is None:
|
|
383
|
+
return None
|
|
384
|
+
try:
|
|
385
|
+
value = float(raw)
|
|
386
|
+
except (TypeError, ValueError):
|
|
387
|
+
return None
|
|
388
|
+
return value if value > 0 else None
|
|
@@ -2122,6 +2122,10 @@ class CoreInput(BaseModel):
|
|
|
2122
2122
|
)
|
|
2123
2123
|
sampling: SamplingConfig
|
|
2124
2124
|
resources: ResourceOverrides | None = None
|
|
2125
|
+
cleanup_removed_detector_findings: bool | None = Field(
|
|
2126
|
+
True,
|
|
2127
|
+
description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
|
|
2128
|
+
)
|
|
2125
2129
|
|
|
2126
2130
|
|
|
2127
2131
|
class SlackInput(CoreInput):
|
|
@@ -2142,6 +2146,10 @@ class SlackInput(CoreInput):
|
|
|
2142
2146
|
)
|
|
2143
2147
|
sampling: SamplingConfig
|
|
2144
2148
|
resources: ResourceOverrides | None = None
|
|
2149
|
+
cleanup_removed_detector_findings: bool | None = Field(
|
|
2150
|
+
True,
|
|
2151
|
+
description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
|
|
2152
|
+
)
|
|
2145
2153
|
|
|
2146
2154
|
|
|
2147
2155
|
class EmailInput(CoreInput):
|
|
@@ -2160,6 +2168,10 @@ class EmailInput(CoreInput):
|
|
|
2160
2168
|
)
|
|
2161
2169
|
sampling: SamplingConfig
|
|
2162
2170
|
resources: ResourceOverrides | None = None
|
|
2171
|
+
cleanup_removed_detector_findings: bool | None = Field(
|
|
2172
|
+
True,
|
|
2173
|
+
description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
|
|
2174
|
+
)
|
|
2163
2175
|
|
|
2164
2176
|
|
|
2165
2177
|
class S3CompatibleStorageInput(CoreInput):
|
|
@@ -2178,6 +2190,10 @@ class S3CompatibleStorageInput(CoreInput):
|
|
|
2178
2190
|
)
|
|
2179
2191
|
sampling: SamplingConfig
|
|
2180
2192
|
resources: ResourceOverrides | None = None
|
|
2193
|
+
cleanup_removed_detector_findings: bool | None = Field(
|
|
2194
|
+
True,
|
|
2195
|
+
description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
|
|
2196
|
+
)
|
|
2181
2197
|
|
|
2182
2198
|
|
|
2183
2199
|
class LocalFolderInput(CoreInput):
|
|
@@ -2196,6 +2212,10 @@ class LocalFolderInput(CoreInput):
|
|
|
2196
2212
|
)
|
|
2197
2213
|
sampling: SamplingConfig
|
|
2198
2214
|
resources: ResourceOverrides | None = None
|
|
2215
|
+
cleanup_removed_detector_findings: bool | None = Field(
|
|
2216
|
+
True,
|
|
2217
|
+
description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
|
|
2218
|
+
)
|
|
2199
2219
|
|
|
2200
2220
|
|
|
2201
2221
|
class SandboxInput(CoreInput):
|
|
@@ -2214,6 +2234,10 @@ class SandboxInput(CoreInput):
|
|
|
2214
2234
|
)
|
|
2215
2235
|
sampling: SamplingConfig
|
|
2216
2236
|
resources: ResourceOverrides | None = None
|
|
2237
|
+
cleanup_removed_detector_findings: bool | None = Field(
|
|
2238
|
+
True,
|
|
2239
|
+
description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
|
|
2240
|
+
)
|
|
2217
2241
|
|
|
2218
2242
|
|
|
2219
2243
|
class AzureBlobStorageInput(CoreInput):
|
|
@@ -2232,6 +2256,10 @@ class AzureBlobStorageInput(CoreInput):
|
|
|
2232
2256
|
)
|
|
2233
2257
|
sampling: SamplingConfig
|
|
2234
2258
|
resources: ResourceOverrides | None = None
|
|
2259
|
+
cleanup_removed_detector_findings: bool | None = Field(
|
|
2260
|
+
True,
|
|
2261
|
+
description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
|
|
2262
|
+
)
|
|
2235
2263
|
|
|
2236
2264
|
|
|
2237
2265
|
class GoogleCloudStorageInput(CoreInput):
|
|
@@ -2250,6 +2278,10 @@ class GoogleCloudStorageInput(CoreInput):
|
|
|
2250
2278
|
)
|
|
2251
2279
|
sampling: SamplingConfig
|
|
2252
2280
|
resources: ResourceOverrides | None = None
|
|
2281
|
+
cleanup_removed_detector_findings: bool | None = Field(
|
|
2282
|
+
True,
|
|
2283
|
+
description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
|
|
2284
|
+
)
|
|
2253
2285
|
|
|
2254
2286
|
|
|
2255
2287
|
class WordPressInput(CoreInput):
|
|
@@ -2268,6 +2300,10 @@ class WordPressInput(CoreInput):
|
|
|
2268
2300
|
)
|
|
2269
2301
|
sampling: SamplingConfig
|
|
2270
2302
|
resources: ResourceOverrides | None = None
|
|
2303
|
+
cleanup_removed_detector_findings: bool | None = Field(
|
|
2304
|
+
True,
|
|
2305
|
+
description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
|
|
2306
|
+
)
|
|
2271
2307
|
|
|
2272
2308
|
|
|
2273
2309
|
class PostgreSQLInput(CoreInput):
|
|
@@ -2286,6 +2322,10 @@ class PostgreSQLInput(CoreInput):
|
|
|
2286
2322
|
)
|
|
2287
2323
|
sampling: SamplingConfig
|
|
2288
2324
|
resources: ResourceOverrides | None = None
|
|
2325
|
+
cleanup_removed_detector_findings: bool | None = Field(
|
|
2326
|
+
True,
|
|
2327
|
+
description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
|
|
2328
|
+
)
|
|
2289
2329
|
|
|
2290
2330
|
|
|
2291
2331
|
class MySQLInput(CoreInput):
|
|
@@ -2304,6 +2344,10 @@ class MySQLInput(CoreInput):
|
|
|
2304
2344
|
)
|
|
2305
2345
|
sampling: SamplingConfig
|
|
2306
2346
|
resources: ResourceOverrides | None = None
|
|
2347
|
+
cleanup_removed_detector_findings: bool | None = Field(
|
|
2348
|
+
True,
|
|
2349
|
+
description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
|
|
2350
|
+
)
|
|
2307
2351
|
|
|
2308
2352
|
|
|
2309
2353
|
class MSSQLInput(CoreInput):
|
|
@@ -2322,6 +2366,10 @@ class MSSQLInput(CoreInput):
|
|
|
2322
2366
|
)
|
|
2323
2367
|
sampling: SamplingConfig
|
|
2324
2368
|
resources: ResourceOverrides | None = None
|
|
2369
|
+
cleanup_removed_detector_findings: bool | None = Field(
|
|
2370
|
+
True,
|
|
2371
|
+
description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
|
|
2372
|
+
)
|
|
2325
2373
|
|
|
2326
2374
|
|
|
2327
2375
|
class OracleInput(CoreInput):
|
|
@@ -2340,6 +2388,10 @@ class OracleInput(CoreInput):
|
|
|
2340
2388
|
)
|
|
2341
2389
|
sampling: SamplingConfig
|
|
2342
2390
|
resources: ResourceOverrides | None = None
|
|
2391
|
+
cleanup_removed_detector_findings: bool | None = Field(
|
|
2392
|
+
True,
|
|
2393
|
+
description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
|
|
2394
|
+
)
|
|
2343
2395
|
|
|
2344
2396
|
|
|
2345
2397
|
class HiveInput(CoreInput):
|
|
@@ -2358,6 +2410,10 @@ class HiveInput(CoreInput):
|
|
|
2358
2410
|
)
|
|
2359
2411
|
sampling: SamplingConfig
|
|
2360
2412
|
resources: ResourceOverrides | None = None
|
|
2413
|
+
cleanup_removed_detector_findings: bool | None = Field(
|
|
2414
|
+
True,
|
|
2415
|
+
description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
|
|
2416
|
+
)
|
|
2361
2417
|
|
|
2362
2418
|
|
|
2363
2419
|
class DatabricksInput(CoreInput):
|
|
@@ -2380,6 +2436,10 @@ class DatabricksInput(CoreInput):
|
|
|
2380
2436
|
)
|
|
2381
2437
|
sampling: SamplingConfig
|
|
2382
2438
|
resources: ResourceOverrides | None = None
|
|
2439
|
+
cleanup_removed_detector_findings: bool | None = Field(
|
|
2440
|
+
True,
|
|
2441
|
+
description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
|
|
2442
|
+
)
|
|
2383
2443
|
|
|
2384
2444
|
|
|
2385
2445
|
class SnowflakeInput(CoreInput):
|
|
@@ -2408,6 +2468,10 @@ class SnowflakeInput(CoreInput):
|
|
|
2408
2468
|
)
|
|
2409
2469
|
sampling: SamplingConfig
|
|
2410
2470
|
resources: ResourceOverrides | None = None
|
|
2471
|
+
cleanup_removed_detector_findings: bool | None = Field(
|
|
2472
|
+
True,
|
|
2473
|
+
description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
|
|
2474
|
+
)
|
|
2411
2475
|
|
|
2412
2476
|
|
|
2413
2477
|
class MongoDBInput(CoreInput):
|
|
@@ -2430,6 +2494,10 @@ class MongoDBInput(CoreInput):
|
|
|
2430
2494
|
)
|
|
2431
2495
|
sampling: SamplingConfig
|
|
2432
2496
|
resources: ResourceOverrides | None = None
|
|
2497
|
+
cleanup_removed_detector_findings: bool | None = Field(
|
|
2498
|
+
True,
|
|
2499
|
+
description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
|
|
2500
|
+
)
|
|
2433
2501
|
|
|
2434
2502
|
|
|
2435
2503
|
class Neo4jRequired(BaseModel):
|
|
@@ -2556,6 +2624,10 @@ class Neo4jInput(CoreInput):
|
|
|
2556
2624
|
)
|
|
2557
2625
|
sampling: SamplingConfig
|
|
2558
2626
|
resources: ResourceOverrides | None = None
|
|
2627
|
+
cleanup_removed_detector_findings: bool | None = Field(
|
|
2628
|
+
True,
|
|
2629
|
+
description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
|
|
2630
|
+
)
|
|
2559
2631
|
|
|
2560
2632
|
|
|
2561
2633
|
class PowerBIInput(CoreInput):
|
|
@@ -2578,6 +2650,10 @@ class PowerBIInput(CoreInput):
|
|
|
2578
2650
|
)
|
|
2579
2651
|
sampling: SamplingConfig
|
|
2580
2652
|
resources: ResourceOverrides | None = None
|
|
2653
|
+
cleanup_removed_detector_findings: bool | None = Field(
|
|
2654
|
+
True,
|
|
2655
|
+
description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
|
|
2656
|
+
)
|
|
2581
2657
|
|
|
2582
2658
|
|
|
2583
2659
|
class TableauInput(CoreInput):
|
|
@@ -2600,6 +2676,10 @@ class TableauInput(CoreInput):
|
|
|
2600
2676
|
)
|
|
2601
2677
|
sampling: SamplingConfig
|
|
2602
2678
|
resources: ResourceOverrides | None = None
|
|
2679
|
+
cleanup_removed_detector_findings: bool | None = Field(
|
|
2680
|
+
True,
|
|
2681
|
+
description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
|
|
2682
|
+
)
|
|
2603
2683
|
|
|
2604
2684
|
|
|
2605
2685
|
class ConfluenceRequired(BaseModel):
|
|
@@ -3006,6 +3086,10 @@ class ConfluenceInput(CoreInput):
|
|
|
3006
3086
|
)
|
|
3007
3087
|
sampling: SamplingConfig
|
|
3008
3088
|
resources: ResourceOverrides | None = None
|
|
3089
|
+
cleanup_removed_detector_findings: bool | None = Field(
|
|
3090
|
+
True,
|
|
3091
|
+
description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
|
|
3092
|
+
)
|
|
3009
3093
|
|
|
3010
3094
|
|
|
3011
3095
|
class JiraInput(CoreInput):
|
|
@@ -3024,6 +3108,10 @@ class JiraInput(CoreInput):
|
|
|
3024
3108
|
)
|
|
3025
3109
|
sampling: SamplingConfig
|
|
3026
3110
|
resources: ResourceOverrides | None = None
|
|
3111
|
+
cleanup_removed_detector_findings: bool | None = Field(
|
|
3112
|
+
True,
|
|
3113
|
+
description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
|
|
3114
|
+
)
|
|
3027
3115
|
|
|
3028
3116
|
|
|
3029
3117
|
class ServiceDeskInput(CoreInput):
|
|
@@ -3042,6 +3130,10 @@ class ServiceDeskInput(CoreInput):
|
|
|
3042
3130
|
)
|
|
3043
3131
|
sampling: SamplingConfig
|
|
3044
3132
|
resources: ResourceOverrides | None = None
|
|
3133
|
+
cleanup_removed_detector_findings: bool | None = Field(
|
|
3134
|
+
True,
|
|
3135
|
+
description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
|
|
3136
|
+
)
|
|
3045
3137
|
|
|
3046
3138
|
|
|
3047
3139
|
class SQLiteRequired(BaseModel):
|
|
@@ -3098,6 +3190,10 @@ class SQLiteInput(CoreInput):
|
|
|
3098
3190
|
)
|
|
3099
3191
|
sampling: SamplingConfig
|
|
3100
3192
|
resources: ResourceOverrides | None = None
|
|
3193
|
+
cleanup_removed_detector_findings: bool | None = Field(
|
|
3194
|
+
True,
|
|
3195
|
+
description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
|
|
3196
|
+
)
|
|
3101
3197
|
|
|
3102
3198
|
|
|
3103
3199
|
class NotionRequired(BaseModel):
|
|
@@ -3229,6 +3325,10 @@ class NotionInput(CoreInput):
|
|
|
3229
3325
|
)
|
|
3230
3326
|
sampling: SamplingConfig
|
|
3231
3327
|
resources: ResourceOverrides | None = None
|
|
3328
|
+
cleanup_removed_detector_findings: bool | None = Field(
|
|
3329
|
+
True,
|
|
3330
|
+
description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
|
|
3331
|
+
)
|
|
3232
3332
|
|
|
3233
3333
|
|
|
3234
3334
|
class KafkaSecurityProtocol(StrEnum):
|
|
@@ -3424,6 +3524,10 @@ class KafkaInput(CoreInput):
|
|
|
3424
3524
|
)
|
|
3425
3525
|
sampling: SamplingConfig
|
|
3426
3526
|
resources: ResourceOverrides | None = None
|
|
3527
|
+
cleanup_removed_detector_findings: bool | None = Field(
|
|
3528
|
+
True,
|
|
3529
|
+
description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
|
|
3530
|
+
)
|
|
3427
3531
|
|
|
3428
3532
|
|
|
3429
3533
|
class NoAuthentication2(BaseModel):
|
|
@@ -3554,6 +3658,10 @@ class ElasticsearchInput(CoreInput):
|
|
|
3554
3658
|
)
|
|
3555
3659
|
sampling: SamplingConfig
|
|
3556
3660
|
resources: ResourceOverrides | None = None
|
|
3661
|
+
cleanup_removed_detector_findings: bool | None = Field(
|
|
3662
|
+
True,
|
|
3663
|
+
description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
|
|
3664
|
+
)
|
|
3557
3665
|
|
|
3558
3666
|
|
|
3559
3667
|
class OpenSearchOptional(BaseModel):
|
|
@@ -3584,6 +3692,10 @@ class OpenSearchInput(CoreInput):
|
|
|
3584
3692
|
)
|
|
3585
3693
|
sampling: SamplingConfig
|
|
3586
3694
|
resources: ResourceOverrides | None = None
|
|
3695
|
+
cleanup_removed_detector_findings: bool | None = Field(
|
|
3696
|
+
True,
|
|
3697
|
+
description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
|
|
3698
|
+
)
|
|
3587
3699
|
|
|
3588
3700
|
|
|
3589
3701
|
class NoAuthentication4(BaseModel):
|
|
@@ -3672,6 +3784,10 @@ class MeilisearchInput(CoreInput):
|
|
|
3672
3784
|
)
|
|
3673
3785
|
sampling: SamplingConfig
|
|
3674
3786
|
resources: ResourceOverrides | None = None
|
|
3787
|
+
cleanup_removed_detector_findings: bool | None = Field(
|
|
3788
|
+
True,
|
|
3789
|
+
description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
|
|
3790
|
+
)
|
|
3675
3791
|
|
|
3676
3792
|
|
|
3677
3793
|
class LakehouseStorageConnection(BaseModel):
|
|
@@ -3908,6 +4024,10 @@ class Microsoft365Input(CoreInput):
|
|
|
3908
4024
|
)
|
|
3909
4025
|
sampling: SamplingConfig
|
|
3910
4026
|
resources: ResourceOverrides | None = None
|
|
4027
|
+
cleanup_removed_detector_findings: bool | None = Field(
|
|
4028
|
+
True,
|
|
4029
|
+
description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
|
|
4030
|
+
)
|
|
3911
4031
|
|
|
3912
4032
|
|
|
3913
4033
|
class GoogleWorkspaceRequiredServiceAccount(BaseModel):
|
|
@@ -4048,6 +4168,10 @@ class GoogleWorkspaceInput(CoreInput):
|
|
|
4048
4168
|
)
|
|
4049
4169
|
sampling: SamplingConfig
|
|
4050
4170
|
resources: ResourceOverrides | None = None
|
|
4171
|
+
cleanup_removed_detector_findings: bool | None = Field(
|
|
4172
|
+
True,
|
|
4173
|
+
description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
|
|
4174
|
+
)
|
|
4051
4175
|
|
|
4052
4176
|
|
|
4053
4177
|
class YouTubeInput(CoreInput):
|
|
@@ -4066,6 +4190,10 @@ class YouTubeInput(CoreInput):
|
|
|
4066
4190
|
)
|
|
4067
4191
|
sampling: SamplingConfig
|
|
4068
4192
|
resources: ResourceOverrides | None = None
|
|
4193
|
+
cleanup_removed_detector_findings: bool | None = Field(
|
|
4194
|
+
True,
|
|
4195
|
+
description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
|
|
4196
|
+
)
|
|
4069
4197
|
|
|
4070
4198
|
|
|
4071
4199
|
class DeltaLakeOptional(BaseModel):
|
|
@@ -4092,6 +4220,10 @@ class DeltaLakeInput(CoreInput):
|
|
|
4092
4220
|
)
|
|
4093
4221
|
sampling: SamplingConfig
|
|
4094
4222
|
resources: ResourceOverrides | None = None
|
|
4223
|
+
cleanup_removed_detector_findings: bool | None = Field(
|
|
4224
|
+
True,
|
|
4225
|
+
description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
|
|
4226
|
+
)
|
|
4095
4227
|
|
|
4096
4228
|
|
|
4097
4229
|
class IcebergOptional(BaseModel):
|
|
@@ -4118,6 +4250,10 @@ class IcebergInput(CoreInput):
|
|
|
4118
4250
|
)
|
|
4119
4251
|
sampling: SamplingConfig
|
|
4120
4252
|
resources: ResourceOverrides | None = None
|
|
4253
|
+
cleanup_removed_detector_findings: bool | None = Field(
|
|
4254
|
+
True,
|
|
4255
|
+
description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
|
|
4256
|
+
)
|
|
4121
4257
|
|
|
4122
4258
|
|
|
4123
4259
|
class SourceInput(
|
|
@@ -150,6 +150,15 @@ class DetectorPipeline:
|
|
|
150
150
|
"application/x.asset-links",
|
|
151
151
|
)
|
|
152
152
|
]
|
|
153
|
+
# A detector can qualify for both the text and the binary bucket — a
|
|
154
|
+
# vision-enabled LLM detector supports text plus image/pdf types — and
|
|
155
|
+
# would then run twice per asset (2x cost, 2x provider rate-limit
|
|
156
|
+
# pressure). Keep both passes only for IMAGE assets, where OCR text and
|
|
157
|
+
# pixels are complementary evidence; everywhere else (e.g. extensionless
|
|
158
|
+
# text files typed BINARY) the raw-bytes pass duplicates the text pass.
|
|
159
|
+
if text_detectors and binary_detectors and asset.asset_type != OutputAssetType.IMAGE:
|
|
160
|
+
text_ids = {id(d) for d in text_detectors}
|
|
161
|
+
binary_detectors = [d for d in binary_detectors if id(d) not in text_ids]
|
|
153
162
|
# Any asset we resolved a text content type for is expected to yield
|
|
154
163
|
# text, so warn whenever it yields none. This previously covered only
|
|
155
164
|
# TXT/TABLE/URL — excluding exactly the types whose text is *derived*
|
|
@@ -11,7 +11,7 @@ from unittest.mock import MagicMock
|
|
|
11
11
|
|
|
12
12
|
import pytest
|
|
13
13
|
|
|
14
|
-
from src.detectors.custom.runners._llm import LLMRunner
|
|
14
|
+
from src.detectors.custom.runners._llm import LLMCompletionError, LLMRunner
|
|
15
15
|
from src.models.generated_detectors import (
|
|
16
16
|
LLMLabelDefinition,
|
|
17
17
|
LLMOutputField,
|
|
@@ -161,6 +161,63 @@ def test_malformed_json_returns_no_findings() -> None:
|
|
|
161
161
|
assert runner.detect(TEXT, "text/plain") == []
|
|
162
162
|
|
|
163
163
|
|
|
164
|
+
# ── Provider-failure handling (BUG A) ─────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class _RateLimitError(Exception):
|
|
168
|
+
status_code = 429
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class _AuthError(Exception):
|
|
172
|
+
status_code = 401
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_provider_error_raises_instead_of_empty_result(monkeypatch) -> None:
|
|
176
|
+
# A dead provider must surface as an error the pipeline records, never as
|
|
177
|
+
# a silent zero-findings result indistinguishable from a clean scan.
|
|
178
|
+
monkeypatch.setattr("src.detectors.custom.runners._llm.time.sleep", lambda _s: None)
|
|
179
|
+
completion = MagicMock(side_effect=_RateLimitError("rate limited"))
|
|
180
|
+
runner = _runner(_schema(), completion)
|
|
181
|
+
with pytest.raises(LLMCompletionError, match="sentiment"):
|
|
182
|
+
runner.detect(TEXT, "text/plain")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_transient_provider_error_retries_then_succeeds(monkeypatch) -> None:
|
|
186
|
+
sleeps: list[float] = []
|
|
187
|
+
monkeypatch.setattr("src.detectors.custom.runners._llm.time.sleep", sleeps.append)
|
|
188
|
+
good = _mock_completion({"labels": [{"name": "bad", "confidence": 0.9}]})
|
|
189
|
+
completion = MagicMock(
|
|
190
|
+
side_effect=[_RateLimitError("429"), _RateLimitError("503"), good.return_value]
|
|
191
|
+
)
|
|
192
|
+
runner = _runner(_schema(), completion)
|
|
193
|
+
|
|
194
|
+
results = runner.detect(TEXT, "text/plain")
|
|
195
|
+
|
|
196
|
+
assert [r.finding_type for r in results] == ["bad"]
|
|
197
|
+
assert completion.call_count == 3
|
|
198
|
+
assert len(sleeps) == 2
|
|
199
|
+
assert sleeps[1] > sleeps[0] # exponential backoff
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_non_retryable_provider_error_fails_fast(monkeypatch) -> None:
|
|
203
|
+
monkeypatch.setattr("src.detectors.custom.runners._llm.time.sleep", lambda _s: None)
|
|
204
|
+
completion = MagicMock(side_effect=_AuthError("bad key"))
|
|
205
|
+
runner = _runner(_schema(), completion)
|
|
206
|
+
with pytest.raises(LLMCompletionError):
|
|
207
|
+
runner.detect(TEXT, "text/plain")
|
|
208
|
+
assert completion.call_count == 1
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def test_completion_error_is_picklable() -> None:
|
|
212
|
+
# The error crosses the detector worker-pool process boundary.
|
|
213
|
+
import pickle
|
|
214
|
+
|
|
215
|
+
err = LLMCompletionError("LLM provider call failed for detector 'x': 429")
|
|
216
|
+
restored = pickle.loads(pickle.dumps(err))
|
|
217
|
+
assert isinstance(restored, LLMCompletionError)
|
|
218
|
+
assert "429" in str(restored)
|
|
219
|
+
|
|
220
|
+
|
|
164
221
|
# ── Vision / file input ───────────────────────────────────────────────────────
|
|
165
222
|
|
|
166
223
|
|
|
@@ -554,6 +554,44 @@ async def test_pipeline_runs_mixed_detector_on_ocr_text_and_binary_payloads() ->
|
|
|
554
554
|
assert mixed_detector.seen_content_types == ["text/plain", "image/png"]
|
|
555
555
|
|
|
556
556
|
|
|
557
|
+
@pytest.mark.asyncio
|
|
558
|
+
async def test_pipeline_runs_mixed_detector_once_on_binary_typed_text_assets() -> None:
|
|
559
|
+
# A detector supporting both text and binary types (e.g. a vision LLM
|
|
560
|
+
# detector) must not run twice on BINARY-typed assets whose text is the
|
|
561
|
+
# primary evidence — the raw-bytes pass would duplicate the text pass.
|
|
562
|
+
file_bytes = b"plain text stored without an extension"
|
|
563
|
+
source = OcrBinarySource(
|
|
564
|
+
{"type": "DUMMY"},
|
|
565
|
+
content="",
|
|
566
|
+
binary_data=file_bytes,
|
|
567
|
+
binary_mime="application/octet-stream",
|
|
568
|
+
ocr_pages=["extracted text"],
|
|
569
|
+
)
|
|
570
|
+
mixed_detector = RecordingDetector(["text/plain", "image/png"])
|
|
571
|
+
|
|
572
|
+
pipeline = DetectorPipeline(
|
|
573
|
+
detectors=[mixed_detector],
|
|
574
|
+
source=source,
|
|
575
|
+
runner_id="runner-binary-mixed",
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
now = datetime.now(UTC)
|
|
579
|
+
binary_asset = SingleAssetScanResults(
|
|
580
|
+
hash="bin-1",
|
|
581
|
+
checksum="checksum",
|
|
582
|
+
name="asset-bin-1",
|
|
583
|
+
external_url="urn:test/bin-1",
|
|
584
|
+
links=[],
|
|
585
|
+
asset_type=AssetType.BINARY,
|
|
586
|
+
created_at=now,
|
|
587
|
+
updated_at=now,
|
|
588
|
+
)
|
|
589
|
+
[_asset] = await pipeline.process([binary_asset])
|
|
590
|
+
|
|
591
|
+
assert mixed_detector.seen == ["extracted text"]
|
|
592
|
+
assert mixed_detector.seen_content_types == ["text/plain"]
|
|
593
|
+
|
|
594
|
+
|
|
557
595
|
@pytest.mark.asyncio
|
|
558
596
|
async def test_pipeline_resolves_effective_binary_mime_from_bytes_and_filename() -> None:
|
|
559
597
|
image_bytes = b"\xff\xd8\xffjpeg-data"
|