classifyre-cli 0.4.80__tar.gz → 0.4.82__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.82/.turbo/turbo-build.log +3 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/PKG-INFO +1 -1
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/package.json +1 -1
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/pyproject.toml +1 -1
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/databricks/source.py +3 -1
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/kafka/source.py +5 -4
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/mongodb/source.py +5 -1
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/neo4j/source.py +4 -1
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/oracle/source.py +5 -1
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/snowflake/source.py +3 -1
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/tabular_base.py +6 -1
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/utils/embedded_files.py +73 -27
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/utils/file_parser.py +74 -12
- classifyre_cli-0.4.82/tests/test_byte_valued_fields.py +141 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/utils/test_embedded_files.py +101 -1
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/uv.lock +21 -21
- classifyre_cli-0.4.80/.turbo/turbo-build.log +0 -3
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/.gitignore +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/.python-version +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/README.md +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/main.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/scripts/generate_models.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/config.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/base.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/broken_links/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/broken_links/detector.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/config.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/content/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/custom/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/custom/detector.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/custom/extractor.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/custom/runners/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/custom/runners/_base.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/custom/runners/_factory.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/custom/runners/_gliner2.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/custom/runners/_image_classification.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/custom/runners/_llm.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/custom/runners/_object_detection.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/custom/runners/_regex.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/custom/runners/_text_classification.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/custom/trainer.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/dependencies.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/engine_version.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/pii/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/pii/detector.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/secrets/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/secrets/detector.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/threat/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/threat/code_security_detector.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/detectors/threat/yara_detector.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/file_evaluation/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/file_evaluation/runner.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/main.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/models/generated_detectors.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/models/generated_input.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/models/generated_single_asset_scan_results.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/outputs/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/outputs/base.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/outputs/console.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/outputs/factory.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/outputs/file.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/outputs/rest.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/pipeline/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/pipeline/content_provider.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/pipeline/detector_pipeline.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/pipeline/parsed_content_provider.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/pipeline/payload_window.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/pipeline/scan_cache.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/pipeline/text_artifact.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/pipeline/worker_pool.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/asset_metadata.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/atlassian_common.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/azure_blob_storage/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/azure_blob_storage/source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/base.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/confluence/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/confluence/source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/databricks/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/delta_lake/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/delta_lake/source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/dependencies.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/dropbox/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/dropbox/auth.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/dropbox/source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/elasticsearch/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/elasticsearch/source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/email/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/email/source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/google_cloud_storage/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/google_cloud_storage/source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/google_workspace/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/google_workspace/source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/hive/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/hive/source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/hugging_face/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/hugging_face/source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/iceberg/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/iceberg/source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/jira/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/jira/source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/kafka/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/kafka/rest.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/lakehouse_base.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/local_folder/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/local_folder/source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/meilisearch/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/meilisearch/source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/microsoft_365/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/microsoft_365/source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/mongodb/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/mssql/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/mssql/source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/mysql/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/mysql/source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/neo4j/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/notion/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/notion/client.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/notion/source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/object_storage/base.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/opensearch/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/opensearch/source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/oracle/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/postgresql/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/postgresql/source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/powerbi/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/powerbi/source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/recipe_normalizer.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/reddit/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/reddit/source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/s3_client.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/s3_compatible_storage/README.md +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/s3_compatible_storage/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/s3_compatible_storage/source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/sandbox/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/sandbox/source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/search_engine_base.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/servicedesk/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/servicedesk/source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/slack/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/slack/source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/snowflake/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/sqlite/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/sqlite/source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/tableau/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/tableau/source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/tabular_utils.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/wordpress/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/wordpress/source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/youtube/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/sources/youtube/source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/telemetry.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/utils/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/utils/archive_extraction.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/utils/content_extraction.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/utils/dependency_groups.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/utils/external_video.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/utils/file_metadata.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/utils/file_to_images.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/utils/hashing.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/utils/legacy_office.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/utils/resources.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/utils/transcription.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/utils/uv_sync.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/utils/validation.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/src/utils/video_processing.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/_lakehouse_fakes.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/conftest.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/broken_links/test_broken_links_detector.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/conftest.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/content/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/custom/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/custom/conftest.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/custom/test_gliner2_classification.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/custom/test_invoice_extraction.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/custom/test_llm_runner.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/custom/test_pipeline_integration.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/custom/test_regex_runner.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/custom/test_runner_hardening.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/custom/test_transformer_runners.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/pii/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/pii/conftest.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/pii/sample_invoice.pdf +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/pii/test_pii_chunking.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/pii/test_pii_detector.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/pii/test_pii_detector_extended.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/pii/test_pii_regional_entities.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/pii/test_pii_severity.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/secrets/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/secrets/test_secrets_detector.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/secrets/test_secrets_detector_extended.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/test_base_detector.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/test_custom_detector_examples_runtime.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/test_detector_catalog_commercial.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/test_detector_pipeline_types.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/test_detector_schema_examples.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/test_detector_types.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/test_phase2_detectors.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/test_registry.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/threat/__init__.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/threat/test_code_security_detector.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/detectors/threat/test_yara_detector.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/integration/test_wordpress_broken_links_detector.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/integration/test_wordpress_links_assets.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/pipeline/test_detector_outcomes.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/pipeline/test_detector_pipeline.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/pipeline/test_worker_pool.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_assets_metadata_catalog.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_azure_blob_storage_source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_base_source_attachment.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_base_source_sampling.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_config.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_confluence_source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_custom_extractor.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_databricks_source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_delta_lake_source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_dependency_groups.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_dropbox_auth_command.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_dropbox_source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_elasticsearch_source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_email_source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_file_evaluation_runner.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_google_cloud_storage_source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_google_workspace_source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_hashing.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_hive_source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_hugging_face_source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_iceberg_source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_jira_source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_kafka_source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_local_folder_source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_main.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_meilisearch_source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_microsoft_365_source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_mongodb_source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_mssql_source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_mysql_source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_neo4j_source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_notion_source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_object_storage_download_efficiency.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_opensearch_source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_oracle_source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_outputs.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_payload_window.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_payload_window_parquet.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_postgresql_source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_powerbi_source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_recipe_normalizer.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_reddit_source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_s3_compatible_storage_source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_sampling_automatic.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_sandbox_source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_scan_cache.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_servicedesk_source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_slack_source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_snowflake_source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_source_dependency_groups.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_source_strategy_conformance.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_sqlite_source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_tableau_source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_tabular_automatic_sampling.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_tabular_utils.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_uv_sync.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_wordpress_source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_youtube_source.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/test_youtube_source_integration.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/utils/test_archive_extraction.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/utils/test_content_extraction.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/utils/test_file_metadata.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/utils/test_file_parser.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/utils/test_file_to_images.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/utils/test_legacy_office.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/utils/test_transcription.py +0 -0
- {classifyre_cli-0.4.80 → classifyre_cli-0.4.82}/tests/utils/test_video_processing.py +0 -0
|
@@ -32,6 +32,7 @@ from ...models.generated_single_asset_scan_results import (
|
|
|
32
32
|
from ...models.generated_single_asset_scan_results import (
|
|
33
33
|
SingleAssetScanResults,
|
|
34
34
|
)
|
|
35
|
+
from ...utils.file_parser import render_bytes_cell
|
|
35
36
|
from ..dependencies import require_module
|
|
36
37
|
from ..tabular_base import BaseTabularSource
|
|
37
38
|
from ..tabular_utils import TableRef
|
|
@@ -418,7 +419,8 @@ class DatabricksSource(BaseTabularSource):
|
|
|
418
419
|
if value is None:
|
|
419
420
|
return "null"
|
|
420
421
|
if isinstance(value, (bytes, bytearray, memoryview)):
|
|
421
|
-
|
|
422
|
+
# Text stored in a BINARY column must still reach the detectors.
|
|
423
|
+
return render_bytes_cell(bytes(value))
|
|
422
424
|
if isinstance(value, datetime):
|
|
423
425
|
return value.isoformat()
|
|
424
426
|
return str(value)
|
|
@@ -40,6 +40,7 @@ from ...models.generated_single_asset_scan_results import (
|
|
|
40
40
|
from ...models.generated_single_asset_scan_results import (
|
|
41
41
|
SingleAssetScanResults,
|
|
42
42
|
)
|
|
43
|
+
from ...utils.file_parser import render_bytes_cell
|
|
43
44
|
from ...utils.hashing import hash_id
|
|
44
45
|
from ..base import BaseSource
|
|
45
46
|
from ..dependencies import require_module
|
|
@@ -519,10 +520,10 @@ class KafkaSource(BaseSource):
|
|
|
519
520
|
if value is None:
|
|
520
521
|
return "null"
|
|
521
522
|
if isinstance(value, (bytes, bytearray)):
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
523
|
+
# A decode that succeeds is not proof of readability: control bytes
|
|
524
|
+
# are valid UTF-8, so the old UnicodeDecodeError fallback never fired
|
|
525
|
+
# for a binary payload and emitted raw control characters instead.
|
|
526
|
+
return render_bytes_cell(bytes(value))
|
|
526
527
|
return str(value)
|
|
527
528
|
|
|
528
529
|
def _start_offset(self, strategy: SamplingStrategy, low: int, high: int, per: int) -> int:
|
|
@@ -25,6 +25,7 @@ from ...models.generated_single_asset_scan_results import (
|
|
|
25
25
|
Location,
|
|
26
26
|
SingleAssetScanResults,
|
|
27
27
|
)
|
|
28
|
+
from ...utils.file_parser import json_safe_default
|
|
28
29
|
from ...utils.hashing import hash_id, unhash_id
|
|
29
30
|
from ..base import BaseSource
|
|
30
31
|
from ..dependencies import require_module
|
|
@@ -434,7 +435,10 @@ class MongoDBSource(BaseSource):
|
|
|
434
435
|
)
|
|
435
436
|
|
|
436
437
|
def _serialize_document(self, document: dict[str, Any]) -> str:
|
|
437
|
-
|
|
438
|
+
# json_safe_default, not default=str: a BSON Binary subclasses bytes, so
|
|
439
|
+
# str() renders its Python repr and a JSON payload stored in one arrived
|
|
440
|
+
# at the detectors doubly escaped instead of as the document it is.
|
|
441
|
+
return json.dumps(document, ensure_ascii=False, default=json_safe_default, sort_keys=True)
|
|
438
442
|
|
|
439
443
|
def _format_collection_content(
|
|
440
444
|
self,
|
|
@@ -24,6 +24,7 @@ from ...models.generated_single_asset_scan_results import (
|
|
|
24
24
|
Location,
|
|
25
25
|
SingleAssetScanResults,
|
|
26
26
|
)
|
|
27
|
+
from ...utils.file_parser import json_safe_default
|
|
27
28
|
from ...utils.hashing import hash_id, unhash_id
|
|
28
29
|
from ..base import BaseSource
|
|
29
30
|
from ..dependencies import require_module
|
|
@@ -434,7 +435,9 @@ class Neo4jSource(BaseSource):
|
|
|
434
435
|
return nodes
|
|
435
436
|
|
|
436
437
|
def _serialize_node(self, props: dict[str, Any]) -> str:
|
|
437
|
-
|
|
438
|
+
# Byte-array properties must not fall to str(), which renders
|
|
439
|
+
# bytearray(b'...') instead of the text the property holds.
|
|
440
|
+
return json.dumps(props, ensure_ascii=False, default=json_safe_default, sort_keys=True)
|
|
438
441
|
|
|
439
442
|
def _format_label_content(
|
|
440
443
|
self,
|
|
@@ -11,6 +11,7 @@ from ...models.generated_input import (
|
|
|
11
11
|
SamplingConfig,
|
|
12
12
|
SamplingStrategy,
|
|
13
13
|
)
|
|
14
|
+
from ...utils.file_parser import render_bytes_cell
|
|
14
15
|
from ..dependencies import require_module
|
|
15
16
|
from ..tabular_base import BaseTabularSource
|
|
16
17
|
from ..tabular_utils import TableRef
|
|
@@ -244,7 +245,10 @@ class OracleSource(BaseTabularSource):
|
|
|
244
245
|
value = str(value)
|
|
245
246
|
|
|
246
247
|
if isinstance(value, (bytes, bytearray)):
|
|
247
|
-
|
|
248
|
+
# A CLOB read through the LOB branch above lands here as bytes; text in
|
|
249
|
+
# a BLOB is just as common. Both must stay readable — see
|
|
250
|
+
# ``render_bytes_cell``.
|
|
251
|
+
return render_bytes_cell(bytes(value))
|
|
248
252
|
if isinstance(value, datetime):
|
|
249
253
|
return value.isoformat()
|
|
250
254
|
return str(value)
|
|
@@ -21,6 +21,7 @@ from ...models.generated_input import (
|
|
|
21
21
|
SnowflakeRequiredKeyPairAuthenticator,
|
|
22
22
|
SnowflakeRequiredOauthAuthenticatorToken,
|
|
23
23
|
)
|
|
24
|
+
from ...utils.file_parser import render_bytes_cell
|
|
24
25
|
from ..dependencies import require_module
|
|
25
26
|
from ..tabular_base import BaseTabularSource
|
|
26
27
|
from ..tabular_utils import TableRef
|
|
@@ -299,7 +300,8 @@ class SnowflakeSource(BaseTabularSource):
|
|
|
299
300
|
if isinstance(value, memoryview):
|
|
300
301
|
value = value.tobytes()
|
|
301
302
|
if isinstance(value, (bytes, bytearray)):
|
|
302
|
-
|
|
303
|
+
# Text stored in a BINARY column must still reach the detectors.
|
|
304
|
+
return render_bytes_cell(bytes(value))
|
|
303
305
|
if isinstance(value, (datetime, date)):
|
|
304
306
|
return value.isoformat()
|
|
305
307
|
if isinstance(value, Decimal):
|
|
@@ -28,6 +28,7 @@ from ..models.generated_single_asset_scan_results import (
|
|
|
28
28
|
SingleAssetScanResults,
|
|
29
29
|
)
|
|
30
30
|
from ..utils.file_metadata import build_columns
|
|
31
|
+
from ..utils.file_parser import render_bytes_cell
|
|
31
32
|
from ..utils.hashing import hash_id, unhash_id
|
|
32
33
|
from .base import BaseSource
|
|
33
34
|
from .tabular_utils import TableRef, build_tabular_location, format_tabular_sample_content
|
|
@@ -484,7 +485,11 @@ class BaseTabularSource(BaseSource):
|
|
|
484
485
|
if isinstance(value, memoryview):
|
|
485
486
|
value = value.tobytes()
|
|
486
487
|
if isinstance(value, (bytes, bytearray)):
|
|
487
|
-
|
|
488
|
+
# BYTEA/BLOB/VARBINARY columns routinely hold text — a JSON payload, an
|
|
489
|
+
# encoded document, a note written by an ORM that picked the wrong type.
|
|
490
|
+
# Summarizing them unconditionally meant no detector ever read a byte of
|
|
491
|
+
# it. Genuinely binary values still summarize.
|
|
492
|
+
return render_bytes_cell(bytes(value))
|
|
488
493
|
if isinstance(value, datetime):
|
|
489
494
|
return value.isoformat()
|
|
490
495
|
return str(value)
|
|
@@ -34,6 +34,7 @@ from .file_parser import (
|
|
|
34
34
|
_normalize_mime_type,
|
|
35
35
|
_parquet_row_group_start,
|
|
36
36
|
_require_file_processing,
|
|
37
|
+
is_readable_text,
|
|
37
38
|
resolve_mime_type,
|
|
38
39
|
)
|
|
39
40
|
|
|
@@ -75,29 +76,46 @@ class EmbeddedFile:
|
|
|
75
76
|
mime_type: str # resolved from the bytes (and the cell's own path, when it has one)
|
|
76
77
|
|
|
77
78
|
|
|
79
|
+
# What a byte-carrying parquet column holds, and therefore how a row renders it.
|
|
80
|
+
CONTENT_FILE = "file" # a whole file: becomes a child asset, row text gets a placeholder
|
|
81
|
+
CONTENT_TEXT = "text" # text carried as bytes: decoded straight into the row text
|
|
82
|
+
CONTENT_OPAQUE = "opaque" # bytes that are not a file and not text: placeholder only
|
|
83
|
+
|
|
84
|
+
# Markup and data serializations read fine as raw text and belong to their row.
|
|
85
|
+
# Deliberately not here: message/rfc822, which is a real document format the file
|
|
86
|
+
# parser splits into headers and body, so it is worth its own asset.
|
|
87
|
+
_TEXTUAL_MIME_TYPES = frozenset({"application/json", "application/xml", "application/xhtml+xml"})
|
|
88
|
+
|
|
89
|
+
|
|
78
90
|
@dataclass(frozen=True)
|
|
79
91
|
class EmbeddedColumn:
|
|
80
|
-
"""A parquet column whose cells hold
|
|
92
|
+
"""A parquet column whose cells hold byte payloads instead of plain values."""
|
|
81
93
|
|
|
82
94
|
kind: str # "struct" (a HuggingFace feature: struct<bytes, path>) or "binary"
|
|
83
|
-
mime_hint: str # sniffed once from a sample cell
|
|
95
|
+
mime_hint: str # sniffed once from a sample cell
|
|
96
|
+
content: str # CONTENT_FILE | CONTENT_TEXT | CONTENT_OPAQUE
|
|
84
97
|
|
|
85
98
|
|
|
86
99
|
def is_embeddable_file_mime(mime_type: str) -> bool:
|
|
87
100
|
"""Whether sniffed bytes look like a self-contained file worth its own asset.
|
|
88
101
|
|
|
89
|
-
Text-shaped payloads are excluded deliberately
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
102
|
+
Text-shaped payloads are excluded deliberately: a cell holding JSON or plain
|
|
103
|
+
text is a *field of its row*, so it is decoded into the row's own text (see
|
|
104
|
+
``CONTENT_TEXT``) rather than split off into a second asset. Unrecognized bytes
|
|
105
|
+
are excluded too — a column of embedding vectors or digests is binary without
|
|
106
|
+
being a file, and each cell must not become an asset.
|
|
94
107
|
"""
|
|
108
|
+
return classify_embedded_mime(mime_type) == CONTENT_FILE
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def classify_embedded_mime(mime_type: str) -> str:
|
|
112
|
+
"""Classify sniffed bytes as a file, as text, or as opaque binary."""
|
|
95
113
|
normalized = _normalize_mime_type(mime_type)
|
|
96
114
|
if not normalized or normalized == OCTET_STREAM:
|
|
97
|
-
return
|
|
98
|
-
if normalized.startswith("text/"):
|
|
99
|
-
return
|
|
100
|
-
return
|
|
115
|
+
return CONTENT_OPAQUE
|
|
116
|
+
if normalized.startswith("text/") or normalized in _TEXTUAL_MIME_TYPES:
|
|
117
|
+
return CONTENT_TEXT
|
|
118
|
+
return CONTENT_FILE
|
|
101
119
|
|
|
102
120
|
|
|
103
121
|
def has_embedded_files(mime_type: str) -> bool:
|
|
@@ -164,10 +182,20 @@ def embedded_cell_name(cell: object, kind: str) -> str:
|
|
|
164
182
|
return ""
|
|
165
183
|
|
|
166
184
|
|
|
167
|
-
def
|
|
168
|
-
"""Map column name → ``EmbeddedColumn`` for
|
|
185
|
+
def detect_parquet_payload_columns(parquet_file: Any) -> dict[str, EmbeddedColumn]:
|
|
186
|
+
"""Map column name → ``EmbeddedColumn`` for every column whose cells hold bytes.
|
|
169
187
|
|
|
170
|
-
|
|
188
|
+
All of them are returned, classified by what the bytes turned out to be, because
|
|
189
|
+
each class needs different handling and none of them may fall through to
|
|
190
|
+
``str(cell)`` — that renders a Python bytes repr, which is how a column of JSON
|
|
191
|
+
documents reached the detectors as ``b'{"email": ...}'`` with escaped quotes.
|
|
192
|
+
|
|
193
|
+
Classification is per column, sampled once: sniffing every cell of a
|
|
194
|
+
million-row file would cost more than the scan it feeds. A column of uniform
|
|
195
|
+
content — the normal case — is therefore classified correctly, and the
|
|
196
|
+
child-asset path re-checks each cell it emits anyway.
|
|
197
|
+
|
|
198
|
+
Returns ``{}`` when pyarrow is unavailable or no column carries bytes.
|
|
171
199
|
"""
|
|
172
200
|
try:
|
|
173
201
|
pa = _require_file_processing("pyarrow")
|
|
@@ -201,30 +229,41 @@ def detect_parquet_file_columns(parquet_file: Any) -> dict[str, EmbeddedColumn]:
|
|
|
201
229
|
|
|
202
230
|
columns: dict[str, EmbeddedColumn] = {}
|
|
203
231
|
for name, kind in candidates:
|
|
204
|
-
mime =
|
|
232
|
+
mime, sample_bytes = _sample_column_payload(sample, name, kind)
|
|
233
|
+
content = classify_embedded_mime(mime)
|
|
234
|
+
# The MIME sniffer falls back to text/plain for bytes it cannot place, so a
|
|
235
|
+
# column of packed floats or digests arrives labelled as text. Decoding one
|
|
236
|
+
# would trade a useless repr for a row of invisible control characters.
|
|
237
|
+
if (
|
|
238
|
+
content == CONTENT_TEXT
|
|
239
|
+
and sample_bytes is not None
|
|
240
|
+
and not is_readable_text(sample_bytes)
|
|
241
|
+
):
|
|
242
|
+
content = CONTENT_OPAQUE
|
|
205
243
|
# A struct<bytes, …> column is a *declared* file feature (HuggingFace Image,
|
|
206
|
-
# Audio, Video), so
|
|
207
|
-
#
|
|
208
|
-
#
|
|
209
|
-
if kind == "struct"
|
|
210
|
-
|
|
244
|
+
# Audio, Video), so an unreadable sample still means "file" rather than the
|
|
245
|
+
# opaque default — the alternative is a declared feature silently rendering
|
|
246
|
+
# as a placeholder and never being scanned.
|
|
247
|
+
if content == CONTENT_OPAQUE and kind == "struct" and not mime:
|
|
248
|
+
content = CONTENT_FILE
|
|
249
|
+
columns[name] = EmbeddedColumn(kind=kind, mime_hint=mime, content=content)
|
|
211
250
|
return columns
|
|
212
251
|
|
|
213
252
|
|
|
214
|
-
def
|
|
215
|
-
"""MIME of the first non-empty cell in the sample batch
|
|
253
|
+
def _sample_column_payload(batch: Any, column_name: str, kind: str) -> tuple[str, bytes | None]:
|
|
254
|
+
"""MIME and bytes of the first non-empty cell in the sample batch."""
|
|
216
255
|
if batch is None:
|
|
217
|
-
return ""
|
|
256
|
+
return "", None
|
|
218
257
|
try:
|
|
219
258
|
column = batch.column(column_name)
|
|
220
259
|
except Exception:
|
|
221
|
-
return ""
|
|
260
|
+
return "", None
|
|
222
261
|
for index in range(len(column)):
|
|
223
262
|
cell = column[index].as_py()
|
|
224
263
|
raw = extract_embedded_bytes(cell, kind)
|
|
225
264
|
if raw:
|
|
226
|
-
return resolve_mime_type(raw, file_name=embedded_cell_name(cell, kind))
|
|
227
|
-
return ""
|
|
265
|
+
return resolve_mime_type(raw, file_name=embedded_cell_name(cell, kind)), raw
|
|
266
|
+
return "", None
|
|
228
267
|
|
|
229
268
|
|
|
230
269
|
def _iter_parquet_files(
|
|
@@ -249,7 +288,14 @@ def _iter_parquet_files(
|
|
|
249
288
|
logger.warning("Cannot open parquet for embedded-file extraction: %s", exc)
|
|
250
289
|
return
|
|
251
290
|
|
|
252
|
-
columns
|
|
291
|
+
# Only file-bearing columns produce child assets. A text column is decoded into
|
|
292
|
+
# its own row's text by the page iterator, so walking it here would scan the
|
|
293
|
+
# same characters twice under two different asset identities.
|
|
294
|
+
columns = {
|
|
295
|
+
name: column
|
|
296
|
+
for name, column in detect_parquet_payload_columns(parquet_file).items()
|
|
297
|
+
if column.content == CONTENT_FILE
|
|
298
|
+
}
|
|
253
299
|
if not columns:
|
|
254
300
|
return
|
|
255
301
|
|
|
@@ -1064,6 +1064,52 @@ def _decode_bytes(file_bytes: bytes) -> str:
|
|
|
1064
1064
|
return decoded.replace("\x00", "")
|
|
1065
1065
|
|
|
1066
1066
|
|
|
1067
|
+
def is_readable_text(raw: bytes) -> bool:
|
|
1068
|
+
"""Whether bytes decode to something a text detector could actually read.
|
|
1069
|
+
|
|
1070
|
+
A successful UTF-8 decode is not the test: ``b"\\x01\\x02\\x03"`` decodes
|
|
1071
|
+
cleanly and reads as nothing. What matters is whether the result is mostly
|
|
1072
|
+
printable, which is what separates a UTF-8 document stored in a BLOB from a
|
|
1073
|
+
packed float array stored in the same column type.
|
|
1074
|
+
"""
|
|
1075
|
+
sample = raw[:4096]
|
|
1076
|
+
if not sample:
|
|
1077
|
+
return False
|
|
1078
|
+
if b"\x00" in sample:
|
|
1079
|
+
# UTF-16/32 interleaves a NUL with every character and is still text.
|
|
1080
|
+
return _is_null_byte_unicode_text(sample)
|
|
1081
|
+
decoded = sample.decode("utf-8", errors="replace")
|
|
1082
|
+
printable = sum(1 for char in decoded if char.isprintable() or char.isspace())
|
|
1083
|
+
return printable / len(decoded) >= 0.9
|
|
1084
|
+
|
|
1085
|
+
|
|
1086
|
+
def render_bytes_cell(raw: bytes) -> str:
|
|
1087
|
+
"""Render a byte-valued field as detector-visible text.
|
|
1088
|
+
|
|
1089
|
+
Sources hand rows and documents to text detectors as strings, so every
|
|
1090
|
+
byte-valued field has to become one. The two obvious ways are both wrong:
|
|
1091
|
+
``str(value)`` emits a Python repr — ``b'{"email": ...}'``, with the prefix,
|
|
1092
|
+
escaped quotes and ``\\n`` spelled out — and a flat ``<N bytes>`` summary drops
|
|
1093
|
+
the content entirely, so text stored in a BLOB is never scanned by anything.
|
|
1094
|
+
|
|
1095
|
+
This decodes what is readable and summarizes only what isn't.
|
|
1096
|
+
"""
|
|
1097
|
+
if is_readable_text(raw):
|
|
1098
|
+
return _decode_bytes(raw)
|
|
1099
|
+
return f"<{len(raw)} bytes>"
|
|
1100
|
+
|
|
1101
|
+
|
|
1102
|
+
def json_safe_default(value: Any) -> Any:
|
|
1103
|
+
"""``json.dumps(default=...)`` that keeps byte fields readable.
|
|
1104
|
+
|
|
1105
|
+
``default=str`` is the usual choice and it is how a BSON ``Binary`` holding a
|
|
1106
|
+
JSON document reached the detectors as a doubly-escaped repr string.
|
|
1107
|
+
"""
|
|
1108
|
+
if isinstance(value, bytes | bytearray | memoryview):
|
|
1109
|
+
return render_bytes_cell(bytes(value))
|
|
1110
|
+
return str(value)
|
|
1111
|
+
|
|
1112
|
+
|
|
1067
1113
|
def resolve_mime_type(
|
|
1068
1114
|
file_bytes: bytes,
|
|
1069
1115
|
*,
|
|
@@ -1369,12 +1415,20 @@ def _iter_parquet_pages(
|
|
|
1369
1415
|
# (before reading any data) so a bad file can't lock the C++ thread pool.
|
|
1370
1416
|
pf = pq.ParquetFile(io.BytesIO(file_bytes)) # type: ignore[attr-defined]
|
|
1371
1417
|
|
|
1372
|
-
#
|
|
1373
|
-
#
|
|
1374
|
-
#
|
|
1375
|
-
|
|
1418
|
+
# Columns whose cells hold bytes never reach str(): that renders a Python
|
|
1419
|
+
# repr, so a column of JSON documents would arrive at the detectors as
|
|
1420
|
+
# b'{"email": ...}' with escaped quotes. Whole files (HF Image/Audio
|
|
1421
|
+
# structs, file-byte columns) render as a placeholder and are scanned
|
|
1422
|
+
# separately as child assets; text carried as bytes is decoded into the row
|
|
1423
|
+
# it belongs to; anything else is summarized rather than spelled out.
|
|
1424
|
+
from .embedded_files import (
|
|
1425
|
+
CONTENT_FILE,
|
|
1426
|
+
CONTENT_TEXT,
|
|
1427
|
+
detect_parquet_payload_columns,
|
|
1428
|
+
extract_embedded_bytes,
|
|
1429
|
+
)
|
|
1376
1430
|
|
|
1377
|
-
|
|
1431
|
+
payload_columns = detect_parquet_payload_columns(pf)
|
|
1378
1432
|
|
|
1379
1433
|
begin = max(0, start_row)
|
|
1380
1434
|
first_group, drop_in_group = _parquet_row_group_start(pf, begin)
|
|
@@ -1411,12 +1465,15 @@ def _iter_parquet_pages(
|
|
|
1411
1465
|
lines.append(f"row_{abs_row + 1}:")
|
|
1412
1466
|
for col_i, col in enumerate(col_names):
|
|
1413
1467
|
cell = batch.column(col_i)[local_idx].as_py()
|
|
1414
|
-
if col in
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1468
|
+
if col in payload_columns:
|
|
1469
|
+
payload_column = payload_columns[col]
|
|
1470
|
+
raw = extract_embedded_bytes(cell, payload_column.kind)
|
|
1471
|
+
if payload_column.content == CONTENT_TEXT:
|
|
1472
|
+
cell_str = _decode_bytes(raw) if raw else ""
|
|
1473
|
+
elif payload_column.content == CONTENT_FILE:
|
|
1474
|
+
cell_str = _format_embedded_placeholder(raw, payload_column.mime_hint)
|
|
1475
|
+
else:
|
|
1476
|
+
cell_str = _format_embedded_placeholder(raw, OCTET_STREAM)
|
|
1420
1477
|
else:
|
|
1421
1478
|
cell_str = "" if cell is None else str(cell)
|
|
1422
1479
|
first, *rest = cell_str.splitlines() or [""]
|
|
@@ -1470,7 +1527,12 @@ def _format_embedded_placeholder(raw: bytes | None, mime_hint: str = "") -> str:
|
|
|
1470
1527
|
The label names what the column holds, since that is the only trace of it left
|
|
1471
1528
|
in the row text once the child asset carries the content.
|
|
1472
1529
|
"""
|
|
1473
|
-
|
|
1530
|
+
if mime_hint.startswith("image/"):
|
|
1531
|
+
label = "image"
|
|
1532
|
+
elif not mime_hint or mime_hint == OCTET_STREAM:
|
|
1533
|
+
label = "binary"
|
|
1534
|
+
else:
|
|
1535
|
+
label = mime_hint
|
|
1474
1536
|
if not raw:
|
|
1475
1537
|
return f"<{label}>"
|
|
1476
1538
|
size = len(raw)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Byte-valued fields must stay readable, whichever source produced them.
|
|
2
|
+
|
|
3
|
+
Every source hands rows and documents to text detectors as strings, so each one
|
|
4
|
+
has to turn byte-valued fields into text. Two ways of doing that are wrong and
|
|
5
|
+
both were in the tree:
|
|
6
|
+
|
|
7
|
+
* ``str(value)`` emits a Python repr — ``b'{"email": ...}'`` — so quotes arrive
|
|
8
|
+
escaped and newlines arrive spelled out as ``\\n``.
|
|
9
|
+
* a flat ``<N bytes>`` summary drops the content, so text stored in a BLOB, a
|
|
10
|
+
BSON ``Binary`` or a byte-array property was never read by any detector.
|
|
11
|
+
|
|
12
|
+
These are family-wide guards: the rendering lives in one helper, and a regression
|
|
13
|
+
in it silently blinds every source at once.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
|
|
20
|
+
import pytest
|
|
21
|
+
|
|
22
|
+
from src.utils.file_parser import is_readable_text, json_safe_default, render_bytes_cell
|
|
23
|
+
|
|
24
|
+
TEXT_DOC = b"Contact Grace at grace@example.com about invoice 4471."
|
|
25
|
+
JSON_DOC = b'{"name": "Ada Lovelace", "email": "ada@example.com"}'
|
|
26
|
+
MULTILINE = b"Dear Grace,\n\nInvoice 4471 is overdue.\nContact ada@example.com\n"
|
|
27
|
+
JUNK = b"\x01\x02\x03\x04\x05\x06"
|
|
28
|
+
PNG_HEADER = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ── The shared helper ────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_is_readable_text_separates_documents_from_blobs() -> None:
|
|
35
|
+
assert is_readable_text(TEXT_DOC)
|
|
36
|
+
assert is_readable_text(JSON_DOC)
|
|
37
|
+
assert is_readable_text(MULTILINE)
|
|
38
|
+
# Control bytes decode as valid UTF-8, so a successful decode proves nothing.
|
|
39
|
+
assert not is_readable_text(JUNK)
|
|
40
|
+
assert not is_readable_text(PNG_HEADER)
|
|
41
|
+
assert not is_readable_text(b"")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_is_readable_text_accepts_utf16() -> None:
|
|
45
|
+
"""UTF-16 interleaves a NUL with every character and is still text."""
|
|
46
|
+
assert is_readable_text(TEXT_DOC.decode().encode("utf-16"))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_render_bytes_cell_decodes_text_and_summarizes_binary() -> None:
|
|
50
|
+
assert render_bytes_cell(TEXT_DOC) == TEXT_DOC.decode()
|
|
51
|
+
assert render_bytes_cell(JSON_DOC) == JSON_DOC.decode()
|
|
52
|
+
assert render_bytes_cell(JUNK) == "<6 bytes>"
|
|
53
|
+
assert render_bytes_cell(b"") == "<0 bytes>"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_render_bytes_cell_never_emits_a_python_repr() -> None:
|
|
57
|
+
rendered = render_bytes_cell(MULTILINE)
|
|
58
|
+
assert not rendered.startswith("b'")
|
|
59
|
+
assert "\\n" not in rendered # newlines stayed newlines
|
|
60
|
+
assert rendered.count("\n") == MULTILINE.decode().count("\n")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_json_safe_default_keeps_byte_fields_readable() -> None:
|
|
64
|
+
encoded = json.dumps({"payload": JSON_DOC}, default=json_safe_default)
|
|
65
|
+
assert json.loads(encoded)["payload"] == JSON_DOC.decode()
|
|
66
|
+
assert json.loads(json.dumps({"v": JUNK}, default=json_safe_default))["v"] == "<6 bytes>"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ── Per-source wiring ────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_sql_family_serializes_byte_cells_as_text() -> None:
|
|
73
|
+
"""Covers postgresql, mysql, mssql, sqlite and hive through the shared base."""
|
|
74
|
+
from src.sources.tabular_base import BaseTabularSource
|
|
75
|
+
|
|
76
|
+
serialize = BaseTabularSource._serialize_cell
|
|
77
|
+
assert serialize(None, JSON_DOC) == JSON_DOC.decode()
|
|
78
|
+
assert serialize(None, bytearray(TEXT_DOC)) == TEXT_DOC.decode()
|
|
79
|
+
# psycopg hands BYTEA back as a memoryview.
|
|
80
|
+
assert serialize(None, memoryview(TEXT_DOC)) == TEXT_DOC.decode()
|
|
81
|
+
assert serialize(None, JUNK) == "<6 bytes>"
|
|
82
|
+
assert serialize(None, None) == "null"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@pytest.mark.parametrize(
|
|
86
|
+
("module_path", "class_name"),
|
|
87
|
+
[
|
|
88
|
+
("src.sources.oracle.source", "OracleSource"),
|
|
89
|
+
("src.sources.databricks.source", "DatabricksSource"),
|
|
90
|
+
("src.sources.snowflake.source", "SnowflakeSource"),
|
|
91
|
+
],
|
|
92
|
+
)
|
|
93
|
+
def test_dialect_overrides_serialize_byte_cells_as_text(module_path: str, class_name: str) -> None:
|
|
94
|
+
"""These three override _serialize_cell, so the base fix does not reach them."""
|
|
95
|
+
module = pytest.importorskip(module_path)
|
|
96
|
+
source_class = getattr(module, class_name)
|
|
97
|
+
|
|
98
|
+
assert source_class._serialize_cell(None, TEXT_DOC) == TEXT_DOC.decode()
|
|
99
|
+
assert source_class._serialize_cell(None, JUNK) == "<6 bytes>"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_mongodb_binary_field_is_not_a_repr() -> None:
|
|
103
|
+
"""A BSON Binary subclasses bytes, so default=str rendered its repr.
|
|
104
|
+
|
|
105
|
+
Asserted on plain bytes so the guard runs without pymongo installed — that is
|
|
106
|
+
the same branch ``json_safe_default`` takes for a ``Binary`` — and again on a
|
|
107
|
+
real ``Binary`` wherever the optional driver is available.
|
|
108
|
+
"""
|
|
109
|
+
from src.sources.mongodb.source import MongoDBSource
|
|
110
|
+
|
|
111
|
+
payloads = [JSON_DOC]
|
|
112
|
+
try:
|
|
113
|
+
from bson import Binary
|
|
114
|
+
|
|
115
|
+
payloads.append(Binary(JSON_DOC))
|
|
116
|
+
except ImportError:
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
for payload in payloads:
|
|
120
|
+
document = json.loads(MongoDBSource._serialize_document(None, {"payload": payload}))
|
|
121
|
+
assert document["payload"] == JSON_DOC.decode()
|
|
122
|
+
assert "b'" not in document["payload"]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_neo4j_byte_array_property_is_not_a_repr() -> None:
|
|
126
|
+
from src.sources.neo4j.source import Neo4jSource
|
|
127
|
+
|
|
128
|
+
node = json.loads(Neo4jSource._serialize_node(None, {"blob": bytearray(TEXT_DOC)}))
|
|
129
|
+
|
|
130
|
+
assert node["blob"] == TEXT_DOC.decode()
|
|
131
|
+
assert "bytearray(" not in node["blob"]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_kafka_summarizes_binary_payloads_instead_of_control_characters() -> None:
|
|
135
|
+
"""Regression: the old UnicodeDecodeError fallback never fired for control
|
|
136
|
+
bytes, which are valid UTF-8, so binary messages emitted raw control chars."""
|
|
137
|
+
from src.sources.kafka.source import KafkaSource
|
|
138
|
+
|
|
139
|
+
assert KafkaSource._decode(TEXT_DOC) == TEXT_DOC.decode()
|
|
140
|
+
assert KafkaSource._decode(JUNK) == "<6 bytes>"
|
|
141
|
+
assert KafkaSource._decode(None) == "null"
|