classifyre-cli 0.4.85__tar.gz → 0.4.86__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.85 → classifyre_cli-0.4.86}/.turbo/turbo-build.log +1 -1
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/PKG-INFO +1 -1
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/package.json +1 -1
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/pyproject.toml +1 -1
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/custom/runners/_gliner2.py +11 -1
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/custom/runners/_text_classification.py +68 -1
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/pii/detector.py +75 -9
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/models/generated_detectors.py +4 -4
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/custom/test_transformer_runners.py +50 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/pii/test_pii_chunking.py +78 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/uv.lock +2 -2
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/.gitignore +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/.python-version +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/README.md +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/main.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/scripts/generate_models.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/config.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/base.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/broken_links/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/broken_links/detector.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/config.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/content/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/custom/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/custom/detector.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/custom/extractor.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/custom/runners/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/custom/runners/_base.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/custom/runners/_factory.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/custom/runners/_image_classification.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/custom/runners/_llm.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/custom/runners/_object_detection.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/custom/runners/_regex.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/custom/trainer.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/dependencies.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/engine_version.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/pii/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/secrets/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/secrets/detector.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/threat/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/threat/code_security_detector.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/threat/yara_detector.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/file_evaluation/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/file_evaluation/runner.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/main.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/models/generated_input.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/models/generated_single_asset_scan_results.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/outputs/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/outputs/base.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/outputs/console.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/outputs/factory.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/outputs/file.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/outputs/rest.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/pipeline/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/pipeline/content_provider.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/pipeline/detector_pipeline.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/pipeline/parsed_content_provider.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/pipeline/payload_window.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/pipeline/scan_cache.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/pipeline/text_artifact.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/pipeline/worker_pool.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/asset_metadata.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/atlassian_common.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/azure_blob_storage/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/azure_blob_storage/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/base.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/confluence/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/confluence/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/databricks/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/databricks/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/delta_lake/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/delta_lake/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/dependencies.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/dropbox/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/dropbox/auth.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/dropbox/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/elasticsearch/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/elasticsearch/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/email/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/email/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/google_cloud_storage/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/google_cloud_storage/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/google_workspace/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/google_workspace/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/hive/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/hive/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/hugging_face/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/hugging_face/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/iceberg/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/iceberg/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/jira/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/jira/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/kafka/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/kafka/rest.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/kafka/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/lakehouse_base.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/local_folder/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/local_folder/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/meilisearch/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/meilisearch/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/microsoft_365/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/microsoft_365/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/mongodb/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/mongodb/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/mssql/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/mssql/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/mysql/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/mysql/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/neo4j/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/neo4j/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/notion/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/notion/client.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/notion/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/object_storage/base.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/opensearch/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/opensearch/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/oracle/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/oracle/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/postgresql/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/postgresql/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/powerbi/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/powerbi/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/recipe_normalizer.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/reddit/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/reddit/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/s3_client.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/s3_compatible_storage/README.md +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/s3_compatible_storage/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/s3_compatible_storage/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/sandbox/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/sandbox/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/search_engine_base.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/servicedesk/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/servicedesk/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/slack/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/slack/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/snowflake/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/snowflake/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/sqlite/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/sqlite/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/tableau/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/tableau/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/tabular_base.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/tabular_utils.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/wordpress/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/wordpress/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/youtube/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/sources/youtube/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/telemetry.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/utils/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/utils/archive_extraction.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/utils/content_extraction.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/utils/dependency_groups.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/utils/embedded_files.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/utils/external_video.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/utils/file_metadata.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/utils/file_parser.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/utils/file_to_images.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/utils/hashing.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/utils/legacy_office.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/utils/payload.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/utils/range_reader.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/utils/resources.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/utils/transcription.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/utils/uv_sync.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/utils/validation.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/utils/video_processing.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/_lakehouse_fakes.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/conftest.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/broken_links/test_broken_links_detector.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/conftest.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/content/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/custom/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/custom/conftest.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/custom/test_gliner2_classification.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/custom/test_invoice_extraction.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/custom/test_llm_runner.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/custom/test_pipeline_integration.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/custom/test_regex_runner.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/custom/test_runner_hardening.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/pii/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/pii/conftest.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/pii/sample_invoice.pdf +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/pii/test_pii_detector.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/pii/test_pii_detector_extended.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/pii/test_pii_regional_entities.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/pii/test_pii_severity.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/secrets/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/secrets/test_secrets_detector.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/secrets/test_secrets_detector_extended.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/test_base_detector.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/test_custom_detector_examples_runtime.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/test_detector_catalog_commercial.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/test_detector_pipeline_types.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/test_detector_schema_examples.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/test_detector_types.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/test_phase2_detectors.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/test_registry.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/threat/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/threat/test_code_security_detector.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/threat/test_yara_detector.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/integration/test_wordpress_broken_links_detector.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/integration/test_wordpress_links_assets.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/pipeline/test_detector_outcomes.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/pipeline/test_detector_pipeline.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/pipeline/test_worker_pool.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_assets_metadata_catalog.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_azure_blob_storage_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_base_source_attachment.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_base_source_sampling.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_byte_valued_fields.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_config.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_confluence_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_custom_extractor.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_databricks_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_delta_lake_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_dependency_groups.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_dropbox_auth_command.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_dropbox_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_elasticsearch_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_email_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_file_evaluation_runner.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_google_cloud_storage_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_google_workspace_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_hashing.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_hive_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_hugging_face_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_iceberg_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_jira_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_kafka_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_local_folder_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_main.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_meilisearch_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_microsoft_365_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_mongodb_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_mssql_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_mysql_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_neo4j_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_notion_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_object_storage_download_efficiency.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_opensearch_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_oracle_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_outputs.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_payload_window.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_payload_window_parquet.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_postgresql_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_powerbi_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_range_reading.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_recipe_normalizer.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_reddit_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_s3_compatible_storage_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_sampling_automatic.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_sandbox_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_scan_cache.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_servicedesk_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_slack_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_snowflake_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_source_dependency_groups.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_source_strategy_conformance.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_sqlite_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_tableau_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_tabular_automatic_sampling.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_tabular_utils.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_uv_sync.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_wordpress_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_youtube_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/test_youtube_source_integration.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/utils/test_archive_extraction.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/utils/test_content_extraction.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/utils/test_embedded_files.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/utils/test_file_metadata.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/utils/test_file_parser.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/utils/test_file_to_images.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/utils/test_legacy_office.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/utils/test_payload.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/utils/test_transcription.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/utils/test_video_processing.py +0 -0
|
@@ -189,7 +189,17 @@ class GLiNER2Runner(BaseRunner):
|
|
|
189
189
|
self, model: Any, text: str, task_name: str, labels: list[str]
|
|
190
190
|
) -> dict[str, object]:
|
|
191
191
|
"""Classify text, chunking long inputs and keeping the max-confidence label."""
|
|
192
|
-
|
|
192
|
+
all_chunks = _chunk_text(text, _CLS_CHUNK_SIZE, _CLS_CHUNK_OVERLAP)
|
|
193
|
+
chunks = all_chunks[:_MAX_CLS_CHUNKS]
|
|
194
|
+
if len(all_chunks) > _MAX_CLS_CHUNKS:
|
|
195
|
+
logger.warning(
|
|
196
|
+
"GLiNER2 classification for detector '%s': text of %d chars needs %d chunks, "
|
|
197
|
+
"capping at %d; the remainder was not classified",
|
|
198
|
+
self._detector_key,
|
|
199
|
+
len(text),
|
|
200
|
+
len(all_chunks),
|
|
201
|
+
_MAX_CLS_CHUNKS,
|
|
202
|
+
)
|
|
193
203
|
|
|
194
204
|
best: dict[str, object] = {}
|
|
195
205
|
for chunk in chunks:
|
{classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/custom/runners/_text_classification.py
RENAMED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
|
+
from collections.abc import Iterator
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
8
9
|
from ....models.generated_detectors import Severity, TextClassificationPipelineSchema
|
|
@@ -13,6 +14,22 @@ from ._base import _TEXT_CONTENT_TYPES, BaseRunner, _resolve_pipeline_severity
|
|
|
13
14
|
logger = logging.getLogger(__name__)
|
|
14
15
|
|
|
15
16
|
|
|
17
|
+
# A transformer pipeline called with truncation=True silently keeps only the
|
|
18
|
+
# first model_max_length tokens, so without chunking a long document is
|
|
19
|
+
# classified on its opening paragraph alone -- no error, no log, and everything
|
|
20
|
+
# after it is never looked at. When chunk_size is not configured we derive a
|
|
21
|
+
# window from the model's own token limit instead.
|
|
22
|
+
# Deliberate under-estimate: too small a window only costs compute, too large
|
|
23
|
+
# a one hands the tail back to truncation.
|
|
24
|
+
_CHARS_PER_TOKEN = 3
|
|
25
|
+
_DEFAULT_MODEL_MAX_TOKENS = 512
|
|
26
|
+
# model_max_length is a huge sentinel on tokenizers that declare no limit.
|
|
27
|
+
_MODEL_MAX_TOKENS_SENTINEL = 100_000
|
|
28
|
+
_AUTO_CHUNK_OVERLAP = 128
|
|
29
|
+
# Bound per-asset CPU cost; hitting this is logged, never silent.
|
|
30
|
+
_MAX_AUTO_CHUNKS = 200
|
|
31
|
+
|
|
32
|
+
|
|
16
33
|
def _chunk_text(text: str, chunk_size: int | None, chunk_overlap: int) -> list[str]:
|
|
17
34
|
"""Split text into chunks. Returns [text] when chunk_size is not set."""
|
|
18
35
|
if not chunk_size:
|
|
@@ -21,6 +38,55 @@ def _chunk_text(text: str, chunk_size: int | None, chunk_overlap: int) -> list[s
|
|
|
21
38
|
return [text[i : i + chunk_size] for i in range(0, len(text), step)]
|
|
22
39
|
|
|
23
40
|
|
|
41
|
+
def _model_chunk_chars(pipe: Any, max_length: int | None) -> int:
|
|
42
|
+
"""Characters that comfortably fit one forward pass of *pipe*."""
|
|
43
|
+
tokens = max_length
|
|
44
|
+
if tokens is None:
|
|
45
|
+
tokenizer_limit = getattr(getattr(pipe, "tokenizer", None), "model_max_length", None)
|
|
46
|
+
if isinstance(tokenizer_limit, int) and 0 < tokenizer_limit < _MODEL_MAX_TOKENS_SENTINEL:
|
|
47
|
+
tokens = tokenizer_limit
|
|
48
|
+
if not isinstance(tokens, int) or tokens <= 0:
|
|
49
|
+
tokens = _DEFAULT_MODEL_MAX_TOKENS
|
|
50
|
+
return max(256, tokens * _CHARS_PER_TOKEN)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _iter_analysis_chunks(
|
|
54
|
+
text: str,
|
|
55
|
+
chunk_size: int | None,
|
|
56
|
+
chunk_overlap: int,
|
|
57
|
+
auto_chunk_chars: int,
|
|
58
|
+
) -> Iterator[str]:
|
|
59
|
+
"""Yield the windows to classify, one resident at a time.
|
|
60
|
+
|
|
61
|
+
An explicit chunk_size wins. Otherwise the text is passed through whole
|
|
62
|
+
unless it would overflow the model window, in which case it is auto-chunked
|
|
63
|
+
so the tail is classified rather than truncated away.
|
|
64
|
+
"""
|
|
65
|
+
if chunk_size:
|
|
66
|
+
step = max(1, chunk_size - chunk_overlap)
|
|
67
|
+
for start in range(0, len(text), step):
|
|
68
|
+
yield text[start : start + chunk_size]
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
if len(text) <= auto_chunk_chars:
|
|
72
|
+
yield text
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
overlap = min(_AUTO_CHUNK_OVERLAP, auto_chunk_chars // 4)
|
|
76
|
+
step = max(1, auto_chunk_chars - overlap)
|
|
77
|
+
for index, start in enumerate(range(0, len(text), step)):
|
|
78
|
+
if index >= _MAX_AUTO_CHUNKS:
|
|
79
|
+
logger.warning(
|
|
80
|
+
"text_classification: text of %d chars exceeds %d auto-chunks of %d; "
|
|
81
|
+
"the remainder was not classified (set chunk_size to control this)",
|
|
82
|
+
len(text),
|
|
83
|
+
_MAX_AUTO_CHUNKS,
|
|
84
|
+
auto_chunk_chars,
|
|
85
|
+
)
|
|
86
|
+
return
|
|
87
|
+
yield text[start : start + auto_chunk_chars]
|
|
88
|
+
|
|
89
|
+
|
|
24
90
|
class TextClassificationRunner(BaseRunner):
|
|
25
91
|
"""Text classification via a single HuggingFace text-classification pipeline."""
|
|
26
92
|
|
|
@@ -94,8 +160,9 @@ class TextClassificationRunner(BaseRunner):
|
|
|
94
160
|
default_severity = schema.severity if schema.severity is not None else Severity.info
|
|
95
161
|
|
|
96
162
|
best_scores: dict[str, float] = {}
|
|
163
|
+
auto_chunk_chars = _model_chunk_chars(pipe, max_length)
|
|
97
164
|
try:
|
|
98
|
-
for chunk in
|
|
165
|
+
for chunk in _iter_analysis_chunks(text, chunk_size, chunk_overlap, auto_chunk_chars):
|
|
99
166
|
call_kwargs: dict[str, Any] = {"truncation": True}
|
|
100
167
|
if max_length is not None:
|
|
101
168
|
call_kwargs["max_length"] = max_length
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
"""PII detector powered by Microsoft Presidio."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import bisect
|
|
4
5
|
import importlib
|
|
5
6
|
import logging
|
|
6
7
|
import re
|
|
7
8
|
import subprocess
|
|
8
9
|
import sys
|
|
9
10
|
import warnings
|
|
11
|
+
from collections.abc import Iterator
|
|
10
12
|
from dataclasses import dataclass
|
|
11
13
|
from typing import Any, ClassVar
|
|
12
14
|
|
|
@@ -239,6 +241,17 @@ class PIIDetector(BaseDetector):
|
|
|
239
241
|
# Per-cell analysis at scale causes O(rowsxcolumns) Presidio calls per page.
|
|
240
242
|
_TABULAR_CELL_LIMIT: ClassVar[int] = 200
|
|
241
243
|
|
|
244
|
+
# spaCy's own ceiling when nlp.max_length is not overridden.
|
|
245
|
+
_SPACY_DEFAULT_MAX_LENGTH: ClassVar[int] = 1_000_000
|
|
246
|
+
|
|
247
|
+
# Window used when a page exceeds the spaCy ceiling and no chunk_size is
|
|
248
|
+
# configured. The parser/NER need roughly 1GB of scratch memory per 100k
|
|
249
|
+
# characters, so 200k caps peak usage near 2GB no matter how large the page
|
|
250
|
+
# is -- whereas simply raising nlp.max_length scales memory with file size
|
|
251
|
+
# (a 1.4M-char page would need ~14GB).
|
|
252
|
+
_AUTO_CHUNK_CHARS: ClassVar[int] = 200_000
|
|
253
|
+
_AUTO_CHUNK_OVERLAP: ClassVar[int] = 512
|
|
254
|
+
|
|
242
255
|
def __init__(self, config: DetectorConfig | None = None) -> None:
|
|
243
256
|
super().__init__(config)
|
|
244
257
|
self._cfg: PIIDetectorConfig = (
|
|
@@ -983,14 +996,64 @@ class PIIDetector(BaseDetector):
|
|
|
983
996
|
# Offloading to a thread keeps the loop alive and allows I/O to proceed.
|
|
984
997
|
return await asyncio.to_thread(self._detect_sync, content)
|
|
985
998
|
|
|
986
|
-
def
|
|
987
|
-
"""
|
|
999
|
+
def _spacy_char_limit(self) -> int:
|
|
1000
|
+
"""Largest text spaCy will accept in a single call, honouring max_length."""
|
|
1001
|
+
return _unwrap_int(getattr(self._cfg, "max_length", None)) or self._SPACY_DEFAULT_MAX_LENGTH
|
|
1002
|
+
|
|
1003
|
+
def _iter_chunks(self, text: str) -> Iterator[tuple[str, int]]:
|
|
1004
|
+
"""Yield (chunk, offset) pairs, one chunk resident at a time.
|
|
1005
|
+
|
|
1006
|
+
With chunk_size configured the text is split on that fixed window. With
|
|
1007
|
+
chunk_size null the text is passed through whole -- unless it exceeds
|
|
1008
|
+
the spaCy ceiling, in which case it is auto-chunked instead of raising
|
|
1009
|
+
E088 and losing every finding on the page.
|
|
1010
|
+
"""
|
|
988
1011
|
chunk_size = _unwrap_int(getattr(self._cfg, "chunk_size", None))
|
|
989
|
-
if
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1012
|
+
if chunk_size:
|
|
1013
|
+
overlap = _unwrap_int(getattr(self._cfg, "chunk_overlap", None)) or 0
|
|
1014
|
+
step = max(1, chunk_size - overlap)
|
|
1015
|
+
for start in range(0, len(text), step):
|
|
1016
|
+
yield (text[start : start + chunk_size], start)
|
|
1017
|
+
return
|
|
1018
|
+
|
|
1019
|
+
if len(text) <= self._spacy_char_limit():
|
|
1020
|
+
yield (text, 0)
|
|
1021
|
+
return
|
|
1022
|
+
|
|
1023
|
+
yield from self._auto_chunks(text)
|
|
1024
|
+
|
|
1025
|
+
def _auto_chunks(self, text: str) -> Iterator[tuple[str, int]]:
|
|
1026
|
+
"""Split an oversized page into spaCy-sized windows on whitespace boundaries."""
|
|
1027
|
+
window = min(self._AUTO_CHUNK_CHARS, self._spacy_char_limit())
|
|
1028
|
+
overlap = min(self._AUTO_CHUNK_OVERLAP, window // 4)
|
|
1029
|
+
total = len(text)
|
|
1030
|
+
logger.info(
|
|
1031
|
+
"Page of %d chars exceeds the spaCy limit (%d); auto-chunking at %d chars "
|
|
1032
|
+
"(set chunk_size to control this explicitly)",
|
|
1033
|
+
total,
|
|
1034
|
+
self._spacy_char_limit(),
|
|
1035
|
+
window,
|
|
1036
|
+
)
|
|
1037
|
+
|
|
1038
|
+
start = 0
|
|
1039
|
+
while start < total:
|
|
1040
|
+
end = min(start + window, total)
|
|
1041
|
+
if end < total:
|
|
1042
|
+
# Prefer a line/word boundary inside the trailing overlap zone so
|
|
1043
|
+
# entities are not cut in half; the overlap re-covers the seam.
|
|
1044
|
+
split = text.rfind("\n", end - overlap, end)
|
|
1045
|
+
if split == -1:
|
|
1046
|
+
split = text.rfind(" ", end - overlap, end)
|
|
1047
|
+
if split > start:
|
|
1048
|
+
end = split + 1
|
|
1049
|
+
yield (text[start:end], start)
|
|
1050
|
+
if end >= total:
|
|
1051
|
+
return
|
|
1052
|
+
start = max(start + 1, end - overlap)
|
|
1053
|
+
|
|
1054
|
+
def _chunk_text(self, text: str) -> list[tuple[str, int]]:
|
|
1055
|
+
"""Materialized :meth:`_iter_chunks` (kept for callers that need a list)."""
|
|
1056
|
+
return list(self._iter_chunks(text))
|
|
994
1057
|
|
|
995
1058
|
def _detect_sync(self, content: str) -> list[DetectionResult]:
|
|
996
1059
|
tabular_results = self._detect_tabular_content(content)
|
|
@@ -1004,8 +1067,11 @@ class PIIDetector(BaseDetector):
|
|
|
1004
1067
|
threshold = self._cfg.confidence_threshold or 0.7
|
|
1005
1068
|
results: list[DetectionResult] = []
|
|
1006
1069
|
seen: set[tuple[str, int, int]] = set()
|
|
1070
|
+
# Precomputed once: content[:start].count("\n") per finding is O(n) each
|
|
1071
|
+
# and copies the prefix, which is quadratic on multi-megabyte pages.
|
|
1072
|
+
newline_offsets = [m.start() for m in re.finditer("\n", content)]
|
|
1007
1073
|
|
|
1008
|
-
for chunk, offset in self.
|
|
1074
|
+
for chunk, offset in self._iter_chunks(content):
|
|
1009
1075
|
for result in self._analyze_content(chunk, entities=entities):
|
|
1010
1076
|
if not self._is_entity_enabled(result.entity_type):
|
|
1011
1077
|
continue
|
|
@@ -1017,7 +1083,7 @@ class PIIDetector(BaseDetector):
|
|
|
1017
1083
|
continue
|
|
1018
1084
|
seen.add(dedup_key)
|
|
1019
1085
|
|
|
1020
|
-
line_number =
|
|
1086
|
+
line_number = bisect.bisect_left(newline_offsets, abs_start) + 1
|
|
1021
1087
|
matched_content = content[abs_start:abs_end]
|
|
1022
1088
|
|
|
1023
1089
|
detection = self._build_detection_result(
|
|
@@ -432,7 +432,7 @@ class SecretsDetectorConfig(DetectorConfig):
|
|
|
432
432
|
class MaxLength(RootModel[int]):
|
|
433
433
|
root: int = Field(
|
|
434
434
|
None,
|
|
435
|
-
description="Override spaCy's nlp.max_length (default 1,000,000 chars).
|
|
435
|
+
description="Override spaCy's nlp.max_length (default 1,000,000 chars). Raising it makes memory scale with input size (~1GB of scratch per 100,000 chars), so prefer chunk_size for very large texts. Oversized pages are auto-chunked rather than failing, so this rarely needs setting.",
|
|
436
436
|
ge=1,
|
|
437
437
|
)
|
|
438
438
|
|
|
@@ -440,7 +440,7 @@ class MaxLength(RootModel[int]):
|
|
|
440
440
|
class ChunkSize(RootModel[int]):
|
|
441
441
|
root: int = Field(
|
|
442
442
|
None,
|
|
443
|
-
description='Split text into chunks of this many characters before analysis. Findings from all chunks are merged with corrected offsets. When null the full text is passed as-is
|
|
443
|
+
description='Split text into chunks of this many characters before analysis. Findings from all chunks are merged with corrected offsets. When null the full text is passed as-is, unless it exceeds the spaCy limit, in which case it is auto-chunked at 200,000 characters on whitespace boundaries.',
|
|
444
444
|
ge=1,
|
|
445
445
|
)
|
|
446
446
|
|
|
@@ -483,11 +483,11 @@ class PIIDetectorConfig(DetectorConfig):
|
|
|
483
483
|
)
|
|
484
484
|
max_length: MaxLength | None = Field(
|
|
485
485
|
None,
|
|
486
|
-
description="Override spaCy's nlp.max_length (default 1,000,000 chars).
|
|
486
|
+
description="Override spaCy's nlp.max_length (default 1,000,000 chars). Raising it makes memory scale with input size (~1GB of scratch per 100,000 chars), so prefer chunk_size for very large texts. Oversized pages are auto-chunked rather than failing, so this rarely needs setting.",
|
|
487
487
|
)
|
|
488
488
|
chunk_size: ChunkSize | None = Field(
|
|
489
489
|
None,
|
|
490
|
-
description='Split text into chunks of this many characters before analysis. Findings from all chunks are merged with corrected offsets. When null the full text is passed as-is
|
|
490
|
+
description='Split text into chunks of this many characters before analysis. Findings from all chunks are merged with corrected offsets. When null the full text is passed as-is, unless it exceeds the spaCy limit, in which case it is auto-chunked at 200,000 characters on whitespace boundaries.',
|
|
491
491
|
)
|
|
492
492
|
chunk_overlap: ChunkOverlap | None = Field(
|
|
493
493
|
0,
|
{classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/tests/detectors/custom/test_transformer_runners.py
RENAMED
|
@@ -89,6 +89,56 @@ async def test_text_classification_runner_applies_severity_map() -> None:
|
|
|
89
89
|
assert findings[0].severity == Severity.high
|
|
90
90
|
|
|
91
91
|
|
|
92
|
+
@pytest.mark.asyncio
|
|
93
|
+
async def test_text_classification_short_text_is_a_single_call() -> None:
|
|
94
|
+
runner = _make_text_cls_runner([{"label": "SPAM", "score": 0.95}])
|
|
95
|
+
|
|
96
|
+
runner.detect("Win a free iPhone now!", "text/plain")
|
|
97
|
+
|
|
98
|
+
assert runner._pipe.call_count == 1
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@pytest.mark.asyncio
|
|
102
|
+
async def test_text_classification_long_text_is_chunked_not_truncated() -> None:
|
|
103
|
+
# A pipeline called with truncation=True keeps only the first ~512 tokens,
|
|
104
|
+
# so an unchunked long document was classified on its opening lines alone
|
|
105
|
+
# and the rest was silently discarded.
|
|
106
|
+
runner = _make_text_cls_runner([{"label": "SPAM", "score": 0.95}])
|
|
107
|
+
text = "spam and eggs " * 5_000 # 70k chars, far past any model window
|
|
108
|
+
|
|
109
|
+
runner.detect(text, "text/plain")
|
|
110
|
+
|
|
111
|
+
assert runner._pipe.call_count > 1
|
|
112
|
+
seen = "".join(call.args[0] for call in runner._pipe.call_args_list)
|
|
113
|
+
# Overlapping windows, so the concatenation is longer than the input -- the
|
|
114
|
+
# point is that no part of it went unseen.
|
|
115
|
+
assert len(seen) >= len(text)
|
|
116
|
+
assert all(len(call.args[0]) <= 4_000 for call in runner._pipe.call_args_list)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@pytest.mark.asyncio
|
|
120
|
+
async def test_text_classification_chunk_cap_is_bounded() -> None:
|
|
121
|
+
from src.detectors.custom.runners._text_classification import _MAX_AUTO_CHUNKS
|
|
122
|
+
|
|
123
|
+
runner = _make_text_cls_runner([{"label": "SPAM", "score": 0.95}])
|
|
124
|
+
|
|
125
|
+
runner.detect("spam and eggs " * 200_000, "text/plain")
|
|
126
|
+
|
|
127
|
+
assert runner._pipe.call_count <= _MAX_AUTO_CHUNKS
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@pytest.mark.asyncio
|
|
131
|
+
async def test_text_classification_scores_are_max_across_chunks() -> None:
|
|
132
|
+
runner = _make_text_cls_runner([])
|
|
133
|
+
runner._pipe = MagicMock(
|
|
134
|
+
side_effect=[[{"label": "SPAM", "score": s}] for s in (0.1, 0.99, 0.2)] * 200
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
findings = runner.detect("spam and eggs " * 400, "text/plain")
|
|
138
|
+
|
|
139
|
+
assert findings[0].confidence == pytest.approx(0.99)
|
|
140
|
+
|
|
141
|
+
|
|
92
142
|
def test_text_classification_run_raises_not_implemented() -> None:
|
|
93
143
|
runner = _make_text_cls_runner([])
|
|
94
144
|
with pytest.raises(NotImplementedError):
|
|
@@ -10,6 +10,8 @@ produced nothing for the whole corpus.
|
|
|
10
10
|
These tests need no Presidio: they exercise the config plumbing directly.
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
+
import itertools
|
|
14
|
+
|
|
13
15
|
from src.detectors.pii.detector import PIIDetector, _unwrap_int
|
|
14
16
|
from src.models.generated_detectors import ChunkOverlap, ChunkSize, MaxLength
|
|
15
17
|
|
|
@@ -119,3 +121,79 @@ class TestMaxLength:
|
|
|
119
121
|
|
|
120
122
|
def test_unset_max_length_is_none(self):
|
|
121
123
|
assert _unwrap_int(None) is None
|
|
124
|
+
|
|
125
|
+
def test_limit_defaults_to_spacy_default(self):
|
|
126
|
+
detector = _detector_with(_Cfg())
|
|
127
|
+
|
|
128
|
+
assert detector._spacy_char_limit() == PIIDetector._SPACY_DEFAULT_MAX_LENGTH
|
|
129
|
+
|
|
130
|
+
def test_limit_follows_configured_max_length(self):
|
|
131
|
+
detector = _detector_with(_Cfg(max_length=MaxLength(root=2_000_000)))
|
|
132
|
+
|
|
133
|
+
assert detector._spacy_char_limit() == 2_000_000
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class TestAutoChunking:
|
|
137
|
+
"""A page longer than spaCy's ceiling used to raise E088 inside Presidio,
|
|
138
|
+
which the detector swallowed -- so every finding on that page was lost.
|
|
139
|
+
Oversized pages are now split into bounded windows instead."""
|
|
140
|
+
|
|
141
|
+
def _oversized(self, chars: int) -> str:
|
|
142
|
+
# Word-shaped text so boundary snapping has somewhere to cut.
|
|
143
|
+
return ("lorem ipsum dolor sit amet " * ((chars // 27) + 1))[:chars]
|
|
144
|
+
|
|
145
|
+
def test_text_under_the_limit_is_not_chunked(self):
|
|
146
|
+
detector = _detector_with(_Cfg())
|
|
147
|
+
|
|
148
|
+
assert detector._chunk_text("some text") == [("some text", 0)]
|
|
149
|
+
|
|
150
|
+
def test_oversized_text_is_split_without_config(self):
|
|
151
|
+
detector = _detector_with(_Cfg())
|
|
152
|
+
text = self._oversized(PIIDetector._SPACY_DEFAULT_MAX_LENGTH + 500_000)
|
|
153
|
+
|
|
154
|
+
chunks = detector._chunk_text(text)
|
|
155
|
+
|
|
156
|
+
assert len(chunks) > 1
|
|
157
|
+
assert all(len(chunk) <= PIIDetector._AUTO_CHUNK_CHARS for chunk, _ in chunks)
|
|
158
|
+
|
|
159
|
+
def test_chunks_cover_the_whole_text_at_correct_offsets(self):
|
|
160
|
+
detector = _detector_with(_Cfg())
|
|
161
|
+
text = self._oversized(PIIDetector._SPACY_DEFAULT_MAX_LENGTH + 12_345)
|
|
162
|
+
|
|
163
|
+
chunks = detector._chunk_text(text)
|
|
164
|
+
|
|
165
|
+
# Every chunk must sit at the offset it claims, or finding positions
|
|
166
|
+
# reported back to the API point at the wrong bytes.
|
|
167
|
+
assert all(text[offset : offset + len(chunk)] == chunk for chunk, offset in chunks)
|
|
168
|
+
assert chunks[0][1] == 0
|
|
169
|
+
last_chunk, last_offset = chunks[-1]
|
|
170
|
+
assert last_offset + len(last_chunk) == len(text)
|
|
171
|
+
# No gaps between consecutive windows.
|
|
172
|
+
for (chunk, offset), (_, next_offset) in itertools.pairwise(chunks):
|
|
173
|
+
assert next_offset <= offset + len(chunk)
|
|
174
|
+
|
|
175
|
+
def test_a_lower_max_length_tightens_the_window(self):
|
|
176
|
+
detector = _detector_with(_Cfg(max_length=MaxLength(root=50_000)))
|
|
177
|
+
text = self._oversized(120_000)
|
|
178
|
+
|
|
179
|
+
chunks = detector._chunk_text(text)
|
|
180
|
+
|
|
181
|
+
assert all(len(chunk) <= 50_000 for chunk, _ in chunks)
|
|
182
|
+
|
|
183
|
+
def test_explicit_chunk_size_still_wins_over_auto(self):
|
|
184
|
+
detector = _detector_with(_Cfg(chunk_size=ChunkSize(root=10), chunk_overlap=None))
|
|
185
|
+
text = self._oversized(PIIDetector._SPACY_DEFAULT_MAX_LENGTH + 1)
|
|
186
|
+
|
|
187
|
+
first, offset = next(iter(detector._iter_chunks(text)))
|
|
188
|
+
|
|
189
|
+
assert offset == 0
|
|
190
|
+
assert len(first) == 10
|
|
191
|
+
|
|
192
|
+
def test_text_without_whitespace_still_terminates(self):
|
|
193
|
+
detector = _detector_with(_Cfg(max_length=MaxLength(root=1_000)))
|
|
194
|
+
text = "a" * 5_000
|
|
195
|
+
|
|
196
|
+
chunks = detector._chunk_text(text)
|
|
197
|
+
|
|
198
|
+
assert chunks[-1][1] + len(chunks[-1][0]) == len(text)
|
|
199
|
+
assert all(len(chunk) <= 1_000 for chunk, _ in chunks)
|
|
@@ -830,7 +830,7 @@ wheels = [
|
|
|
830
830
|
|
|
831
831
|
[[package]]
|
|
832
832
|
name = "classifyre-cli"
|
|
833
|
-
version = "0.4.
|
|
833
|
+
version = "0.4.86"
|
|
834
834
|
source = { editable = "." }
|
|
835
835
|
dependencies = [
|
|
836
836
|
{ name = "beautifulsoup4" },
|
|
@@ -1369,7 +1369,7 @@ youtube = [
|
|
|
1369
1369
|
|
|
1370
1370
|
[[package]]
|
|
1371
1371
|
name = "classifyre-schemas"
|
|
1372
|
-
version = "0.4.
|
|
1372
|
+
version = "0.4.86"
|
|
1373
1373
|
source = { editable = "../../packages/schemas" }
|
|
1374
1374
|
dependencies = [
|
|
1375
1375
|
{ name = "fastjsonschema" },
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/custom/runners/_object_detection.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/detectors/threat/code_security_detector.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{classifyre_cli-0.4.85 → classifyre_cli-0.4.86}/src/models/generated_single_asset_scan_results.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|