classifyre-cli 0.4.85__tar.gz → 0.4.87__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.87/.turbo/turbo-build.log +3 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/PKG-INFO +1 -1
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/package.json +1 -1
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/pyproject.toml +1 -1
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/custom/runners/_gliner2.py +11 -1
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/custom/runners/_text_classification.py +68 -1
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/pii/detector.py +105 -9
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/models/generated_detectors.py +4 -4
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/outputs/base.py +8 -1
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/outputs/factory.py +18 -4
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/outputs/rest.py +17 -7
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/sandbox/source.py +8 -3
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/custom/test_transformer_runners.py +50 -0
- classifyre_cli-0.4.87/tests/detectors/pii/test_pii_chunking.py +327 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_outputs.py +50 -12
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/uv.lock +2 -2
- classifyre_cli-0.4.85/.turbo/turbo-build.log +0 -3
- classifyre_cli-0.4.85/tests/detectors/pii/test_pii_chunking.py +0 -121
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/.gitignore +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/.python-version +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/README.md +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/main.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/scripts/generate_models.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/config.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/base.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/broken_links/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/broken_links/detector.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/config.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/content/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/custom/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/custom/detector.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/custom/extractor.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/custom/runners/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/custom/runners/_base.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/custom/runners/_factory.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/custom/runners/_image_classification.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/custom/runners/_llm.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/custom/runners/_object_detection.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/custom/runners/_regex.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/custom/trainer.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/dependencies.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/engine_version.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/pii/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/secrets/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/secrets/detector.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/threat/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/threat/code_security_detector.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/detectors/threat/yara_detector.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/file_evaluation/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/file_evaluation/runner.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/main.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/models/generated_input.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/models/generated_single_asset_scan_results.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/outputs/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/outputs/console.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/outputs/file.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/pipeline/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/pipeline/content_provider.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/pipeline/detector_pipeline.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/pipeline/parsed_content_provider.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/pipeline/payload_window.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/pipeline/scan_cache.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/pipeline/text_artifact.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/pipeline/worker_pool.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/asset_metadata.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/atlassian_common.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/azure_blob_storage/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/azure_blob_storage/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/base.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/confluence/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/confluence/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/databricks/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/databricks/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/delta_lake/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/delta_lake/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/dependencies.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/dropbox/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/dropbox/auth.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/dropbox/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/elasticsearch/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/elasticsearch/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/email/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/email/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/google_cloud_storage/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/google_cloud_storage/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/google_workspace/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/google_workspace/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/hive/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/hive/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/hugging_face/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/hugging_face/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/iceberg/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/iceberg/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/jira/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/jira/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/kafka/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/kafka/rest.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/kafka/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/lakehouse_base.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/local_folder/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/local_folder/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/meilisearch/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/meilisearch/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/microsoft_365/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/microsoft_365/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/mongodb/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/mongodb/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/mssql/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/mssql/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/mysql/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/mysql/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/neo4j/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/neo4j/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/notion/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/notion/client.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/notion/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/object_storage/base.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/opensearch/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/opensearch/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/oracle/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/oracle/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/postgresql/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/postgresql/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/powerbi/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/powerbi/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/recipe_normalizer.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/reddit/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/reddit/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/s3_client.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/s3_compatible_storage/README.md +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/s3_compatible_storage/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/s3_compatible_storage/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/sandbox/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/search_engine_base.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/servicedesk/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/servicedesk/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/slack/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/slack/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/snowflake/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/snowflake/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/sqlite/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/sqlite/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/tableau/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/tableau/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/tabular_base.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/tabular_utils.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/wordpress/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/wordpress/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/youtube/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/sources/youtube/source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/telemetry.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/utils/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/utils/archive_extraction.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/utils/content_extraction.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/utils/dependency_groups.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/utils/embedded_files.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/utils/external_video.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/utils/file_metadata.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/utils/file_parser.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/utils/file_to_images.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/utils/hashing.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/utils/legacy_office.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/utils/payload.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/utils/range_reader.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/utils/resources.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/utils/transcription.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/utils/uv_sync.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/utils/validation.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/src/utils/video_processing.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/_lakehouse_fakes.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/conftest.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/broken_links/test_broken_links_detector.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/conftest.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/content/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/custom/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/custom/conftest.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/custom/test_gliner2_classification.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/custom/test_invoice_extraction.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/custom/test_llm_runner.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/custom/test_pipeline_integration.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/custom/test_regex_runner.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/custom/test_runner_hardening.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/pii/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/pii/conftest.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/pii/sample_invoice.pdf +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/pii/test_pii_detector.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/pii/test_pii_detector_extended.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/pii/test_pii_regional_entities.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/pii/test_pii_severity.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/secrets/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/secrets/test_secrets_detector.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/secrets/test_secrets_detector_extended.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/test_base_detector.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/test_custom_detector_examples_runtime.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/test_detector_catalog_commercial.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/test_detector_pipeline_types.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/test_detector_schema_examples.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/test_detector_types.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/test_phase2_detectors.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/test_registry.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/threat/__init__.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/threat/test_code_security_detector.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/detectors/threat/test_yara_detector.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/integration/test_wordpress_broken_links_detector.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/integration/test_wordpress_links_assets.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/pipeline/test_detector_outcomes.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/pipeline/test_detector_pipeline.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/pipeline/test_worker_pool.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_assets_metadata_catalog.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_azure_blob_storage_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_base_source_attachment.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_base_source_sampling.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_byte_valued_fields.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_config.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_confluence_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_custom_extractor.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_databricks_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_delta_lake_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_dependency_groups.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_dropbox_auth_command.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_dropbox_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_elasticsearch_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_email_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_file_evaluation_runner.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_google_cloud_storage_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_google_workspace_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_hashing.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_hive_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_hugging_face_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_iceberg_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_jira_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_kafka_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_local_folder_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_main.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_meilisearch_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_microsoft_365_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_mongodb_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_mssql_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_mysql_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_neo4j_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_notion_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_object_storage_download_efficiency.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_opensearch_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_oracle_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_payload_window.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_payload_window_parquet.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_postgresql_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_powerbi_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_range_reading.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_recipe_normalizer.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_reddit_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_s3_compatible_storage_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_sampling_automatic.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_sandbox_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_scan_cache.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_servicedesk_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_slack_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_snowflake_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_source_dependency_groups.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_source_strategy_conformance.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_sqlite_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_tableau_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_tabular_automatic_sampling.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_tabular_utils.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_uv_sync.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_wordpress_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_youtube_source.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/test_youtube_source_integration.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/utils/test_archive_extraction.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/utils/test_content_extraction.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/utils/test_embedded_files.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/utils/test_file_metadata.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/utils/test_file_parser.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/utils/test_file_to_images.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/utils/test_legacy_office.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/utils/test_payload.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/tests/utils/test_transcription.py +0 -0
- {classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/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.87}/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 = (
|
|
@@ -659,10 +672,40 @@ class PIIDetector(BaseDetector):
|
|
|
659
672
|
# ------------------------------------------------------------------
|
|
660
673
|
|
|
661
674
|
def _analyze_content(self, content: str, *, entities: list[str] | None = None) -> list[Any]:
|
|
675
|
+
"""Analyze *content*, splitting it first if it would overflow spaCy.
|
|
676
|
+
|
|
677
|
+
This is the single chokepoint every caller goes through -- full-text
|
|
678
|
+
pages, individual tabular cells, and pre-chunked windows alike -- so the
|
|
679
|
+
oversize guard lives here rather than at any one call site. A page that
|
|
680
|
+
arrives already chunked passes straight through.
|
|
681
|
+
"""
|
|
662
682
|
if self.analyzer is None:
|
|
663
683
|
if self._init_error is not None:
|
|
664
684
|
raise self._init_error
|
|
665
685
|
return []
|
|
686
|
+
if len(content) > self._spacy_char_limit():
|
|
687
|
+
return self._analyze_oversized(content, entities=entities)
|
|
688
|
+
return self._analyze_once(content, entities=entities)
|
|
689
|
+
|
|
690
|
+
def _analyze_oversized(self, content: str, *, entities: list[str] | None) -> list[Any]:
|
|
691
|
+
"""Analyze text past the spaCy ceiling window by window, merged into one result set.
|
|
692
|
+
|
|
693
|
+
Results are re-based onto absolute offsets in *content* and deduped
|
|
694
|
+
across the overlap, keeping the highest-scoring hit for each span.
|
|
695
|
+
"""
|
|
696
|
+
merged: dict[tuple[str, int, int], Any] = {}
|
|
697
|
+
for chunk, offset in self._auto_chunks(content):
|
|
698
|
+
for result in self._analyze_once(chunk, entities=entities):
|
|
699
|
+
result.start += offset
|
|
700
|
+
result.end += offset
|
|
701
|
+
key = (result.entity_type, result.start, result.end)
|
|
702
|
+
existing = merged.get(key)
|
|
703
|
+
if existing is None or result.score > existing.score:
|
|
704
|
+
merged[key] = result
|
|
705
|
+
return sorted(merged.values(), key=lambda r: (r.start, r.end))
|
|
706
|
+
|
|
707
|
+
def _analyze_once(self, content: str, *, entities: list[str] | None = None) -> list[Any]:
|
|
708
|
+
"""One Presidio pass over text already known to fit the spaCy window."""
|
|
666
709
|
try:
|
|
667
710
|
return self.analyzer.analyze(text=content, language="en", entities=entities)
|
|
668
711
|
except ModuleNotFoundError as exc:
|
|
@@ -983,14 +1026,64 @@ class PIIDetector(BaseDetector):
|
|
|
983
1026
|
# Offloading to a thread keeps the loop alive and allows I/O to proceed.
|
|
984
1027
|
return await asyncio.to_thread(self._detect_sync, content)
|
|
985
1028
|
|
|
986
|
-
def
|
|
987
|
-
"""
|
|
1029
|
+
def _spacy_char_limit(self) -> int:
|
|
1030
|
+
"""Largest text spaCy will accept in a single call, honouring max_length."""
|
|
1031
|
+
return _unwrap_int(getattr(self._cfg, "max_length", None)) or self._SPACY_DEFAULT_MAX_LENGTH
|
|
1032
|
+
|
|
1033
|
+
def _iter_chunks(self, text: str) -> Iterator[tuple[str, int]]:
|
|
1034
|
+
"""Yield (chunk, offset) pairs, one chunk resident at a time.
|
|
1035
|
+
|
|
1036
|
+
With chunk_size configured the text is split on that fixed window. With
|
|
1037
|
+
chunk_size null the text is passed through whole -- unless it exceeds
|
|
1038
|
+
the spaCy ceiling, in which case it is auto-chunked instead of raising
|
|
1039
|
+
E088 and losing every finding on the page.
|
|
1040
|
+
"""
|
|
988
1041
|
chunk_size = _unwrap_int(getattr(self._cfg, "chunk_size", None))
|
|
989
|
-
if
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1042
|
+
if chunk_size:
|
|
1043
|
+
overlap = _unwrap_int(getattr(self._cfg, "chunk_overlap", None)) or 0
|
|
1044
|
+
step = max(1, chunk_size - overlap)
|
|
1045
|
+
for start in range(0, len(text), step):
|
|
1046
|
+
yield (text[start : start + chunk_size], start)
|
|
1047
|
+
return
|
|
1048
|
+
|
|
1049
|
+
if len(text) <= self._spacy_char_limit():
|
|
1050
|
+
yield (text, 0)
|
|
1051
|
+
return
|
|
1052
|
+
|
|
1053
|
+
yield from self._auto_chunks(text)
|
|
1054
|
+
|
|
1055
|
+
def _auto_chunks(self, text: str) -> Iterator[tuple[str, int]]:
|
|
1056
|
+
"""Split an oversized page into spaCy-sized windows on whitespace boundaries."""
|
|
1057
|
+
window = min(self._AUTO_CHUNK_CHARS, self._spacy_char_limit())
|
|
1058
|
+
overlap = min(self._AUTO_CHUNK_OVERLAP, window // 4)
|
|
1059
|
+
total = len(text)
|
|
1060
|
+
logger.info(
|
|
1061
|
+
"Page of %d chars exceeds the spaCy limit (%d); auto-chunking at %d chars "
|
|
1062
|
+
"(set chunk_size to control this explicitly)",
|
|
1063
|
+
total,
|
|
1064
|
+
self._spacy_char_limit(),
|
|
1065
|
+
window,
|
|
1066
|
+
)
|
|
1067
|
+
|
|
1068
|
+
start = 0
|
|
1069
|
+
while start < total:
|
|
1070
|
+
end = min(start + window, total)
|
|
1071
|
+
if end < total:
|
|
1072
|
+
# Prefer a line/word boundary inside the trailing overlap zone so
|
|
1073
|
+
# entities are not cut in half; the overlap re-covers the seam.
|
|
1074
|
+
split = text.rfind("\n", end - overlap, end)
|
|
1075
|
+
if split == -1:
|
|
1076
|
+
split = text.rfind(" ", end - overlap, end)
|
|
1077
|
+
if split > start:
|
|
1078
|
+
end = split + 1
|
|
1079
|
+
yield (text[start:end], start)
|
|
1080
|
+
if end >= total:
|
|
1081
|
+
return
|
|
1082
|
+
start = max(start + 1, end - overlap)
|
|
1083
|
+
|
|
1084
|
+
def _chunk_text(self, text: str) -> list[tuple[str, int]]:
|
|
1085
|
+
"""Materialized :meth:`_iter_chunks` (kept for callers that need a list)."""
|
|
1086
|
+
return list(self._iter_chunks(text))
|
|
994
1087
|
|
|
995
1088
|
def _detect_sync(self, content: str) -> list[DetectionResult]:
|
|
996
1089
|
tabular_results = self._detect_tabular_content(content)
|
|
@@ -1004,8 +1097,11 @@ class PIIDetector(BaseDetector):
|
|
|
1004
1097
|
threshold = self._cfg.confidence_threshold or 0.7
|
|
1005
1098
|
results: list[DetectionResult] = []
|
|
1006
1099
|
seen: set[tuple[str, int, int]] = set()
|
|
1100
|
+
# Precomputed once: content[:start].count("\n") per finding is O(n) each
|
|
1101
|
+
# and copies the prefix, which is quadratic on multi-megabyte pages.
|
|
1102
|
+
newline_offsets = [m.start() for m in re.finditer("\n", content)]
|
|
1007
1103
|
|
|
1008
|
-
for chunk, offset in self.
|
|
1104
|
+
for chunk, offset in self._iter_chunks(content):
|
|
1009
1105
|
for result in self._analyze_content(chunk, entities=entities):
|
|
1010
1106
|
if not self._is_entity_enabled(result.entity_type):
|
|
1011
1107
|
continue
|
|
@@ -1017,7 +1113,7 @@ class PIIDetector(BaseDetector):
|
|
|
1017
1113
|
continue
|
|
1018
1114
|
seen.add(dedup_key)
|
|
1019
1115
|
|
|
1020
|
-
line_number =
|
|
1116
|
+
line_number = bisect.bisect_left(newline_offsets, abs_start) + 1
|
|
1021
1117
|
matched_content = content[abs_start:abs_end]
|
|
1022
1118
|
|
|
1023
1119
|
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,
|
|
@@ -7,6 +7,12 @@ from pydantic import BaseModel
|
|
|
7
7
|
|
|
8
8
|
OutputType = Literal["rest", "file", "console"]
|
|
9
9
|
|
|
10
|
+
# How long to wait for the API to answer a write-back. Generous enough that a
|
|
11
|
+
# bulk ingest of a large batch finishes well inside it — the old 30 s cut those
|
|
12
|
+
# off mid-flight and made urllib3 re-upload the same body — but finite, so a
|
|
13
|
+
# wedged API fails the run instead of hanging it forever.
|
|
14
|
+
DEFAULT_REST_TIMEOUT_SEC = 300
|
|
15
|
+
|
|
10
16
|
|
|
11
17
|
@dataclass(frozen=True)
|
|
12
18
|
class OutputRuntimeContext:
|
|
@@ -24,7 +30,8 @@ class OutputSettings:
|
|
|
24
30
|
runner_id: str | None
|
|
25
31
|
managed_runner: bool
|
|
26
32
|
rest_url: str | None = None
|
|
27
|
-
|
|
33
|
+
# Read timeout in seconds; None disables it. See resolve_output_settings.
|
|
34
|
+
rest_timeout_sec: int | None = DEFAULT_REST_TIMEOUT_SEC
|
|
28
35
|
file_path: str | None = None
|
|
29
36
|
|
|
30
37
|
|
|
@@ -4,7 +4,13 @@ import argparse
|
|
|
4
4
|
import os
|
|
5
5
|
from typing import Any, cast
|
|
6
6
|
|
|
7
|
-
from .base import
|
|
7
|
+
from .base import (
|
|
8
|
+
DEFAULT_REST_TIMEOUT_SEC,
|
|
9
|
+
OutputRuntimeContext,
|
|
10
|
+
OutputSettings,
|
|
11
|
+
OutputSink,
|
|
12
|
+
OutputType,
|
|
13
|
+
)
|
|
8
14
|
from .console import ConsoleOutputSink
|
|
9
15
|
from .file import FileOutputSink
|
|
10
16
|
from .rest import RestOutputSink
|
|
@@ -96,9 +102,17 @@ def resolve_output_settings(
|
|
|
96
102
|
)
|
|
97
103
|
rest_url = str(rest_url_value) if rest_url_value is not None else None
|
|
98
104
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
105
|
+
# 5 minutes by default (see DEFAULT_REST_TIMEOUT_SEC): long enough that a
|
|
106
|
+
# bulk ingest of a large batch is never cut off mid-flight, short enough
|
|
107
|
+
# that a wedged API surfaces as a failure. Override with
|
|
108
|
+
# CLASSIFYRE_OUTPUT_REST_TIMEOUT_SEC; 0 or negative disables the read
|
|
109
|
+
# timeout entirely.
|
|
110
|
+
rest_timeout_sec: int | None = _parse_int(
|
|
111
|
+
_coalesce(env_rest_timeout, DEFAULT_REST_TIMEOUT_SEC),
|
|
112
|
+
DEFAULT_REST_TIMEOUT_SEC,
|
|
113
|
+
)
|
|
114
|
+
if rest_timeout_sec is not None and rest_timeout_sec < 1:
|
|
115
|
+
rest_timeout_sec = None
|
|
102
116
|
|
|
103
117
|
file_path_value = _coalesce(
|
|
104
118
|
getattr(args, "output_file_path", None),
|
|
@@ -55,9 +55,12 @@ class _JitteredRetry(Retry):
|
|
|
55
55
|
# What we retry and why:
|
|
56
56
|
# connect=8 — pod restarted / not yet ready (RemoteDisconnected, ConnectionReset,
|
|
57
57
|
# ConnectTimeout). Request never reached the application.
|
|
58
|
-
# read=8 — API is under load and slow to respond (ReadTimeout).
|
|
59
|
-
#
|
|
60
|
-
#
|
|
58
|
+
# read=8 — API is under load and slow to respond (ReadTimeout), i.e. it did
|
|
59
|
+
# not answer within DEFAULT_REST_TIMEOUT_SEC. Safe to retry because
|
|
60
|
+
# all endpoints are idempotent (bulk ingest is upsert-based,
|
|
61
|
+
# status/findings updates are set-operations) — but note that a
|
|
62
|
+
# retry re-uploads the whole body, which is why that budget is
|
|
63
|
+
# generous rather than tight.
|
|
61
64
|
# status=8 — transient HTTP errors from an overloaded or restarting API:
|
|
62
65
|
# 408 Request Timeout - API-level timeout
|
|
63
66
|
# 429 Too Many Requests - rate-limited / backpressure
|
|
@@ -76,8 +79,7 @@ class _JitteredRetry(Retry):
|
|
|
76
79
|
# attempt 7 → ~60 s (capped)
|
|
77
80
|
# attempt 8 → ~60 s (capped)
|
|
78
81
|
# Total extra wait: ~182 s (~3 min) — covers extended load spikes on a
|
|
79
|
-
# single-node VPS before event-loop pressure drops.
|
|
80
|
-
# call costs 8 * 120 s + 182 s = ~18 min, acceptable for long-running scans.
|
|
82
|
+
# single-node VPS before event-loop pressure drops.
|
|
81
83
|
#
|
|
82
84
|
# POST and PATCH are explicitly allowed: without this urllib3 only retries
|
|
83
85
|
# idempotent methods (GET/HEAD) by default.
|
|
@@ -161,6 +163,12 @@ class ExternalRunnerResponse(BaseModel):
|
|
|
161
163
|
source_id: str = Field(validation_alias="sourceId")
|
|
162
164
|
|
|
163
165
|
|
|
166
|
+
# Applied to connection establishment only, kept short so a refused or
|
|
167
|
+
# unreachable API fails fast and is retried by _RETRY_POLICY. How long the API
|
|
168
|
+
# may then take to answer is a separate budget (timeout_sec, 5 min by default).
|
|
169
|
+
_CONNECT_TIMEOUT_SEC = 30
|
|
170
|
+
|
|
171
|
+
|
|
164
172
|
class RestOutputSink:
|
|
165
173
|
output_type: OutputType = "rest"
|
|
166
174
|
|
|
@@ -169,7 +177,7 @@ class RestOutputSink:
|
|
|
169
177
|
context: OutputRuntimeContext,
|
|
170
178
|
*,
|
|
171
179
|
base_url: str,
|
|
172
|
-
timeout_sec: int,
|
|
180
|
+
timeout_sec: int | None,
|
|
173
181
|
):
|
|
174
182
|
self.context = context
|
|
175
183
|
self.batch_size = context.batch_size
|
|
@@ -443,7 +451,9 @@ class RestOutputSink:
|
|
|
443
451
|
method=method,
|
|
444
452
|
url=url,
|
|
445
453
|
json=payload,
|
|
446
|
-
|
|
454
|
+
# (connect, read): the read budget is the API's whole processing
|
|
455
|
+
# time, not just the first byte. None waits indefinitely.
|
|
456
|
+
timeout=(_CONNECT_TIMEOUT_SEC, self.timeout_sec),
|
|
447
457
|
)
|
|
448
458
|
|
|
449
459
|
if response.status_code >= 400:
|
|
@@ -10,6 +10,7 @@ from urllib.parse import quote
|
|
|
10
10
|
import requests
|
|
11
11
|
|
|
12
12
|
from ...models.generated_input import SandboxInput
|
|
13
|
+
from ...outputs.base import DEFAULT_REST_TIMEOUT_SEC
|
|
13
14
|
from ..object_storage.base import ObjectRef, ObjectStorageSourceBase
|
|
14
15
|
|
|
15
16
|
logger = logging.getLogger(__name__)
|
|
@@ -48,15 +49,19 @@ class SandboxSource(ObjectStorageSourceBase):
|
|
|
48
49
|
return default
|
|
49
50
|
|
|
50
51
|
def _max_object_bytes(self) -> int:
|
|
51
|
-
#
|
|
52
|
-
#
|
|
52
|
+
# Spool threshold (RAM before spilling to disk), not a refusal ceiling —
|
|
53
|
+
# uploads are no longer size-capped. Raised above the object-storage
|
|
54
|
+
# family's conservative 5 MiB default because these files are local.
|
|
53
55
|
return 50 * 1024 * 1024
|
|
54
56
|
|
|
55
57
|
def _request(self, method: str, path: str, *, stream: bool = False) -> requests.Response:
|
|
56
58
|
response = self._session.request(
|
|
57
59
|
method,
|
|
58
60
|
f"{self._api_url}{path}",
|
|
59
|
-
|
|
61
|
+
# (connect, read). The read budget applies per socket read, so 5
|
|
62
|
+
# minutes is generous for a streamed download of any size; the old
|
|
63
|
+
# flat 120 s applied to connect and read alike.
|
|
64
|
+
timeout=(30, DEFAULT_REST_TIMEOUT_SEC),
|
|
60
65
|
stream=stream,
|
|
61
66
|
headers={"Connection": "close"},
|
|
62
67
|
)
|
{classifyre_cli-0.4.85 → classifyre_cli-0.4.87}/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):
|