classifyre-cli 0.4.55__tar.gz → 0.4.56__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.55 → classifyre_cli-0.4.56}/.turbo/turbo-build.log +1 -1
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/PKG-INFO +1 -1
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/package.json +1 -1
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/pyproject.toml +1 -3
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/custom/runners/__init__.py +0 -4
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/custom/runners/_factory.py +0 -5
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/main.py +5 -17
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/models/generated_detectors.py +0 -73
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/models/generated_single_asset_scan_results.py +17 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/outputs/rest.py +10 -0
- classifyre_cli-0.4.56/src/pipeline/__init__.py +25 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/pipeline/detector_pipeline.py +42 -11
- classifyre_cli-0.4.56/src/pipeline/text_artifact.py +54 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/pipeline/worker_pool.py +0 -1
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/utils/file_parser.py +53 -5
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/utils/video_processing.py +28 -7
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/custom/test_transformer_runners.py +1 -69
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/test_custom_detector_examples_runtime.py +0 -2
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/test_detector_catalog_commercial.py +1 -3
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/pipeline/test_detector_outcomes.py +45 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/pipeline/test_detector_pipeline.py +3 -2
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/utils/test_file_parser.py +21 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/utils/test_video_processing.py +45 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/uv.lock +41 -47
- classifyre_cli-0.4.55/src/detectors/custom/runners/_feature_extraction.py +0 -165
- classifyre_cli-0.4.55/src/pipeline/__init__.py +0 -7
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/.gitignore +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/.python-version +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/README.md +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/main.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/scripts/generate_models.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/config.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/base.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/broken_links/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/broken_links/detector.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/config.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/content/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/custom/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/custom/detector.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/custom/extractor.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/custom/runners/_base.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/custom/runners/_gliner2.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/custom/runners/_image_classification.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/custom/runners/_llm.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/custom/runners/_object_detection.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/custom/runners/_regex.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/custom/runners/_text_classification.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/custom/trainer.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/dependencies.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/pii/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/pii/detector.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/secrets/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/secrets/detector.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/threat/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/threat/code_security_detector.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/threat/yara_detector.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/models/generated_input.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/outputs/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/outputs/base.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/outputs/console.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/outputs/factory.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/outputs/file.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/pipeline/content_provider.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/pipeline/parsed_content_provider.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sandbox/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sandbox/runner.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/asset_metadata.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/atlassian_common.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/azure_blob_storage/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/azure_blob_storage/source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/base.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/confluence/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/confluence/source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/databricks/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/databricks/source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/delta_lake/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/delta_lake/source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/dependencies.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/elasticsearch/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/elasticsearch/source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/email/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/email/source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/google_cloud_storage/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/google_cloud_storage/source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/google_workspace/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/google_workspace/source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/hive/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/hive/source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/iceberg/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/iceberg/source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/jira/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/jira/source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/kafka/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/kafka/source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/lakehouse_base.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/local_folder/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/local_folder/source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/meilisearch/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/meilisearch/source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/microsoft_365/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/microsoft_365/source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/mongodb/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/mongodb/source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/mssql/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/mssql/source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/mysql/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/mysql/source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/neo4j/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/neo4j/source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/notion/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/notion/client.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/notion/source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/object_storage/base.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/opensearch/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/opensearch/source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/oracle/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/oracle/source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/postgresql/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/postgresql/source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/powerbi/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/powerbi/source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/recipe_normalizer.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/s3_client.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/s3_compatible_storage/README.md +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/s3_compatible_storage/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/s3_compatible_storage/source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/search_engine_base.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/servicedesk/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/servicedesk/source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/slack/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/slack/source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/snowflake/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/snowflake/source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/sqlite/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/sqlite/source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/tableau/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/tableau/source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/tabular_base.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/tabular_utils.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/wordpress/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/wordpress/source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/youtube/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/youtube/source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/telemetry.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/utils/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/utils/content_extraction.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/utils/dependency_groups.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/utils/embedded_images.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/utils/file_metadata.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/utils/file_to_images.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/utils/hashing.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/utils/resources.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/utils/transcription.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/utils/uv_sync.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/utils/validation.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/_lakehouse_fakes.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/conftest.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/broken_links/test_broken_links_detector.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/conftest.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/content/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/custom/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/custom/conftest.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/custom/test_gliner2_classification.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/custom/test_invoice_extraction.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/custom/test_llm_runner.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/custom/test_pipeline_integration.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/custom/test_regex_runner.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/custom/test_runner_hardening.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/pii/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/pii/conftest.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/pii/sample_invoice.pdf +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/pii/test_pii_chunking.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/pii/test_pii_detector.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/pii/test_pii_detector_extended.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/pii/test_pii_severity.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/secrets/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/secrets/test_secrets_detector.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/secrets/test_secrets_detector_extended.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/test_base_detector.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/test_detector_pipeline_types.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/test_detector_schema_examples.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/test_detector_types.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/test_phase2_detectors.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/test_registry.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/threat/__init__.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/threat/test_code_security_detector.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/threat/test_yara_detector.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/integration/test_wordpress_broken_links_detector.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/integration/test_wordpress_links_assets.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/pipeline/test_worker_pool.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_assets_metadata_catalog.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_azure_blob_storage_source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_base_source_attachment.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_base_source_sampling.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_config.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_confluence_source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_custom_extractor.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_databricks_source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_delta_lake_source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_dependency_groups.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_elasticsearch_source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_email_source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_google_cloud_storage_source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_google_workspace_source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_hashing.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_hive_source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_iceberg_source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_jira_source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_kafka_source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_local_folder_source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_meilisearch_source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_microsoft_365_source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_mongodb_source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_mssql_source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_mysql_source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_neo4j_source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_notion_source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_opensearch_source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_oracle_source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_outputs.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_postgresql_source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_powerbi_source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_recipe_normalizer.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_s3_compatible_storage_source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_sampling_automatic.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_sandbox_runner.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_servicedesk_source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_slack_source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_snowflake_source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_source_dependency_groups.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_source_strategy_conformance.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_sqlite_source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_tableau_source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_tabular_automatic_sampling.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_tabular_utils.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_uv_sync.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_wordpress_source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_youtube_source.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_youtube_source_integration.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/utils/test_content_extraction.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/utils/test_embedded_images.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/utils/test_file_metadata.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/utils/test_file_to_images.py +0 -0
- {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/utils/test_transcription.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "classifyre-cli"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.56"
|
|
4
4
|
description = "Classifyre CLI — scan and classify unstructured data sources"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -83,7 +83,6 @@ content = [
|
|
|
83
83
|
]
|
|
84
84
|
quality = [
|
|
85
85
|
"datasketch>=1.10.0",
|
|
86
|
-
"sentence-transformers>=5.5.0",
|
|
87
86
|
"textstat>=0.7.13",
|
|
88
87
|
"transformers>=4.50.0,<6.0.0",
|
|
89
88
|
"torch>=2.12.0",
|
|
@@ -101,7 +100,6 @@ custom = [
|
|
|
101
100
|
"datasets>=4.8.5",
|
|
102
101
|
"scikit-learn>=1.7.2",
|
|
103
102
|
"gliner2>=1.3.1",
|
|
104
|
-
"sentence-transformers>=5.5.0",
|
|
105
103
|
# Direct declaration so the [tool.uv.sources] cpu-index pin applies (timm
|
|
106
104
|
# pulls torchvision transitively; see the ocr group note).
|
|
107
105
|
"torchvision>=0.27.0",
|
|
@@ -14,7 +14,6 @@ from ._base import (
|
|
|
14
14
|
_resolve_pipeline_severity,
|
|
15
15
|
)
|
|
16
16
|
from ._factory import create_runner
|
|
17
|
-
from ._feature_extraction import FeatureExtractionRunner, _chunk_text_with_offsets, _pool_hidden
|
|
18
17
|
from ._gliner2 import (
|
|
19
18
|
GLiNER2Runner,
|
|
20
19
|
_apply_classification_validation,
|
|
@@ -35,7 +34,6 @@ __all__ = [
|
|
|
35
34
|
"_IMAGE_CONTENT_TYPES",
|
|
36
35
|
"_TEXT_CONTENT_TYPES",
|
|
37
36
|
"BaseRunner",
|
|
38
|
-
"FeatureExtractionRunner",
|
|
39
37
|
"GLiNER2Runner",
|
|
40
38
|
"ImageClassificationRunner",
|
|
41
39
|
"LLMRunner",
|
|
@@ -45,12 +43,10 @@ __all__ = [
|
|
|
45
43
|
"_apply_classification_validation",
|
|
46
44
|
"_apply_entity_validation",
|
|
47
45
|
"_chunk_text",
|
|
48
|
-
"_chunk_text_with_offsets",
|
|
49
46
|
"_load_regex_engine",
|
|
50
47
|
"_normalise_classification_output",
|
|
51
48
|
"_normalise_entity_output",
|
|
52
49
|
"_normalise_span",
|
|
53
|
-
"_pool_hidden",
|
|
54
50
|
"_resolve_pipeline_severity",
|
|
55
51
|
"create_runner",
|
|
56
52
|
]
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from ....models.generated_detectors import (
|
|
6
|
-
FeatureExtractionPipelineSchema,
|
|
7
6
|
GLiNER2PipelineSchema,
|
|
8
7
|
ImageClassificationPipelineSchema,
|
|
9
8
|
LLMPipelineSchema,
|
|
@@ -12,7 +11,6 @@ from ....models.generated_detectors import (
|
|
|
12
11
|
TextClassificationPipelineSchema,
|
|
13
12
|
)
|
|
14
13
|
from ._base import BaseRunner
|
|
15
|
-
from ._feature_extraction import FeatureExtractionRunner
|
|
16
14
|
from ._gliner2 import GLiNER2Runner
|
|
17
15
|
from ._image_classification import ImageClassificationRunner
|
|
18
16
|
from ._llm import LLMRunner
|
|
@@ -28,7 +26,6 @@ def create_runner(
|
|
|
28
26
|
| LLMPipelineSchema
|
|
29
27
|
| TextClassificationPipelineSchema
|
|
30
28
|
| ImageClassificationPipelineSchema
|
|
31
|
-
| FeatureExtractionPipelineSchema
|
|
32
29
|
| ObjectDetectionPipelineSchema
|
|
33
30
|
),
|
|
34
31
|
detector_key: str = "",
|
|
@@ -39,8 +36,6 @@ def create_runner(
|
|
|
39
36
|
return TextClassificationRunner(schema, detector_key, detector_name)
|
|
40
37
|
if isinstance(schema, ImageClassificationPipelineSchema):
|
|
41
38
|
return ImageClassificationRunner(schema, detector_key, detector_name)
|
|
42
|
-
if isinstance(schema, FeatureExtractionPipelineSchema):
|
|
43
|
-
return FeatureExtractionRunner(schema, detector_key, detector_name)
|
|
44
39
|
if isinstance(schema, ObjectDetectionPipelineSchema):
|
|
45
40
|
return ObjectDetectionRunner(schema, detector_key, detector_name)
|
|
46
41
|
if isinstance(schema, RegexPipelineSchema):
|
|
@@ -264,7 +264,7 @@ async def run_command_async(args: argparse.Namespace, recipe: dict[str, Any]) ->
|
|
|
264
264
|
logger.info("Phase 1 complete: %d assets discovered", total_assets)
|
|
265
265
|
|
|
266
266
|
# --- Phase 2: Processing ---
|
|
267
|
-
if
|
|
267
|
+
if all_stubs:
|
|
268
268
|
import asyncio as _asyncio
|
|
269
269
|
|
|
270
270
|
processed_count = 0
|
|
@@ -319,6 +319,10 @@ async def run_command_async(args: argparse.Namespace, recipe: dict[str, Any]) ->
|
|
|
319
319
|
)
|
|
320
320
|
payload = _asset_to_payload(result)
|
|
321
321
|
await sink.emit_batch([payload], skip_findings=False)
|
|
322
|
+
if hasattr(sink, "emit_text_chunks"):
|
|
323
|
+
artifact = pipeline.take_text_artifact(asset_hash)
|
|
324
|
+
if artifact is not None:
|
|
325
|
+
await sink.emit_text_chunks(asset_hash, artifact)
|
|
322
326
|
|
|
323
327
|
if hasattr(sink, "update_asset_status"):
|
|
324
328
|
f_total, f_by_sev, f_by_det = _compute_findings_counts(
|
|
@@ -353,22 +357,6 @@ async def run_command_async(args: argparse.Namespace, recipe: dict[str, Any]) ->
|
|
|
353
357
|
processed_count,
|
|
354
358
|
error_count,
|
|
355
359
|
)
|
|
356
|
-
elif all_stubs and hasattr(sink, "update_asset_status"):
|
|
357
|
-
# No detectors configured: mark discovered assets as PROCESSED
|
|
358
|
-
import asyncio as _asyncio
|
|
359
|
-
|
|
360
|
-
async def _mark_processed(asset: Any) -> None:
|
|
361
|
-
asset_hash = getattr(asset, "hash", None) or ""
|
|
362
|
-
await sink.update_asset_status(
|
|
363
|
-
asset_hash, "PROCESSED", findings_total=0
|
|
364
|
-
)
|
|
365
|
-
|
|
366
|
-
tasks = [_asyncio.create_task(_mark_processed(a)) for a in all_stubs]
|
|
367
|
-
await _asyncio.gather(*tasks, return_exceptions=True)
|
|
368
|
-
logger.info(
|
|
369
|
-
"Phase 2 skipped (no detectors): %d assets marked processed",
|
|
370
|
-
len(all_stubs),
|
|
371
|
-
)
|
|
372
360
|
|
|
373
361
|
# Persist the advanced AUTOMATIC sampling cursor (no-op for
|
|
374
362
|
# other strategies, which return None). Only on the normal
|
|
@@ -1255,78 +1255,6 @@ class ImageClassificationPipelineSchema(BaseModel):
|
|
|
1255
1255
|
|
|
1256
1256
|
|
|
1257
1257
|
class Type7(StrEnum):
|
|
1258
|
-
FEATURE_EXTRACTION = 'FEATURE_EXTRACTION'
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
class PoolingStrategy(StrEnum):
|
|
1262
|
-
"""
|
|
1263
|
-
How to aggregate per-token hidden states into a single embedding vector.
|
|
1264
|
-
"""
|
|
1265
|
-
|
|
1266
|
-
mean = 'mean'
|
|
1267
|
-
cls = 'cls'
|
|
1268
|
-
max = 'max'
|
|
1269
|
-
none = 'none'
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
class ChunkSize2(RootModel[int]):
|
|
1273
|
-
root: int = Field(
|
|
1274
|
-
None,
|
|
1275
|
-
description='Split text into chunks of this many characters before embedding. Each chunk produces its own finding.',
|
|
1276
|
-
ge=1,
|
|
1277
|
-
)
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
class FeatureExtractionPipelineSchema(BaseModel):
|
|
1281
|
-
"""
|
|
1282
|
-
Feature extraction (embedding) pipeline using a HuggingFace model. Runs a single encoder; create multiple custom detectors to run multiple embedding models.
|
|
1283
|
-
"""
|
|
1284
|
-
|
|
1285
|
-
model_config = ConfigDict(
|
|
1286
|
-
extra='forbid',
|
|
1287
|
-
)
|
|
1288
|
-
type: Literal['FEATURE_EXTRACTION']
|
|
1289
|
-
model: str = Field(
|
|
1290
|
-
...,
|
|
1291
|
-
description="HuggingFace hub ID (e.g. 'BAAI/bge-base-en-v1.5', 'sentence-transformers/all-MiniLM-L6-v2') or absolute local directory path.",
|
|
1292
|
-
)
|
|
1293
|
-
model_revision: str | None = Field(
|
|
1294
|
-
None,
|
|
1295
|
-
description='Git branch, tag, or commit hash when fetching from the HuggingFace hub.',
|
|
1296
|
-
)
|
|
1297
|
-
device: str | None = Field(
|
|
1298
|
-
'cpu',
|
|
1299
|
-
description="Inference device: 'cpu' (default), 'cuda', 'mps', or a CUDA device string like 'cuda:0'.",
|
|
1300
|
-
)
|
|
1301
|
-
pooling_strategy: PoolingStrategy | None = Field(
|
|
1302
|
-
'mean',
|
|
1303
|
-
description='How to aggregate per-token hidden states into a single embedding vector.',
|
|
1304
|
-
)
|
|
1305
|
-
normalize_embeddings: bool | None = Field(
|
|
1306
|
-
True,
|
|
1307
|
-
description='L2-normalise the final embedding vector. Recommended for cosine-similarity workloads.',
|
|
1308
|
-
)
|
|
1309
|
-
truncation: bool | None = Field(
|
|
1310
|
-
True, description="Truncate input to the model's maximum sequence length."
|
|
1311
|
-
)
|
|
1312
|
-
max_length: int | None = Field(
|
|
1313
|
-
None, description="Override the tokenizer's default maximum sequence length."
|
|
1314
|
-
)
|
|
1315
|
-
batch_size: int | None = Field(
|
|
1316
|
-
8, description='Number of texts to encode in a single forward pass.'
|
|
1317
|
-
)
|
|
1318
|
-
chunk_size: ChunkSize2 | None = Field(
|
|
1319
|
-
None,
|
|
1320
|
-
description='Split text into chunks of this many characters before embedding. Each chunk produces its own finding.',
|
|
1321
|
-
)
|
|
1322
|
-
chunk_overlap: ChunkOverlap1 | None = Field(
|
|
1323
|
-
0,
|
|
1324
|
-
description='Character overlap between consecutive chunks.',
|
|
1325
|
-
validate_default=True,
|
|
1326
|
-
)
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
class Type8(StrEnum):
|
|
1330
1258
|
OBJECT_DETECTION = 'OBJECT_DETECTION'
|
|
1331
1259
|
|
|
1332
1260
|
|
|
@@ -1407,7 +1335,6 @@ class CustomDetectorConfig(DetectorConfig):
|
|
|
1407
1335
|
| LLMPipelineSchema
|
|
1408
1336
|
| TextClassificationPipelineSchema
|
|
1409
1337
|
| ImageClassificationPipelineSchema
|
|
1410
|
-
| FeatureExtractionPipelineSchema
|
|
1411
1338
|
| ObjectDetectionPipelineSchema
|
|
1412
1339
|
| None
|
|
1413
1340
|
) = Field(None, discriminator='type', title='AnyPipelineSchema')
|
{classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/models/generated_single_asset_scan_results.py
RENAMED
|
@@ -87,6 +87,19 @@ class Location(BaseModel):
|
|
|
87
87
|
end: int | None = Field(None, description='End offset (0-indexed)')
|
|
88
88
|
|
|
89
89
|
|
|
90
|
+
class TextExtractionStatus(StrEnum):
|
|
91
|
+
"""
|
|
92
|
+
Structured text coverage outcome. EXTRACTED means detector-visible text was produced. EMPTY means extraction completed without an error but yielded no text. ENGINE_UNAVAILABLE, ZERO_FRAMES, and FAILED are incomplete coverage states and must not be treated as an empty document.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
NOT_APPLICABLE = 'NOT_APPLICABLE'
|
|
96
|
+
EXTRACTED = 'EXTRACTED'
|
|
97
|
+
EMPTY = 'EMPTY'
|
|
98
|
+
ENGINE_UNAVAILABLE = 'ENGINE_UNAVAILABLE'
|
|
99
|
+
ZERO_FRAMES = 'ZERO_FRAMES'
|
|
100
|
+
FAILED = 'FAILED'
|
|
101
|
+
|
|
102
|
+
|
|
90
103
|
class DetectionResult(BaseModel):
|
|
91
104
|
"""
|
|
92
105
|
Result from detector scan
|
|
@@ -221,6 +234,10 @@ class ScanStats(BaseModel):
|
|
|
221
234
|
None,
|
|
222
235
|
description="True when an asset that should carry text yielded none. Most significant for assets whose text is derived rather than native (OCR'd images and scanned PDFs, transcribed audio/video): empty derived text means the content may have been missed entirely, not that the document is blank. Distinct from an error — nothing failed — so it is reported as coverage, not as a run failure.",
|
|
223
236
|
)
|
|
237
|
+
text_extraction_status: TextExtractionStatus | None = Field(
|
|
238
|
+
None,
|
|
239
|
+
description='Structured text coverage outcome. EXTRACTED means detector-visible text was produced. EMPTY means extraction completed without an error but yielded no text. ENGINE_UNAVAILABLE, ZERO_FRAMES, and FAILED are incomplete coverage states and must not be treated as an empty document.',
|
|
240
|
+
)
|
|
224
241
|
findings_count: int | None = Field(
|
|
225
242
|
None, description='Total number of findings detected'
|
|
226
243
|
)
|
|
@@ -10,6 +10,7 @@ from pydantic import BaseModel, ConfigDict, Field
|
|
|
10
10
|
from requests.adapters import HTTPAdapter
|
|
11
11
|
from urllib3.util.retry import Retry # type: ignore[import-untyped]
|
|
12
12
|
|
|
13
|
+
from ..pipeline.text_artifact import TextArtifact
|
|
13
14
|
from .base import OutputRuntimeContext, OutputType
|
|
14
15
|
|
|
15
16
|
logger = logging.getLogger(__name__)
|
|
@@ -223,6 +224,15 @@ class RestOutputSink:
|
|
|
223
224
|
payload.model_dump(mode="json", by_alias=True),
|
|
224
225
|
)
|
|
225
226
|
|
|
227
|
+
async def emit_text_chunks(self, asset_hash: str, artifact: TextArtifact) -> None:
|
|
228
|
+
"""Send extracted text provenance; the API embeds it asynchronously."""
|
|
229
|
+
source_id = self._require_source_id()
|
|
230
|
+
self._request_json(
|
|
231
|
+
"POST",
|
|
232
|
+
f"/sources/{source_id}/embeddings/chunks",
|
|
233
|
+
{"assetHash": asset_hash, "chunks": artifact.chunks},
|
|
234
|
+
)
|
|
235
|
+
|
|
226
236
|
def _split_by_size(self, assets: list[dict[str, Any]]) -> list[list[dict[str, Any]]]:
|
|
227
237
|
"""Split assets into chunks that each stay under _MAX_BATCH_BYTES."""
|
|
228
238
|
import json as _json
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Pipeline for processing assets through detectors.
|
|
2
|
+
|
|
3
|
+
Exports are lazy so lightweight callers do not import the detector and source
|
|
4
|
+
graph (which itself uses the REST output types).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
__all__ = ["ContentProvider", "DetectorPipeline", "ParsedContentProvider"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def __getattr__(name: str) -> Any:
|
|
13
|
+
if name == "ContentProvider":
|
|
14
|
+
from .content_provider import ContentProvider
|
|
15
|
+
|
|
16
|
+
return ContentProvider
|
|
17
|
+
if name == "DetectorPipeline":
|
|
18
|
+
from .detector_pipeline import DetectorPipeline
|
|
19
|
+
|
|
20
|
+
return DetectorPipeline
|
|
21
|
+
if name == "ParsedContentProvider":
|
|
22
|
+
from .parsed_content_provider import ParsedContentProvider
|
|
23
|
+
|
|
24
|
+
return ParsedContentProvider
|
|
25
|
+
raise AttributeError(name)
|
|
@@ -20,10 +20,12 @@ from ..models.generated_single_asset_scan_results import (
|
|
|
20
20
|
ScanStats,
|
|
21
21
|
SingleAssetScanResults,
|
|
22
22
|
Status,
|
|
23
|
+
TextExtractionStatus,
|
|
23
24
|
)
|
|
24
25
|
from ..sources.base import BaseSource
|
|
25
|
-
from ..utils.file_parser import resolve_mime_type
|
|
26
|
+
from ..utils.file_parser import TextExtractionCoverageError, resolve_mime_type
|
|
26
27
|
from .content_provider import ContentProvider
|
|
28
|
+
from .text_artifact import TextArtifact
|
|
27
29
|
from .worker_pool import DetectorWorkerPool, is_io_bound_detector
|
|
28
30
|
|
|
29
31
|
logger = logging.getLogger(__name__)
|
|
@@ -67,6 +69,12 @@ class DetectorPipeline:
|
|
|
67
69
|
|
|
68
70
|
self.content_provider = ParsedContentProvider(source)
|
|
69
71
|
self.init_warnings: list[str] = []
|
|
72
|
+
self._text_artifacts: dict[str, TextArtifact] = {}
|
|
73
|
+
self._text_extraction_errors: dict[str, list[str]] = {}
|
|
74
|
+
self._text_extraction_statuses: dict[str, TextExtractionStatus] = {}
|
|
75
|
+
|
|
76
|
+
def take_text_artifact(self, asset_hash: str) -> TextArtifact | None:
|
|
77
|
+
return self._text_artifacts.pop(asset_hash, None)
|
|
70
78
|
|
|
71
79
|
def _register_detector_info(self, detector: BaseDetector, info: _DetectorInfo) -> None:
|
|
72
80
|
self._detector_info[id(detector)] = info
|
|
@@ -105,10 +113,6 @@ class DetectorPipeline:
|
|
|
105
113
|
findings_flush_size: int = 50,
|
|
106
114
|
) -> SingleAssetScanResults:
|
|
107
115
|
"""Process a single asset through detectors."""
|
|
108
|
-
if not self.detectors:
|
|
109
|
-
asset.findings = []
|
|
110
|
-
return asset
|
|
111
|
-
|
|
112
116
|
scan_started = datetime.now(UTC)
|
|
113
117
|
text_content_type = self._text_content_type_for_asset(asset.asset_type)
|
|
114
118
|
link_content = self._build_links_payload(asset.links)
|
|
@@ -167,8 +171,10 @@ class DetectorPipeline:
|
|
|
167
171
|
# Per-asset, per-detector outcomes. Local to this call, so concurrent
|
|
168
172
|
# assets never share it.
|
|
169
173
|
outcome_sink: dict[tuple[DetectorType, str | None], DetectorOutcome] = {}
|
|
174
|
+
artifact = TextArtifact()
|
|
175
|
+
self._text_artifacts[str(asset.hash)] = artifact
|
|
170
176
|
|
|
171
|
-
if
|
|
177
|
+
if text_content_type:
|
|
172
178
|
(
|
|
173
179
|
text_findings,
|
|
174
180
|
text_detector_types_run,
|
|
@@ -187,6 +193,7 @@ class DetectorPipeline:
|
|
|
187
193
|
findings.extend(text_findings)
|
|
188
194
|
scan_warnings.extend(text_warnings)
|
|
189
195
|
scan_errors.extend(text_errors)
|
|
196
|
+
scan_errors.extend(self._text_extraction_errors.pop(str(asset.hash), []))
|
|
190
197
|
detector_types_run = self._merge_detector_types(
|
|
191
198
|
detector_types_run,
|
|
192
199
|
text_detector_types_run,
|
|
@@ -243,6 +250,16 @@ class DetectorPipeline:
|
|
|
243
250
|
# Structured rather than left for a consumer to parse out of the
|
|
244
251
|
# warning strings, so missing-text coverage is queryable.
|
|
245
252
|
empty_text=should_warn_on_empty_text and content_size == 0,
|
|
253
|
+
text_extraction_status=self._text_extraction_statuses.pop(
|
|
254
|
+
str(asset.hash),
|
|
255
|
+
(
|
|
256
|
+
TextExtractionStatus.EXTRACTED
|
|
257
|
+
if text_content_type and content_size > 0
|
|
258
|
+
else TextExtractionStatus.EMPTY
|
|
259
|
+
if text_content_type
|
|
260
|
+
else TextExtractionStatus.NOT_APPLICABLE
|
|
261
|
+
),
|
|
262
|
+
),
|
|
246
263
|
findings_count=len(findings),
|
|
247
264
|
warnings=scan_warnings or None,
|
|
248
265
|
errors=scan_errors or None,
|
|
@@ -354,6 +371,7 @@ class DetectorPipeline:
|
|
|
354
371
|
async for text_content in self._iter_text_content_pages(asset):
|
|
355
372
|
page_index += 1
|
|
356
373
|
content_size += len(text_content)
|
|
374
|
+
self._text_artifacts[str(asset.hash)].add_page(text_content, page_index)
|
|
357
375
|
|
|
358
376
|
if not text_content:
|
|
359
377
|
continue
|
|
@@ -484,6 +502,7 @@ class DetectorPipeline:
|
|
|
484
502
|
async for text_content in self._iter_text_content_pages(asset):
|
|
485
503
|
page_index += 1
|
|
486
504
|
content_size += len(text_content)
|
|
505
|
+
self._text_artifacts[str(asset.hash)].add_page(text_content, page_index)
|
|
487
506
|
|
|
488
507
|
if not text_content:
|
|
489
508
|
continue
|
|
@@ -535,11 +554,23 @@ class DetectorPipeline:
|
|
|
535
554
|
|
|
536
555
|
for candidate_id in candidate_ids:
|
|
537
556
|
saw_candidate_content = False
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
557
|
+
try:
|
|
558
|
+
async for text_content in self.content_provider.fetch_text_pages(candidate_id):
|
|
559
|
+
if not text_content:
|
|
560
|
+
continue
|
|
561
|
+
saw_candidate_content = True
|
|
562
|
+
yield text_content
|
|
563
|
+
except TextExtractionCoverageError as exc:
|
|
564
|
+
# Text extraction failure must not discard independent link or
|
|
565
|
+
# binary detector results. The structured empty-text coverage
|
|
566
|
+
# signal still records that this asset was not embedded.
|
|
567
|
+
message = f"Text extraction failed for {candidate_id}: {exc}"
|
|
568
|
+
logger.warning(message)
|
|
569
|
+
self._text_extraction_errors.setdefault(str(asset.hash), []).append(message)
|
|
570
|
+
self._text_extraction_statuses[str(asset.hash)] = TextExtractionStatus(
|
|
571
|
+
exc.code.value
|
|
572
|
+
)
|
|
573
|
+
return
|
|
543
574
|
|
|
544
575
|
if saw_candidate_content:
|
|
545
576
|
return
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Extracted text chunks sent to the API for asynchronous semantic indexing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
_WHITESPACE = re.compile(r"\s+")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def normalize_text(text: str) -> str:
|
|
13
|
+
return _WHITESPACE.sub(" ", text).strip()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def chunk_text_with_offsets(
|
|
17
|
+
text: str, *, max_chars: int = 1200, overlap_chars: int = 200
|
|
18
|
+
) -> list[tuple[str, int, int]]:
|
|
19
|
+
normalized = normalize_text(text)
|
|
20
|
+
if not normalized:
|
|
21
|
+
return []
|
|
22
|
+
chunks: list[tuple[str, int, int]] = []
|
|
23
|
+
start = 0
|
|
24
|
+
while start < len(normalized):
|
|
25
|
+
end = min(len(normalized), start + max_chars)
|
|
26
|
+
if end < len(normalized):
|
|
27
|
+
boundary = normalized.rfind(" ", start + max_chars // 2, end)
|
|
28
|
+
if boundary > start:
|
|
29
|
+
end = boundary
|
|
30
|
+
value = normalized[start:end].strip()
|
|
31
|
+
if value:
|
|
32
|
+
actual_start = normalized.find(value, start, end + 1)
|
|
33
|
+
chunks.append((value, actual_start, len(value)))
|
|
34
|
+
if end >= len(normalized):
|
|
35
|
+
break
|
|
36
|
+
start = max(start + 1, end - overlap_chars)
|
|
37
|
+
return chunks
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class TextArtifact:
|
|
42
|
+
chunks: list[dict[str, Any]] = field(default_factory=list)
|
|
43
|
+
|
|
44
|
+
def add_page(self, text: str, page: int) -> None:
|
|
45
|
+
for value, offset, length in chunk_text_with_offsets(text):
|
|
46
|
+
self.chunks.append(
|
|
47
|
+
{
|
|
48
|
+
"ordinal": len(self.chunks),
|
|
49
|
+
"page": page,
|
|
50
|
+
"charOffset": offset,
|
|
51
|
+
"charLength": length,
|
|
52
|
+
"text": value,
|
|
53
|
+
}
|
|
54
|
+
)
|
|
@@ -7,12 +7,34 @@ import tempfile
|
|
|
7
7
|
import threading
|
|
8
8
|
from collections.abc import Generator
|
|
9
9
|
from dataclasses import dataclass
|
|
10
|
+
from enum import StrEnum
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
from urllib.parse import urlsplit
|
|
12
13
|
|
|
13
14
|
logger = logging.getLogger(__name__)
|
|
14
15
|
|
|
15
16
|
|
|
17
|
+
class TextExtractionCoverageCode(StrEnum):
|
|
18
|
+
"""Machine-readable reason detector-visible text could not be produced."""
|
|
19
|
+
|
|
20
|
+
ENGINE_UNAVAILABLE = "ENGINE_UNAVAILABLE"
|
|
21
|
+
ZERO_FRAMES = "ZERO_FRAMES"
|
|
22
|
+
FAILED = "FAILED"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TextExtractionCoverageError(RuntimeError):
|
|
26
|
+
"""Extraction failed after source content was fetched successfully."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
message: str,
|
|
31
|
+
*,
|
|
32
|
+
code: TextExtractionCoverageCode = TextExtractionCoverageCode.FAILED,
|
|
33
|
+
) -> None:
|
|
34
|
+
super().__init__(message)
|
|
35
|
+
self.code = code
|
|
36
|
+
|
|
37
|
+
|
|
16
38
|
@dataclass
|
|
17
39
|
class ParsedFile:
|
|
18
40
|
"""Result of parsing a local file."""
|
|
@@ -432,6 +454,7 @@ def extract_text(
|
|
|
432
454
|
Returns:
|
|
433
455
|
(text_content, error_message_or_None)
|
|
434
456
|
"""
|
|
457
|
+
ocr_error: str | None = None
|
|
435
458
|
if _supports_docling_ocr(mime_type, file_name):
|
|
436
459
|
# PDFs: try cheap native text extraction first. Only hand off to the
|
|
437
460
|
# heavy docling pipeline when the native path yields too little text,
|
|
@@ -460,6 +483,7 @@ def extract_text(
|
|
|
460
483
|
return text, None
|
|
461
484
|
if error:
|
|
462
485
|
logger.warning("OCR extraction failed for %s: %s", file_name or mime_type, error)
|
|
486
|
+
ocr_error = error
|
|
463
487
|
|
|
464
488
|
# Media processing is based on detected content type, never source config.
|
|
465
489
|
if mime_type.startswith("audio/"):
|
|
@@ -497,11 +521,12 @@ def extract_text(
|
|
|
497
521
|
# Unsupported image formats have no native text path; supported images were
|
|
498
522
|
# already handled by the automatic OCR branch above.
|
|
499
523
|
if mime_type.startswith("image/"):
|
|
500
|
-
return "",
|
|
524
|
+
return "", ocr_error
|
|
501
525
|
|
|
502
526
|
# PDF
|
|
503
527
|
if mime_type == "application/pdf":
|
|
504
|
-
|
|
528
|
+
native_text, native_error = _extract_pdf_text(file_bytes)
|
|
529
|
+
return native_text, native_error or ocr_error
|
|
505
530
|
|
|
506
531
|
# DOCX
|
|
507
532
|
if mime_type == "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
|
|
@@ -576,7 +601,7 @@ def extract_text(
|
|
|
576
601
|
return "", f"Parquet extraction failed: {e}"
|
|
577
602
|
|
|
578
603
|
# Unknown / binary
|
|
579
|
-
return "",
|
|
604
|
+
return "", ocr_error
|
|
580
605
|
|
|
581
606
|
|
|
582
607
|
def _decode_bytes(file_bytes: bytes) -> str:
|
|
@@ -707,7 +732,11 @@ def iter_file_pages(
|
|
|
707
732
|
)
|
|
708
733
|
elif normalized.startswith("video/"):
|
|
709
734
|
from .transcription import iter_transcription_pages
|
|
710
|
-
from .video_processing import
|
|
735
|
+
from .video_processing import (
|
|
736
|
+
VideoOCREngineUnavailableError,
|
|
737
|
+
VideoOCRZeroFramesError,
|
|
738
|
+
iter_video_ocr_segments,
|
|
739
|
+
)
|
|
711
740
|
|
|
712
741
|
try:
|
|
713
742
|
yield from iter_transcription_pages(
|
|
@@ -721,7 +750,15 @@ def iter_file_pages(
|
|
|
721
750
|
try:
|
|
722
751
|
yield from iter_video_ocr_segments(file_bytes, file_name=file_name)
|
|
723
752
|
except Exception as exc:
|
|
724
|
-
|
|
753
|
+
code = TextExtractionCoverageCode.FAILED
|
|
754
|
+
if isinstance(exc, VideoOCREngineUnavailableError):
|
|
755
|
+
code = TextExtractionCoverageCode.ENGINE_UNAVAILABLE
|
|
756
|
+
elif isinstance(exc, VideoOCRZeroFramesError):
|
|
757
|
+
code = TextExtractionCoverageCode.ZERO_FRAMES
|
|
758
|
+
raise TextExtractionCoverageError(
|
|
759
|
+
f"Video OCR coverage failed for {file_name or mime_type}: {exc}",
|
|
760
|
+
code=code,
|
|
761
|
+
) from exc
|
|
725
762
|
else:
|
|
726
763
|
text, error = extract_text(
|
|
727
764
|
file_bytes,
|
|
@@ -730,6 +767,17 @@ def iter_file_pages(
|
|
|
730
767
|
)
|
|
731
768
|
if error:
|
|
732
769
|
logger.warning("Text extraction error (%s): %s", mime_type, error)
|
|
770
|
+
if not text:
|
|
771
|
+
normalized_error = error.casefold()
|
|
772
|
+
unavailable = "unavailable" in normalized_error or "dependency" in normalized_error
|
|
773
|
+
raise TextExtractionCoverageError(
|
|
774
|
+
f"Text extraction coverage failed for {file_name or mime_type}: {error}",
|
|
775
|
+
code=(
|
|
776
|
+
TextExtractionCoverageCode.ENGINE_UNAVAILABLE
|
|
777
|
+
if unavailable
|
|
778
|
+
else TextExtractionCoverageCode.FAILED
|
|
779
|
+
),
|
|
780
|
+
)
|
|
733
781
|
if text:
|
|
734
782
|
yield from _iter_text_lines(text, batch_size)
|
|
735
783
|
|