classifyre-cli 0.4.57__tar.gz → 0.4.58__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.58/.turbo/turbo-build.log +3 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/PKG-INFO +1 -1
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/package.json +1 -1
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/pyproject.toml +16 -3
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/file_evaluation/runner.py +5 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/main.py +70 -8
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/object_storage/base.py +141 -6
- classifyre_cli-0.4.58/src/utils/archive_extraction.py +281 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/utils/file_metadata.py +19 -1
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/utils/file_parser.py +346 -21
- classifyre_cli-0.4.58/src/utils/legacy_office.py +152 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_local_folder_source.py +109 -0
- classifyre_cli-0.4.58/tests/test_main.py +36 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_s3_compatible_storage_source.py +45 -0
- classifyre_cli-0.4.58/tests/utils/test_archive_extraction.py +153 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/utils/test_file_parser.py +287 -0
- classifyre_cli-0.4.58/tests/utils/test_legacy_office.py +66 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/uv.lock +616 -426
- classifyre_cli-0.4.57/.turbo/turbo-build.log +0 -3
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/.gitignore +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/.python-version +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/README.md +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/main.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/scripts/generate_models.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/config.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/detectors/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/detectors/base.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/detectors/broken_links/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/detectors/broken_links/detector.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/detectors/config.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/detectors/content/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/detectors/custom/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/detectors/custom/detector.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/detectors/custom/extractor.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/detectors/custom/runners/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/detectors/custom/runners/_base.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/detectors/custom/runners/_factory.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/detectors/custom/runners/_gliner2.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/detectors/custom/runners/_image_classification.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/detectors/custom/runners/_llm.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/detectors/custom/runners/_object_detection.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/detectors/custom/runners/_regex.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/detectors/custom/runners/_text_classification.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/detectors/custom/trainer.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/detectors/dependencies.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/detectors/pii/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/detectors/pii/detector.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/detectors/secrets/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/detectors/secrets/detector.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/detectors/threat/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/detectors/threat/code_security_detector.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/detectors/threat/yara_detector.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/file_evaluation/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/models/generated_detectors.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/models/generated_input.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/models/generated_single_asset_scan_results.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/outputs/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/outputs/base.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/outputs/console.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/outputs/factory.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/outputs/file.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/outputs/rest.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/pipeline/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/pipeline/content_provider.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/pipeline/detector_pipeline.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/pipeline/parsed_content_provider.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/pipeline/text_artifact.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/pipeline/worker_pool.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/asset_metadata.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/atlassian_common.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/azure_blob_storage/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/azure_blob_storage/source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/base.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/confluence/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/confluence/source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/databricks/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/databricks/source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/delta_lake/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/delta_lake/source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/dependencies.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/elasticsearch/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/elasticsearch/source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/email/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/email/source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/google_cloud_storage/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/google_cloud_storage/source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/google_workspace/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/google_workspace/source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/hive/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/hive/source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/iceberg/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/iceberg/source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/jira/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/jira/source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/kafka/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/kafka/source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/lakehouse_base.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/local_folder/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/local_folder/source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/meilisearch/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/meilisearch/source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/microsoft_365/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/microsoft_365/source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/mongodb/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/mongodb/source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/mssql/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/mssql/source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/mysql/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/mysql/source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/neo4j/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/neo4j/source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/notion/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/notion/client.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/notion/source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/opensearch/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/opensearch/source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/oracle/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/oracle/source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/postgresql/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/postgresql/source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/powerbi/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/powerbi/source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/recipe_normalizer.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/s3_client.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/s3_compatible_storage/README.md +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/s3_compatible_storage/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/s3_compatible_storage/source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/sandbox/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/sandbox/source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/search_engine_base.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/servicedesk/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/servicedesk/source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/slack/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/slack/source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/snowflake/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/snowflake/source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/sqlite/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/sqlite/source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/tableau/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/tableau/source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/tabular_base.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/tabular_utils.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/wordpress/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/wordpress/source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/youtube/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/sources/youtube/source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/telemetry.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/utils/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/utils/content_extraction.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/utils/dependency_groups.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/utils/embedded_images.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/utils/file_to_images.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/utils/hashing.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/utils/resources.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/utils/transcription.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/utils/uv_sync.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/utils/validation.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/src/utils/video_processing.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/_lakehouse_fakes.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/conftest.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/detectors/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/detectors/broken_links/test_broken_links_detector.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/detectors/conftest.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/detectors/content/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/detectors/custom/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/detectors/custom/conftest.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/detectors/custom/test_gliner2_classification.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/detectors/custom/test_invoice_extraction.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/detectors/custom/test_llm_runner.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/detectors/custom/test_pipeline_integration.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/detectors/custom/test_regex_runner.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/detectors/custom/test_runner_hardening.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/detectors/custom/test_transformer_runners.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/detectors/pii/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/detectors/pii/conftest.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/detectors/pii/sample_invoice.pdf +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/detectors/pii/test_pii_chunking.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/detectors/pii/test_pii_detector.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/detectors/pii/test_pii_detector_extended.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/detectors/pii/test_pii_severity.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/detectors/secrets/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/detectors/secrets/test_secrets_detector.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/detectors/secrets/test_secrets_detector_extended.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/detectors/test_base_detector.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/detectors/test_custom_detector_examples_runtime.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/detectors/test_detector_catalog_commercial.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/detectors/test_detector_pipeline_types.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/detectors/test_detector_schema_examples.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/detectors/test_detector_types.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/detectors/test_phase2_detectors.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/detectors/test_registry.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/detectors/threat/__init__.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/detectors/threat/test_code_security_detector.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/detectors/threat/test_yara_detector.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/integration/test_wordpress_broken_links_detector.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/integration/test_wordpress_links_assets.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/pipeline/test_detector_outcomes.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/pipeline/test_detector_pipeline.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/pipeline/test_worker_pool.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_assets_metadata_catalog.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_azure_blob_storage_source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_base_source_attachment.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_base_source_sampling.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_config.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_confluence_source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_custom_extractor.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_databricks_source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_delta_lake_source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_dependency_groups.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_elasticsearch_source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_email_source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_file_evaluation_runner.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_google_cloud_storage_source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_google_workspace_source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_hashing.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_hive_source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_iceberg_source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_jira_source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_kafka_source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_meilisearch_source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_microsoft_365_source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_mongodb_source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_mssql_source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_mysql_source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_neo4j_source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_notion_source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_opensearch_source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_oracle_source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_outputs.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_postgresql_source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_powerbi_source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_recipe_normalizer.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_sampling_automatic.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_sandbox_source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_servicedesk_source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_slack_source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_snowflake_source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_source_dependency_groups.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_source_strategy_conformance.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_sqlite_source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_tableau_source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_tabular_automatic_sampling.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_tabular_utils.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_uv_sync.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_wordpress_source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_youtube_source.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/test_youtube_source_integration.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/utils/test_content_extraction.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/utils/test_embedded_images.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/utils/test_file_metadata.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/utils/test_file_to_images.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/utils/test_transcription.py +0 -0
- {classifyre_cli-0.4.57 → classifyre_cli-0.4.58}/tests/utils/test_video_processing.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "classifyre-cli"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.58"
|
|
4
4
|
description = "Classifyre CLI — scan and classify unstructured data sources"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -60,7 +60,7 @@ privacy = [
|
|
|
60
60
|
# phonenumbers 9.x introduced lazy region loading via __import__ which fails
|
|
61
61
|
# mid-run in frozen/venv contexts. 8.x eagerly loads all data at import time,
|
|
62
62
|
# avoiding ModuleNotFoundError during Presidio phone number analysis.
|
|
63
|
-
"phonenumbers>=8.13.0,<
|
|
63
|
+
"phonenumbers>=8.13.0,<9.0.0",
|
|
64
64
|
"numpy>=1.26.0,<3.0.0",
|
|
65
65
|
]
|
|
66
66
|
security = [
|
|
@@ -135,6 +135,18 @@ file-processing = [
|
|
|
135
135
|
"openpyxl>=3.1.5",
|
|
136
136
|
"chardet>=7.4.3",
|
|
137
137
|
"pyarrow>=18.0.0",
|
|
138
|
+
# RTF text extraction (tiny, pure Python).
|
|
139
|
+
"striprtf>=0.0.32",
|
|
140
|
+
# Secure XML parsing (entity-expansion / XXE hardened stdlib drop-in).
|
|
141
|
+
"defusedxml>=0.7.1",
|
|
142
|
+
# Image decoding for GIF-frame extraction and HEIC/HEIF conversion before OCR.
|
|
143
|
+
"pillow>=12.2.0",
|
|
144
|
+
"pillow-heif>=1.4.0",
|
|
145
|
+
# Archive member extraction (zip/tar via stdlib). RAR additionally needs an
|
|
146
|
+
# external unrar/unar/bsdtar backend at runtime; rarfile degrades gracefully
|
|
147
|
+
# when none is installed.
|
|
148
|
+
"py7zr>=1.1.3",
|
|
149
|
+
"rarfile>=4.3",
|
|
138
150
|
# Table layer: Iceberg metadata resolution + SQL over Parquet without Spark.
|
|
139
151
|
"pyiceberg>=0.9.0",
|
|
140
152
|
"duckdb>=1.1.0",
|
|
@@ -152,7 +164,8 @@ video = [
|
|
|
152
164
|
]
|
|
153
165
|
ocr = [
|
|
154
166
|
{ include-group = "file-processing" },
|
|
155
|
-
|
|
167
|
+
# >=2.113 for the OpenDocument (odt/ods/odp) and email (eml/msg) backends.
|
|
168
|
+
"docling>=2.113.0",
|
|
156
169
|
"opencv-python-headless>=5.0.0.93,<6.0.0",
|
|
157
170
|
"rapidocr-onnxruntime>=1.4.0",
|
|
158
171
|
# torchvision is a transitive dep of docling-ibm-models, but [tool.uv.sources]
|
|
@@ -20,6 +20,10 @@ class FileEvaluationRunner:
|
|
|
20
20
|
|
|
21
21
|
def __init__(self, detectors_config: list[dict[str, Any]]) -> None:
|
|
22
22
|
self._config = detectors_config
|
|
23
|
+
# Detector construction failures, surfaced in the evaluate-file JSON
|
|
24
|
+
# output so callers can distinguish "misconfigured detector" from
|
|
25
|
+
# "no findings".
|
|
26
|
+
self.detector_errors: list[str] = []
|
|
23
27
|
|
|
24
28
|
def _build_detectors(self) -> list[Any]:
|
|
25
29
|
from ..detectors import get_detector
|
|
@@ -44,6 +48,7 @@ class FileEvaluationRunner:
|
|
|
44
48
|
logger.info(f"Initialized file-evaluation detector: {detector_name}")
|
|
45
49
|
except Exception as e:
|
|
46
50
|
logger.error(f"Failed to initialize detector {detector_type}: {e}")
|
|
51
|
+
self.detector_errors.append(f"{detector_type}: {e}")
|
|
47
52
|
|
|
48
53
|
return detectors
|
|
49
54
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import argparse
|
|
2
|
+
import asyncio
|
|
2
3
|
import json
|
|
3
4
|
import logging
|
|
4
5
|
import os
|
|
@@ -17,6 +18,7 @@ _SURROGATE_RE = re.compile(r"[\ud800-\udfff]")
|
|
|
17
18
|
|
|
18
19
|
_TIMEOUT_PHRASES = ("timed out", "timeout", "connection reset", "errno 110", "connection refused")
|
|
19
20
|
_TIMEOUT_MYSQL_CODES = {2003, 2006, 2013}
|
|
21
|
+
_TEXT_CHUNK_EMIT_ATTEMPTS = 3
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
def _is_timeout_error(exc: BaseException) -> bool:
|
|
@@ -43,6 +45,34 @@ def _sanitize_for_json(value: Any) -> Any:
|
|
|
43
45
|
return value
|
|
44
46
|
|
|
45
47
|
|
|
48
|
+
async def _emit_text_chunks_with_retry(
|
|
49
|
+
sink: Any,
|
|
50
|
+
asset_hash: str,
|
|
51
|
+
artifact: Any,
|
|
52
|
+
*,
|
|
53
|
+
attempts: int = _TEXT_CHUNK_EMIT_ATTEMPTS,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Persist chunk provenance, retrying transient API failures.
|
|
56
|
+
|
|
57
|
+
The caller keeps the already-persisted asset successful, but propagates a
|
|
58
|
+
final failure at run level so missing chunks/embeddings cannot look healthy.
|
|
59
|
+
"""
|
|
60
|
+
for attempt in range(1, attempts + 1):
|
|
61
|
+
try:
|
|
62
|
+
await sink.emit_text_chunks(asset_hash, artifact)
|
|
63
|
+
return
|
|
64
|
+
except Exception:
|
|
65
|
+
if attempt == attempts:
|
|
66
|
+
raise
|
|
67
|
+
logger.warning(
|
|
68
|
+
"Text-chunk emission failed for asset %s (attempt %d/%d); retrying",
|
|
69
|
+
asset_hash,
|
|
70
|
+
attempt,
|
|
71
|
+
attempts,
|
|
72
|
+
)
|
|
73
|
+
await asyncio.sleep(0.25 * (2 ** (attempt - 1)))
|
|
74
|
+
|
|
75
|
+
|
|
46
76
|
def setup_logging() -> None:
|
|
47
77
|
"""Configure the logging facility."""
|
|
48
78
|
logging.basicConfig(
|
|
@@ -250,13 +280,18 @@ async def run_command_async(args: argparse.Namespace, recipe: dict[str, Any]) ->
|
|
|
250
280
|
stub_batch = [_asset_to_payload(asset) for asset in raw_batch]
|
|
251
281
|
for stub in stub_batch:
|
|
252
282
|
stub["findings"] = []
|
|
253
|
-
await sink.emit_batch(stub_batch, skip_findings=True)
|
|
254
|
-
output_batch_count += 1
|
|
255
283
|
|
|
284
|
+
# Register BEFORE ingesting: the API stamps per-run
|
|
285
|
+
# change_type onto runner_assets rows during ingest, so
|
|
286
|
+
# the row must exist or the CREATED stamp is lost and
|
|
287
|
+
# first-run counters report every asset as unchanged.
|
|
256
288
|
hashes = [s["hash"] for s in stub_batch if s.get("hash")]
|
|
257
289
|
if hasattr(sink, "register_discovered_assets") and hashes:
|
|
258
290
|
await sink.register_discovered_assets(hashes)
|
|
259
291
|
|
|
292
|
+
await sink.emit_batch(stub_batch, skip_findings=True)
|
|
293
|
+
output_batch_count += 1
|
|
294
|
+
|
|
260
295
|
all_stubs.extend(raw_batch)
|
|
261
296
|
logger.info(
|
|
262
297
|
"Discovered %s assets (total: %s)",
|
|
@@ -269,13 +304,11 @@ async def run_command_async(args: argparse.Namespace, recipe: dict[str, Any]) ->
|
|
|
269
304
|
|
|
270
305
|
# --- Phase 2: Processing ---
|
|
271
306
|
if all_stubs:
|
|
272
|
-
import asyncio as _asyncio
|
|
273
|
-
|
|
274
307
|
processed_count = 0
|
|
275
308
|
_pw = worker_pool.max_workers if worker_pool else 4
|
|
276
309
|
max_concurrent = args.max_concurrent_assets or (_pw * 2)
|
|
277
310
|
max_concurrent = max(1, max_concurrent)
|
|
278
|
-
_asset_semaphore =
|
|
311
|
+
_asset_semaphore = asyncio.Semaphore(max_concurrent)
|
|
279
312
|
logger.info(
|
|
280
313
|
"Phase 2 starting: %d assets, pool_workers=%s, max_concurrent_assets=%d",
|
|
281
314
|
len(all_stubs),
|
|
@@ -283,6 +316,7 @@ async def run_command_async(args: argparse.Namespace, recipe: dict[str, Any]) ->
|
|
|
283
316
|
max_concurrent,
|
|
284
317
|
)
|
|
285
318
|
error_count = 0
|
|
319
|
+
chunk_errors: list[str] = []
|
|
286
320
|
|
|
287
321
|
async def _process_one(asset: Any) -> None:
|
|
288
322
|
async with _asset_semaphore:
|
|
@@ -326,7 +360,24 @@ async def run_command_async(args: argparse.Namespace, recipe: dict[str, Any]) ->
|
|
|
326
360
|
if hasattr(sink, "emit_text_chunks"):
|
|
327
361
|
artifact = pipeline.take_text_artifact(asset_hash)
|
|
328
362
|
if artifact is not None:
|
|
329
|
-
|
|
363
|
+
try:
|
|
364
|
+
await _emit_text_chunks_with_retry(
|
|
365
|
+
sink, asset_hash, artifact
|
|
366
|
+
)
|
|
367
|
+
except Exception as chunk_exc:
|
|
368
|
+
# Findings and the asset remain persisted,
|
|
369
|
+
# preserving B-2's graceful degradation.
|
|
370
|
+
# The run fails after phase 2 so operators
|
|
371
|
+
# cannot mistake missing embeddings for a
|
|
372
|
+
# healthy completed scan.
|
|
373
|
+
detail = f"{asset_hash}: {chunk_exc}"
|
|
374
|
+
chunk_errors.append(detail)
|
|
375
|
+
logger.error(
|
|
376
|
+
"Text-chunk emission exhausted retries"
|
|
377
|
+
" for asset %s: %s",
|
|
378
|
+
asset_hash,
|
|
379
|
+
chunk_exc,
|
|
380
|
+
)
|
|
330
381
|
|
|
331
382
|
if hasattr(sink, "update_asset_status"):
|
|
332
383
|
f_total, f_by_sev, f_by_det = _compute_findings_counts(
|
|
@@ -354,13 +405,22 @@ async def run_command_async(args: argparse.Namespace, recipe: dict[str, Any]) ->
|
|
|
354
405
|
except Exception:
|
|
355
406
|
pass
|
|
356
407
|
|
|
357
|
-
tasks = [
|
|
358
|
-
await
|
|
408
|
+
tasks = [asyncio.create_task(_process_one(a)) for a in all_stubs]
|
|
409
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
359
410
|
logger.info(
|
|
360
411
|
"Phase 2 complete: %d processed, %d errors",
|
|
361
412
|
processed_count,
|
|
362
413
|
error_count,
|
|
363
414
|
)
|
|
415
|
+
if chunk_errors:
|
|
416
|
+
preview = "; ".join(chunk_errors[:3])
|
|
417
|
+
remaining = len(chunk_errors) - 3
|
|
418
|
+
if remaining > 0:
|
|
419
|
+
preview += f"; and {remaining} more"
|
|
420
|
+
raise RuntimeError(
|
|
421
|
+
f"Text-chunk emission failed for {len(chunk_errors)}"
|
|
422
|
+
f" asset(s) after retries: {preview}"
|
|
423
|
+
)
|
|
364
424
|
|
|
365
425
|
# Persist the advanced AUTOMATIC sampling cursor (no-op for
|
|
366
426
|
# other strategies, which return None). Only on the normal
|
|
@@ -506,6 +566,8 @@ def run_evaluate_file_command(args: argparse.Namespace) -> None:
|
|
|
506
566
|
}
|
|
507
567
|
if parsed.parse_error:
|
|
508
568
|
output["parse_error"] = parsed.parse_error
|
|
569
|
+
if runner.detector_errors:
|
|
570
|
+
output["detector_errors"] = runner.detector_errors
|
|
509
571
|
print(json.dumps(_sanitize_for_json(output), ensure_ascii=False))
|
|
510
572
|
except Exception as e:
|
|
511
573
|
logger.error("File evaluation failed: %s", e, exc_info=True)
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import hashlib
|
|
4
5
|
import itertools
|
|
5
6
|
import logging
|
|
6
7
|
import random
|
|
8
|
+
import tempfile
|
|
7
9
|
from abc import ABC, abstractmethod
|
|
8
10
|
from collections.abc import AsyncGenerator, Iterator
|
|
9
11
|
from dataclasses import dataclass
|
|
10
12
|
from datetime import UTC, datetime
|
|
11
|
-
from pathlib import PurePosixPath
|
|
13
|
+
from pathlib import Path, PurePosixPath
|
|
12
14
|
from typing import Any
|
|
13
15
|
|
|
14
16
|
from ...models.generated_input import SamplingStrategy
|
|
@@ -20,6 +22,7 @@ from ...models.generated_single_asset_scan_results import (
|
|
|
20
22
|
Location,
|
|
21
23
|
SingleAssetScanResults,
|
|
22
24
|
)
|
|
25
|
+
from ...utils.archive_extraction import ArchiveMember, is_archive_mime, iter_archive_members
|
|
23
26
|
from ...utils.embedded_images import EmbeddedImage, has_embedded_images, iter_embedded_images
|
|
24
27
|
from ...utils.file_metadata import extract_file_metadata
|
|
25
28
|
from ...utils.file_parser import infer_mime_type_from_file_name, resolve_mime_type
|
|
@@ -65,6 +68,8 @@ _FILE_EXTENSION_HINTS: dict[str, OutputAssetType] = {
|
|
|
65
68
|
".wav": OutputAssetType.AUDIO,
|
|
66
69
|
".aac": OutputAssetType.AUDIO,
|
|
67
70
|
".ogg": OutputAssetType.AUDIO,
|
|
71
|
+
".heic": OutputAssetType.IMAGE,
|
|
72
|
+
".heif": OutputAssetType.IMAGE,
|
|
68
73
|
".pdf": OutputAssetType.BINARY,
|
|
69
74
|
".doc": OutputAssetType.BINARY,
|
|
70
75
|
".docx": OutputAssetType.BINARY,
|
|
@@ -72,11 +77,18 @@ _FILE_EXTENSION_HINTS: dict[str, OutputAssetType] = {
|
|
|
72
77
|
".xlsx": OutputAssetType.TABLE,
|
|
73
78
|
".ppt": OutputAssetType.BINARY,
|
|
74
79
|
".pptx": OutputAssetType.BINARY,
|
|
80
|
+
".odt": OutputAssetType.BINARY,
|
|
81
|
+
".ods": OutputAssetType.TABLE,
|
|
82
|
+
".odp": OutputAssetType.BINARY,
|
|
83
|
+
".rtf": OutputAssetType.BINARY,
|
|
84
|
+
".eml": OutputAssetType.BINARY,
|
|
85
|
+
".msg": OutputAssetType.BINARY,
|
|
75
86
|
".zip": OutputAssetType.BINARY,
|
|
76
87
|
".rar": OutputAssetType.BINARY,
|
|
77
88
|
".7z": OutputAssetType.BINARY,
|
|
78
89
|
".tar": OutputAssetType.BINARY,
|
|
79
90
|
".gz": OutputAssetType.BINARY,
|
|
91
|
+
".tgz": OutputAssetType.BINARY,
|
|
80
92
|
".parquet": OutputAssetType.TABLE,
|
|
81
93
|
".json": OutputAssetType.TXT,
|
|
82
94
|
".xml": OutputAssetType.TXT,
|
|
@@ -134,6 +146,11 @@ class ObjectStorageSourceBase(BaseSource, ABC):
|
|
|
134
146
|
self._file_processing_deps_checked = False
|
|
135
147
|
# Keyed by both asset_hash and external_url for O(1) lookup from either.
|
|
136
148
|
self._bytes_cache: dict[str, bytes] = {}
|
|
149
|
+
# Archive members can expand to 100 MB per container. Keep their
|
|
150
|
+
# discovery-to-processing lifetime on disk so many archives cannot
|
|
151
|
+
# multiply that budget into unbounded resident memory.
|
|
152
|
+
self._archive_bytes_cache: dict[str, Path] = {}
|
|
153
|
+
self._archive_cache_dir: tempfile.TemporaryDirectory[str] | None = None
|
|
137
154
|
self._mime_cache: dict[str, str] = {}
|
|
138
155
|
# asset_ids for which fetch_content_pages ran the full bytes path
|
|
139
156
|
# (even if it produced no text, e.g. all-silence audio). Checked by
|
|
@@ -339,6 +356,13 @@ class ObjectStorageSourceBase(BaseSource, ABC):
|
|
|
339
356
|
}
|
|
340
357
|
return mapping.get(asset_type, "file")
|
|
341
358
|
|
|
359
|
+
@classmethod
|
|
360
|
+
def _asset_kind_for_mime(cls, mime_type: str | None, asset_type: OutputAssetType) -> str:
|
|
361
|
+
"""Catalog kind for an asset: archives get their own kind for display."""
|
|
362
|
+
if mime_type and is_archive_mime(mime_type):
|
|
363
|
+
return "archive"
|
|
364
|
+
return cls._asset_kind_for_asset_type(asset_type)
|
|
365
|
+
|
|
342
366
|
def _ensure_file_processing_dependencies(self) -> None:
|
|
343
367
|
if self._file_processing_deps_checked:
|
|
344
368
|
return
|
|
@@ -399,10 +423,9 @@ class ObjectStorageSourceBase(BaseSource, ABC):
|
|
|
399
423
|
|
|
400
424
|
# Images and audio/video are extractable automatically. Opaque binaries
|
|
401
425
|
# remain available to binary detectors but have no text page stream.
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
)
|
|
426
|
+
# Archives keep their bytes so member files can be expanded into child
|
|
427
|
+
# assets (the container itself still yields no text pages).
|
|
428
|
+
is_non_extractable = normalized_mime == "application/octet-stream"
|
|
406
429
|
|
|
407
430
|
return ContentSnapshot(
|
|
408
431
|
mime_type=mime_type,
|
|
@@ -479,7 +502,9 @@ class ObjectStorageSourceBase(BaseSource, ABC):
|
|
|
479
502
|
created_at=ref.last_modified,
|
|
480
503
|
updated_at=ref.last_modified,
|
|
481
504
|
runner_id=self.runner_id,
|
|
482
|
-
**self.metadata_fields(
|
|
505
|
+
**self.metadata_fields(
|
|
506
|
+
self._asset_kind_for_mime(snapshot.mime_type, asset_type), asset_metadata
|
|
507
|
+
),
|
|
483
508
|
)
|
|
484
509
|
self._hash_to_uri[asset_hash] = external_url
|
|
485
510
|
self._object_ref_by_hash[asset_hash] = ref
|
|
@@ -497,6 +522,18 @@ class ObjectStorageSourceBase(BaseSource, ABC):
|
|
|
497
522
|
ref=ref,
|
|
498
523
|
)
|
|
499
524
|
|
|
525
|
+
# Archive containers (zip/tar/gz/7z/rar) expand into one child asset per
|
|
526
|
+
# member file so each member gets its own MIME resolution, text
|
|
527
|
+
# extraction, and detector pass. Nested archives are kept as opaque
|
|
528
|
+
# child assets (single-level expansion) to bound memory and fan-out.
|
|
529
|
+
if snapshot.raw_bytes is not None and is_archive_mime(snapshot.mime_type):
|
|
530
|
+
self._queue_child_archive_assets(
|
|
531
|
+
parent=asset,
|
|
532
|
+
file_bytes=snapshot.raw_bytes,
|
|
533
|
+
mime_type=snapshot.mime_type,
|
|
534
|
+
ref=ref,
|
|
535
|
+
)
|
|
536
|
+
|
|
500
537
|
return asset
|
|
501
538
|
|
|
502
539
|
def _queue_child_image_assets(
|
|
@@ -555,6 +592,84 @@ class ObjectStorageSourceBase(BaseSource, ABC):
|
|
|
555
592
|
**self.metadata_fields("image", metadata),
|
|
556
593
|
)
|
|
557
594
|
|
|
595
|
+
def _queue_child_archive_assets(
|
|
596
|
+
self,
|
|
597
|
+
*,
|
|
598
|
+
parent: SingleAssetScanResults,
|
|
599
|
+
file_bytes: bytes,
|
|
600
|
+
mime_type: str,
|
|
601
|
+
ref: ObjectRef,
|
|
602
|
+
) -> None:
|
|
603
|
+
"""Expand archive members into child assets linked from the parent
|
|
604
|
+
(appended to ``parent.links``, never removing existing links)."""
|
|
605
|
+
try:
|
|
606
|
+
for member in iter_archive_members(
|
|
607
|
+
file_bytes,
|
|
608
|
+
mime_type,
|
|
609
|
+
file_name=self._object_file_name(ref),
|
|
610
|
+
):
|
|
611
|
+
child = self._build_child_archive_member_asset(parent, member, ref)
|
|
612
|
+
self._pending_child_assets.append(child)
|
|
613
|
+
if child.hash not in parent.links:
|
|
614
|
+
parent.links.append(child.hash)
|
|
615
|
+
except Exception as exc:
|
|
616
|
+
logger.warning("Failed to expand archive members from %s: %s", parent.external_url, exc)
|
|
617
|
+
|
|
618
|
+
def _build_child_archive_member_asset(
|
|
619
|
+
self,
|
|
620
|
+
parent: SingleAssetScanResults,
|
|
621
|
+
member: ArchiveMember,
|
|
622
|
+
ref: ObjectRef,
|
|
623
|
+
) -> SingleAssetScanResults:
|
|
624
|
+
child_url = f"{parent.external_url}#{member.location}"
|
|
625
|
+
child_hash = self.generate_hash_id(child_url)
|
|
626
|
+
member_type = self._asset_type_from_mime_or_key(member.mime_type, member.location)
|
|
627
|
+
metadata: dict[str, Any] = {
|
|
628
|
+
"provider": self.provider_label,
|
|
629
|
+
"object_key": f"{ref.key}#{member.location}",
|
|
630
|
+
"source_hash": parent.hash,
|
|
631
|
+
"location": member.location,
|
|
632
|
+
"mime_type": member.mime_type,
|
|
633
|
+
"size_bytes": len(member.member_bytes),
|
|
634
|
+
}
|
|
635
|
+
file_meta = extract_file_metadata(
|
|
636
|
+
member.member_bytes,
|
|
637
|
+
member.mime_type,
|
|
638
|
+
file_name=member.location,
|
|
639
|
+
)
|
|
640
|
+
metadata.update({k: v for k, v in file_meta.items() if v is not None})
|
|
641
|
+
# Spool member bytes to a per-scan temporary directory. Phase 1 must
|
|
642
|
+
# discover every asset before phase 2 starts (B-4), so retaining these
|
|
643
|
+
# bytes in memory would make the per-archive extraction cap unbounded
|
|
644
|
+
# across the whole source.
|
|
645
|
+
if self._archive_cache_dir is None:
|
|
646
|
+
self._archive_cache_dir = tempfile.TemporaryDirectory(
|
|
647
|
+
prefix="classifyre-archive-members-"
|
|
648
|
+
)
|
|
649
|
+
cache_name = hashlib.sha256(child_hash.encode()).hexdigest()
|
|
650
|
+
member_path = Path(self._archive_cache_dir.name) / cache_name
|
|
651
|
+
member_path.write_bytes(member.member_bytes)
|
|
652
|
+
self._archive_bytes_cache[child_hash] = member_path
|
|
653
|
+
self._archive_bytes_cache[child_url] = member_path
|
|
654
|
+
self._mime_cache[child_hash] = member.mime_type
|
|
655
|
+
self._mime_cache[child_url] = member.mime_type
|
|
656
|
+
self._hash_to_uri[child_hash] = child_url
|
|
657
|
+
return SingleAssetScanResults(
|
|
658
|
+
hash=child_hash,
|
|
659
|
+
checksum=self.calculate_checksum(metadata),
|
|
660
|
+
name=f"{parent.name}#{member.location}",
|
|
661
|
+
external_url=child_url,
|
|
662
|
+
links=[],
|
|
663
|
+
asset_type=member_type,
|
|
664
|
+
source_id=self.source_id,
|
|
665
|
+
created_at=ref.last_modified,
|
|
666
|
+
updated_at=ref.last_modified,
|
|
667
|
+
runner_id=self.runner_id,
|
|
668
|
+
**self.metadata_fields(
|
|
669
|
+
self._asset_kind_for_mime(member.mime_type, member_type), metadata
|
|
670
|
+
),
|
|
671
|
+
)
|
|
672
|
+
|
|
558
673
|
def test_connection(self) -> dict[str, Any]:
|
|
559
674
|
result = {
|
|
560
675
|
"timestamp": datetime.now(UTC).isoformat(),
|
|
@@ -581,6 +696,7 @@ class ObjectStorageSourceBase(BaseSource, ABC):
|
|
|
581
696
|
self._hash_to_uri = {}
|
|
582
697
|
self._object_ref_by_hash = {}
|
|
583
698
|
self._bytes_cache = {}
|
|
699
|
+
self._reset_archive_cache()
|
|
584
700
|
self._mime_cache = {}
|
|
585
701
|
self._content_pages_processed = set()
|
|
586
702
|
self._pending_child_assets = []
|
|
@@ -617,6 +733,10 @@ class ObjectStorageSourceBase(BaseSource, ABC):
|
|
|
617
733
|
|
|
618
734
|
async def fetch_content_bytes(self, asset_id: str) -> tuple[bytes, str] | None:
|
|
619
735
|
raw_bytes = self._bytes_cache.get(asset_id)
|
|
736
|
+
if raw_bytes is None:
|
|
737
|
+
member_path = self._archive_bytes_cache.get(asset_id)
|
|
738
|
+
if member_path is not None:
|
|
739
|
+
raw_bytes = member_path.read_bytes()
|
|
620
740
|
mime = self._mime_cache.get(asset_id, "")
|
|
621
741
|
if raw_bytes is not None and mime:
|
|
622
742
|
return raw_bytes, mime
|
|
@@ -660,6 +780,10 @@ class ObjectStorageSourceBase(BaseSource, ABC):
|
|
|
660
780
|
|
|
661
781
|
async def fetch_content_pages(self, asset_id: str) -> AsyncGenerator[tuple[str, str], None]:
|
|
662
782
|
raw_bytes = self._bytes_cache.get(asset_id)
|
|
783
|
+
if raw_bytes is None:
|
|
784
|
+
member_path = self._archive_bytes_cache.get(asset_id)
|
|
785
|
+
if member_path is not None:
|
|
786
|
+
raw_bytes = member_path.read_bytes()
|
|
663
787
|
mime = self._mime_cache.get(asset_id, "")
|
|
664
788
|
|
|
665
789
|
logger.info(
|
|
@@ -807,20 +931,31 @@ class ObjectStorageSourceBase(BaseSource, ABC):
|
|
|
807
931
|
|
|
808
932
|
def evict_asset_cache(self, asset_hash: str) -> None:
|
|
809
933
|
external_url = self._hash_to_uri.get(asset_hash)
|
|
934
|
+
archive_path = self._archive_bytes_cache.pop(asset_hash, None)
|
|
810
935
|
self._content_cache.pop(asset_hash, None)
|
|
811
936
|
self._bytes_cache.pop(asset_hash, None)
|
|
812
937
|
self._mime_cache.pop(asset_hash, None)
|
|
813
938
|
self._object_ref_by_hash.pop(asset_hash, None)
|
|
814
939
|
if external_url:
|
|
940
|
+
archive_path = self._archive_bytes_cache.pop(external_url, archive_path)
|
|
815
941
|
self._content_cache.pop(external_url, None)
|
|
816
942
|
self._bytes_cache.pop(external_url, None)
|
|
817
943
|
self._mime_cache.pop(external_url, None)
|
|
944
|
+
if archive_path is not None:
|
|
945
|
+
archive_path.unlink(missing_ok=True)
|
|
946
|
+
|
|
947
|
+
def _reset_archive_cache(self) -> None:
|
|
948
|
+
self._archive_bytes_cache = {}
|
|
949
|
+
if self._archive_cache_dir is not None:
|
|
950
|
+
self._archive_cache_dir.cleanup()
|
|
951
|
+
self._archive_cache_dir = None
|
|
818
952
|
|
|
819
953
|
def abort(self) -> None:
|
|
820
954
|
logger.info("Aborting object storage extraction...")
|
|
821
955
|
super().abort()
|
|
822
956
|
|
|
823
957
|
def cleanup(self) -> None:
|
|
958
|
+
self._reset_archive_cache()
|
|
824
959
|
client = self._cached_client
|
|
825
960
|
if client is None:
|
|
826
961
|
return
|