howler-api 4.1.0.dev1405__tar.gz → 4.1.0.dev1430__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.
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/PKG-INFO +1 -1
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/actions/transition.py +32 -10
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/api/v1/hit.py +19 -14
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/cronjobs/action_queue_worker.py +6 -3
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/datastore/collection.py +4 -0
- howler_api-4.1.0.dev1430/howler/external/check_duplicates.py +152 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/services/correlation_service.py +1 -1
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/services/hit_service.py +31 -16
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/pyproject.toml +1 -1
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/README.md +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/__init__.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/actions/__init__.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/actions/add_label.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/actions/add_to_bundle.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/actions/add_to_case.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/actions/change_field.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/actions/demote.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/actions/example_plugin.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/actions/prioritization.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/actions/promote.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/actions/remove_from_bundle.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/actions/remove_label.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/api/__init__.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/api/base.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/api/socket.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/api/v1/__init__.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/api/v1/action.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/api/v1/analytic.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/api/v1/auth.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/api/v1/clue.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/api/v1/configs.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/api/v1/dossier.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/api/v1/help.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/api/v1/notebook.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/api/v1/overview.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/api/v1/search.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/api/v1/template.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/api/v1/tool.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/api/v1/user.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/api/v1/utils/__init__.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/api/v1/utils/etag.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/api/v1/utils/params.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/api/v1/view.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/api/v2/__init__.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/api/v2/case.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/api/v2/fuzzy.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/api/v2/ingest.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/api/v2/search.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/app.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/common/README.md +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/common/__init__.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/common/classification.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/common/classification.yml +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/common/exceptions.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/common/loader.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/common/logging/__init__.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/common/logging/audit.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/common/logging/format.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/common/net.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/common/net_static.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/common/random_user.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/common/swagger.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/config.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/cronjobs/__init__.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/cronjobs/correlation.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/cronjobs/retention.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/cronjobs/view_cleanup.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/datastore/README.md +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/datastore/__init__.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/datastore/bulk.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/datastore/constants.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/datastore/exceptions.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/datastore/howler_store.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/datastore/migrations/fix_process.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/datastore/operations.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/datastore/schemas.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/datastore/store.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/datastore/support/__init__.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/datastore/support/build.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/datastore/support/schemas.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/datastore/types.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/datastore/utils.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/error.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/external/README.md +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/external/__init__.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/external/generate_mitre.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/external/generate_sigma_rules.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/external/generate_tlds.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/external/reindex_data.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/external/wipe_databases.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/gunicorn_config.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/healthz.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/helper/__init__.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/helper/azure.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/helper/discover.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/helper/hit.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/helper/oauth.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/helper/search.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/helper/workflow.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/helper/ws.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/README.md +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/__init__.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/base.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/charter.txt +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/constants.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/helper.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/howler_enum.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/mixins.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/__init__.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/action.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/analytic.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/assemblyline.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/aws.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/azure.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/case.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/cbs.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/clue.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/config.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/dossier.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/__init__.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/agent.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/autonomous_system.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/client.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/cloud.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/code_signature.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/container.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/dns.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/egress.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/elf.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/email.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/error.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/event.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/faas.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/file.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/geo.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/group.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/hash.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/host.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/http.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/ingress.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/interface.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/network.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/observer.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/organization.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/os.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/pe.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/process.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/registry.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/related.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/rule.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/server.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/threat.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/tls.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/url.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/user.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/user_agent.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/ecs/vulnerability.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/event.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/gcp.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/hit.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/howler_data.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/lead.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/localized_label.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/overview.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/pivot.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/record.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/template.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/user.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/models/view.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/random_data.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/odm/randomizer.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/patched.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/plugins/__init__.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/plugins/config.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/remote/__init__.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/remote/datatypes/README.md +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/remote/datatypes/__init__.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/remote/datatypes/counters.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/remote/datatypes/events.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/remote/datatypes/hash.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/remote/datatypes/lock.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/remote/datatypes/queues/__init__.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/remote/datatypes/queues/comms.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/remote/datatypes/queues/multi.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/remote/datatypes/queues/named.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/remote/datatypes/queues/priority.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/remote/datatypes/set.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/remote/datatypes/user_quota_tracker.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/security/__init__.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/security/socket.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/security/utils.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/services/__init__.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/services/action_service.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/services/analytic_service.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/services/auth_service.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/services/bundle_compat_service.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/services/case_service.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/services/comms_service.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/services/config_service.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/services/docs_service.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/services/dossier_service.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/services/event_service.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/services/fuzzy_service.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/services/jwt_service.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/services/lucene_service.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/services/notebook_service.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/services/overview_service.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/services/search_service.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/services/template_service.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/services/user_service.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/services/viewer_service.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/telemetry.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/utils/__init__.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/utils/annotations.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/utils/chunk.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/utils/compat.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/utils/constants.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/utils/dict_utils.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/utils/indexes.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/utils/isotime.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/utils/list_utils.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/utils/lucene.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/utils/net_utils.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/utils/path.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/utils/socket_utils.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/utils/str_utils.py +0 -0
- {howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/utils/uid.py +0 -0
|
@@ -5,6 +5,7 @@ from howler.actions import check_hit_limit
|
|
|
5
5
|
from howler.common.exceptions import InvalidDataException, NotFoundException
|
|
6
6
|
from howler.common.loader import datastore
|
|
7
7
|
from howler.common.logging import get_logger
|
|
8
|
+
from howler.datastore.exceptions import VersionConflictException
|
|
8
9
|
from howler.helper.workflow import Workflow, WorkflowException
|
|
9
10
|
from howler.odm.models.action import VALID_TRIGGERS
|
|
10
11
|
from howler.odm.models.howler_data import (
|
|
@@ -20,11 +21,39 @@ from howler.utils.list_utils import flatten_list
|
|
|
20
21
|
OPERATION_ID = "transition"
|
|
21
22
|
MAX_HITS_BASIC = 10
|
|
22
23
|
MAX_HITS_ADVANCED = 1000
|
|
24
|
+
MAX_VERSION_CONFLICT_ATTEMPTS = 3
|
|
23
25
|
SKIP_CENTRAL_LIMIT = True # This operation transforms the query, handles limit check locally
|
|
24
26
|
|
|
25
27
|
log = get_logger(__file__)
|
|
26
28
|
|
|
27
29
|
|
|
30
|
+
def _transition_failure_report(hit_id: str, error: Exception) -> dict[str, str]:
|
|
31
|
+
if isinstance(error, VersionConflictException):
|
|
32
|
+
return {
|
|
33
|
+
"query": f"howler.id:{hit_id}",
|
|
34
|
+
"outcome": "error",
|
|
35
|
+
"title": "Version Conflict",
|
|
36
|
+
"message": "The hit was modified while this transition was running.",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
"query": f"howler.id:{hit_id}",
|
|
41
|
+
"outcome": "error",
|
|
42
|
+
"title": "An error occurred while processing.",
|
|
43
|
+
"message": str(error),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _transition_hit_with_retry(hit_id: str, transition: HitStatusTransition, user: User, **kwargs) -> None:
|
|
48
|
+
for attempt in range(MAX_VERSION_CONFLICT_ATTEMPTS):
|
|
49
|
+
try:
|
|
50
|
+
hit_service.transition_hit(hit_id, transition, user, **kwargs)
|
|
51
|
+
return
|
|
52
|
+
except VersionConflictException:
|
|
53
|
+
if attempt == MAX_VERSION_CONFLICT_ATTEMPTS - 1:
|
|
54
|
+
raise
|
|
55
|
+
|
|
56
|
+
|
|
28
57
|
def __parse_workflow_actions(workflow: Workflow) -> dict[str, set[str]]:
|
|
29
58
|
"""Take in a workflow, and parse the steps and transitions of that workflow into a format understood by the UI"""
|
|
30
59
|
parsed_args: dict[str, set[str]] = {}
|
|
@@ -126,22 +155,15 @@ def execute(
|
|
|
126
155
|
total_processed = 0
|
|
127
156
|
for hit_id in ids:
|
|
128
157
|
try:
|
|
129
|
-
|
|
158
|
+
_transition_hit_with_retry(
|
|
130
159
|
hit_id,
|
|
131
160
|
cast(HitStatusTransition, HitStatusTransition[transition]),
|
|
132
161
|
user,
|
|
133
162
|
**kwargs,
|
|
134
163
|
)
|
|
135
164
|
success_ids.add(hit_id)
|
|
136
|
-
except (InvalidDataException, NotFoundException, WorkflowException) as e:
|
|
137
|
-
report.append(
|
|
138
|
-
{
|
|
139
|
-
"query": f"howler.id:{hit_id}",
|
|
140
|
-
"outcome": "error",
|
|
141
|
-
"title": "An error occurred while processing.",
|
|
142
|
-
"message": str(e),
|
|
143
|
-
}
|
|
144
|
-
)
|
|
165
|
+
except (InvalidDataException, NotFoundException, VersionConflictException, WorkflowException) as e:
|
|
166
|
+
report.append(_transition_failure_report(hit_id, e))
|
|
145
167
|
|
|
146
168
|
total_processed += 1
|
|
147
169
|
if total_processed % 10 == 0:
|
|
@@ -528,7 +528,7 @@ def get_assigned_hits(user, **kwargs):
|
|
|
528
528
|
@api_login(audit=False, required_priv=["W"])
|
|
529
529
|
@add_etag(getter=hit_service.get_hit, check_if_match=False)
|
|
530
530
|
@parse_parameters(refresh=parse_refresh)
|
|
531
|
-
def add_label(id, label_set, user, **kwargs):
|
|
531
|
+
def add_label(id: str, label_set: str, user: User, server_version: str | None = None, **kwargs):
|
|
532
532
|
"""Add labels to a hit.
|
|
533
533
|
|
|
534
534
|
Variables:
|
|
@@ -575,7 +575,8 @@ def add_label(id, label_set, user, **kwargs):
|
|
|
575
575
|
hit_service.update_hit(
|
|
576
576
|
id,
|
|
577
577
|
[hit_helper.list_add(f"howler.labels.{label_set}", label) for label in labels],
|
|
578
|
-
user
|
|
578
|
+
user.uname,
|
|
579
|
+
version=server_version,
|
|
579
580
|
refresh="true" if DEBUG_FORCE_REFRESH else refresh,
|
|
580
581
|
)
|
|
581
582
|
|
|
@@ -591,7 +592,7 @@ def add_label(id, label_set, user, **kwargs):
|
|
|
591
592
|
@api_login(audit=False, required_priv=["W"])
|
|
592
593
|
@add_etag(getter=hit_service.get_hit, check_if_match=False)
|
|
593
594
|
@parse_parameters(refresh=parse_refresh)
|
|
594
|
-
def remove_labels(id, label_set, user, **kwargs):
|
|
595
|
+
def remove_labels(id: str, label_set: str, user: User, server_version: str | None = None, **kwargs):
|
|
595
596
|
"""Remove labels from a hit.
|
|
596
597
|
|
|
597
598
|
Variables:
|
|
@@ -632,7 +633,8 @@ def remove_labels(id, label_set, user, **kwargs):
|
|
|
632
633
|
hit_service.update_hit(
|
|
633
634
|
id,
|
|
634
635
|
[hit_helper.list_remove(f"howler.labels.{label_set}", label) for label in labels],
|
|
635
|
-
user
|
|
636
|
+
user.uname,
|
|
637
|
+
version=server_version,
|
|
636
638
|
refresh="true" if DEBUG_FORCE_REFRESH else refresh,
|
|
637
639
|
)
|
|
638
640
|
|
|
@@ -648,7 +650,7 @@ def remove_labels(id, label_set, user, **kwargs):
|
|
|
648
650
|
@api_login(audit=False, required_priv=["W"])
|
|
649
651
|
@add_etag(getter=hit_service.get_hit, check_if_match=True)
|
|
650
652
|
@parse_parameters(refresh=parse_refresh)
|
|
651
|
-
def transition(id: str, user: User, **kwargs):
|
|
653
|
+
def transition(id: str, user: User, server_version: str | None = None, **kwargs):
|
|
652
654
|
"""Transition a hit
|
|
653
655
|
|
|
654
656
|
Variables:
|
|
@@ -679,11 +681,12 @@ def transition(id: str, user: User, **kwargs):
|
|
|
679
681
|
return bad_request(err="Invalid data format")
|
|
680
682
|
|
|
681
683
|
transition = transition_data["transition"]
|
|
684
|
+
version: str | None = None
|
|
682
685
|
if "If-Match" in request.headers:
|
|
683
686
|
version = request.headers["If-Match"]
|
|
684
687
|
else:
|
|
685
688
|
logger.warning("User is mising version - no If-Match header in request.")
|
|
686
|
-
version =
|
|
689
|
+
version = server_version
|
|
687
690
|
|
|
688
691
|
try:
|
|
689
692
|
if transition not in HitStatusTransition.list():
|
|
@@ -694,7 +697,7 @@ def transition(id: str, user: User, **kwargs):
|
|
|
694
697
|
)
|
|
695
698
|
)
|
|
696
699
|
|
|
697
|
-
hit_service.transition_hit(
|
|
700
|
+
hit, version = hit_service.transition_hit(
|
|
698
701
|
id, transition, user, version, refresh=refresh, **kwargs, **transition_data.get("data", {})
|
|
699
702
|
)
|
|
700
703
|
except (WorkflowException, DataStoreException, InvalidDataException) as e:
|
|
@@ -704,7 +707,6 @@ def transition(id: str, user: User, **kwargs):
|
|
|
704
707
|
except HowlerException as e:
|
|
705
708
|
return internal_error(err=str(e))
|
|
706
709
|
|
|
707
|
-
hit, version = hit_service.get_hit(id, as_odm=False, version=True)
|
|
708
710
|
return ok(hit), version
|
|
709
711
|
|
|
710
712
|
|
|
@@ -712,7 +714,7 @@ def transition(id: str, user: User, **kwargs):
|
|
|
712
714
|
@hit_api.route("/<id>/comments/<comment_id>", methods=["GET"])
|
|
713
715
|
@api_login(audit=False, required_priv=["R"])
|
|
714
716
|
@add_etag(getter=hit_service.get_hit, check_if_match=False)
|
|
715
|
-
def get_comment(id: str, comment_id: str, user: User, server_version: str, **kwargs):
|
|
717
|
+
def get_comment(id: str, comment_id: str, user: User, server_version: str | None = None, **kwargs):
|
|
716
718
|
"""Get a comment associated with a particular hit
|
|
717
719
|
|
|
718
720
|
Variables:
|
|
@@ -741,7 +743,7 @@ def get_comment(id: str, comment_id: str, user: User, server_version: str, **kwa
|
|
|
741
743
|
@hit_api.route("/<id>/comments", methods=["POST"])
|
|
742
744
|
@api_login(audit=False, required_priv=["W"])
|
|
743
745
|
@add_etag(getter=hit_service.get_hit, check_if_match=False)
|
|
744
|
-
def add_comment(id: str, user:
|
|
746
|
+
def add_comment(id: str, user: User, server_version: str | None = None, **kwargs):
|
|
745
747
|
"""Add a comment
|
|
746
748
|
|
|
747
749
|
Variables:
|
|
@@ -781,12 +783,13 @@ def add_comment(id: str, user: dict[str, Any], **kwargs):
|
|
|
781
783
|
[
|
|
782
784
|
hit_helper.list_add(
|
|
783
785
|
"howler.comment",
|
|
784
|
-
Comment({"user": user
|
|
786
|
+
Comment({"user": user.uname, "value": comment_value}),
|
|
785
787
|
explanation=f"Added a comment:\n\n{comment_value}",
|
|
786
788
|
if_missing=True,
|
|
787
789
|
),
|
|
788
790
|
],
|
|
789
|
-
user
|
|
791
|
+
user.uname,
|
|
792
|
+
version=server_version,
|
|
790
793
|
)
|
|
791
794
|
except DataStoreException as e:
|
|
792
795
|
return bad_request(err=str(e))
|
|
@@ -800,7 +803,7 @@ def add_comment(id: str, user: dict[str, Any], **kwargs):
|
|
|
800
803
|
@hit_api.route("/<id>/comments/<comment_id>", methods=["PUT"])
|
|
801
804
|
@api_login(audit=False, required_priv=["W"])
|
|
802
805
|
@add_etag(getter=hit_service.get_hit, check_if_match=False)
|
|
803
|
-
def edit_comment(id: str, comment_id: str, user: dict[str, Any], **kwargs):
|
|
806
|
+
def edit_comment(id: str, comment_id: str, user: dict[str, Any], server_version: str | None = None, **kwargs):
|
|
804
807
|
"""Edit a comment
|
|
805
808
|
|
|
806
809
|
Variables:
|
|
@@ -865,6 +868,7 @@ def edit_comment(id: str, comment_id: str, user: dict[str, Any], **kwargs):
|
|
|
865
868
|
),
|
|
866
869
|
],
|
|
867
870
|
user["uname"],
|
|
871
|
+
version=server_version,
|
|
868
872
|
)
|
|
869
873
|
|
|
870
874
|
return ok(new_hit), version
|
|
@@ -874,7 +878,7 @@ def edit_comment(id: str, comment_id: str, user: dict[str, Any], **kwargs):
|
|
|
874
878
|
@hit_api.route("/<id>/comments", methods=["DELETE"])
|
|
875
879
|
@api_login(audit=False, required_priv=["W"])
|
|
876
880
|
@add_etag(getter=hit_service.get_hit, check_if_match=False)
|
|
877
|
-
def delete_comments(id: str, user: User, **kwargs):
|
|
881
|
+
def delete_comments(id: str, user: User, server_version: str | None = None, **kwargs):
|
|
878
882
|
"""Delete a set of comments
|
|
879
883
|
|
|
880
884
|
Variables:
|
|
@@ -923,6 +927,7 @@ def delete_comments(id: str, user: User, **kwargs):
|
|
|
923
927
|
for comment in comments
|
|
924
928
|
],
|
|
925
929
|
user.uname,
|
|
930
|
+
version=server_version,
|
|
926
931
|
)
|
|
927
932
|
|
|
928
933
|
except DataStoreException as e:
|
{howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/cronjobs/action_queue_worker.py
RENAMED
|
@@ -50,15 +50,18 @@ def run_worker(trigger: str) -> None: # pragma: no cover – long-running loop,
|
|
|
50
50
|
if item is not None:
|
|
51
51
|
batch.append(item)
|
|
52
52
|
|
|
53
|
+
if len(batch) % 10 == 0 or len(batch) >= BATCH_SIZE:
|
|
54
|
+
logger.info("Batch size: %s", len(batch))
|
|
55
|
+
|
|
53
56
|
if len(batch) >= BATCH_SIZE or (item is None and batch):
|
|
57
|
+
finalized_batch = [*batch]
|
|
58
|
+
batch = []
|
|
54
59
|
logger.debug("Processing action batch of %d item(s) for trigger=%s", len(batch), trigger)
|
|
55
60
|
try:
|
|
56
|
-
process_action_batch(trigger,
|
|
61
|
+
process_action_batch(trigger, finalized_batch)
|
|
57
62
|
logger.info("Action batch complete: %d item(s) processed for trigger=%s", len(batch), trigger)
|
|
58
63
|
except Exception:
|
|
59
64
|
logger.exception("Error processing action batch for trigger=%s", trigger)
|
|
60
|
-
finally:
|
|
61
|
-
batch = []
|
|
62
65
|
except Exception:
|
|
63
66
|
logger.exception("Unexpected error in action queue worker loop for trigger=%s", trigger)
|
|
64
67
|
|
|
@@ -1613,6 +1613,10 @@ class ESCollection(Generic[ModelType]):
|
|
|
1613
1613
|
index = self.name
|
|
1614
1614
|
seq_no = None
|
|
1615
1615
|
primary_term = None
|
|
1616
|
+
|
|
1617
|
+
if version is None and self.ilm_config:
|
|
1618
|
+
_, version = self.get_if_exists(key, as_obj=False, version=True)
|
|
1619
|
+
|
|
1616
1620
|
if version:
|
|
1617
1621
|
index, seq_no, primary_term = self._get_version_write_target(version)
|
|
1618
1622
|
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Find and optionally remove duplicate documents from an ILM collection."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import heapq
|
|
5
|
+
import sys
|
|
6
|
+
from collections.abc import Iterator
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def iter_index_ids(collection: Any, index: str, batch_size: int) -> Iterator[str]:
|
|
11
|
+
"""Yield document IDs from *index* in sorted order without retaining prior pages."""
|
|
12
|
+
search_after = None
|
|
13
|
+
|
|
14
|
+
while True:
|
|
15
|
+
search_args = {
|
|
16
|
+
"index": index,
|
|
17
|
+
"query": {"match_all": {}},
|
|
18
|
+
"sort": [{"id": "asc"}],
|
|
19
|
+
"size": batch_size,
|
|
20
|
+
"_source": False,
|
|
21
|
+
"track_total_hits": False,
|
|
22
|
+
}
|
|
23
|
+
if search_after is not None:
|
|
24
|
+
search_args["search_after"] = search_after
|
|
25
|
+
|
|
26
|
+
response = collection.with_retries(collection.datastore.client.search, **search_args)
|
|
27
|
+
hits = response["hits"]["hits"]
|
|
28
|
+
if not hits:
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
for hit in hits:
|
|
32
|
+
yield hit["_id"]
|
|
33
|
+
|
|
34
|
+
search_after = hits[-1]["sort"]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _push_next_id(heap: list[tuple[str, str, Iterator[str]]], index: str, document_ids: Iterator[str]) -> None:
|
|
38
|
+
try:
|
|
39
|
+
heapq.heappush(heap, (next(document_ids), index, document_ids))
|
|
40
|
+
except StopIteration:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def iter_duplicates(collection: Any, indexes: list[str], batch_size: int) -> Iterator[tuple[str, list[str]]]:
|
|
45
|
+
"""Yield each duplicated ID and the physical indexes that contain it.
|
|
46
|
+
|
|
47
|
+
This is a k-way merge over paginated, sorted searches. Its memory use is bounded by
|
|
48
|
+
the number of physical indexes and one Elasticsearch result page per active iterator.
|
|
49
|
+
"""
|
|
50
|
+
heap: list[tuple[str, str, Iterator[str]]] = []
|
|
51
|
+
for index in indexes:
|
|
52
|
+
document_ids = iter_index_ids(collection, index, batch_size)
|
|
53
|
+
_push_next_id(heap, index, document_ids)
|
|
54
|
+
|
|
55
|
+
while heap:
|
|
56
|
+
document_id, index, document_ids = heapq.heappop(heap)
|
|
57
|
+
duplicate_indexes = [index]
|
|
58
|
+
_push_next_id(heap, index, document_ids)
|
|
59
|
+
|
|
60
|
+
while heap and heap[0][0] == document_id:
|
|
61
|
+
_, index, document_ids = heapq.heappop(heap)
|
|
62
|
+
duplicate_indexes.append(index)
|
|
63
|
+
_push_next_id(heap, index, document_ids)
|
|
64
|
+
|
|
65
|
+
if len(duplicate_indexes) > 1:
|
|
66
|
+
yield document_id, duplicate_indexes
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def delete_duplicates(collection: Any, document_id: str, indexes: list[str]) -> int:
|
|
70
|
+
"""Delete all supplied physical-index copies of a document ID."""
|
|
71
|
+
deleted = 0
|
|
72
|
+
for index in indexes:
|
|
73
|
+
response = collection.with_retries(collection.datastore.client.delete, index=index, id=document_id)
|
|
74
|
+
if response["result"] == "deleted":
|
|
75
|
+
deleted += 1
|
|
76
|
+
return deleted
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def main() -> int:
|
|
80
|
+
"""Run the interactive duplicate scan for an ILM collection."""
|
|
81
|
+
parser = argparse.ArgumentParser(
|
|
82
|
+
description="Find duplicate document IDs across an ILM collection's physical indexes."
|
|
83
|
+
)
|
|
84
|
+
parser.add_argument("collection", help="Collection name registered in the datastore.")
|
|
85
|
+
parser.add_argument(
|
|
86
|
+
"--batch-size",
|
|
87
|
+
type=int,
|
|
88
|
+
default=1_000,
|
|
89
|
+
help="IDs fetched from each physical index per Elasticsearch request (default: 1000).",
|
|
90
|
+
)
|
|
91
|
+
args = parser.parse_args()
|
|
92
|
+
|
|
93
|
+
if args.batch_size <= 0:
|
|
94
|
+
parser.error("--batch-size must be a positive integer.")
|
|
95
|
+
|
|
96
|
+
from howler.datastore.collection import ESCollection
|
|
97
|
+
|
|
98
|
+
# Checking duplicates must not create collections or alter their ILM configuration.
|
|
99
|
+
ESCollection.IGNORE_ENSURE_COLLECTION = True
|
|
100
|
+
ESCollection.ENSURE_COLLECTION_WARNED = True
|
|
101
|
+
|
|
102
|
+
from howler.common import loader
|
|
103
|
+
|
|
104
|
+
datastore = loader.datastore(archive_access=False)
|
|
105
|
+
collection_names = datastore.ds.get_models()
|
|
106
|
+
if args.collection not in collection_names:
|
|
107
|
+
parser.error(
|
|
108
|
+
f"Unknown collection '{args.collection}'. Valid collections: {', '.join(sorted(collection_names))}"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
collection = getattr(datastore, args.collection)
|
|
112
|
+
if collection.ilm_config is None:
|
|
113
|
+
print(f"Collection '{args.collection}' is not ILM-enabled; nothing to check.")
|
|
114
|
+
return 0
|
|
115
|
+
|
|
116
|
+
collection._refresh_ilm_index_name()
|
|
117
|
+
indexes = collection.index_list_full
|
|
118
|
+
if len(indexes) < 2:
|
|
119
|
+
print(f"Collection '{args.collection}' has fewer than two physical ILM indexes; nothing to check.")
|
|
120
|
+
return 0
|
|
121
|
+
|
|
122
|
+
print(f"Scanning for duplicate IDs in: {', '.join(indexes)}")
|
|
123
|
+
duplicates_found = 0
|
|
124
|
+
copies_deleted = 0
|
|
125
|
+
index_order = {index: position for position, index in enumerate(indexes)}
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
for document_id, duplicate_indexes in iter_duplicates(collection, indexes, args.batch_size):
|
|
129
|
+
duplicates_found += 1
|
|
130
|
+
keep_index = min(duplicate_indexes, key=index_order.__getitem__)
|
|
131
|
+
stale_indexes = [index for index in duplicate_indexes if index != keep_index]
|
|
132
|
+
print(
|
|
133
|
+
f"Duplicate ID '{document_id}' found in {', '.join(duplicate_indexes)}. "
|
|
134
|
+
f"Keeping the newest copy in '{keep_index}'."
|
|
135
|
+
)
|
|
136
|
+
answer = input(f"Delete copies from {', '.join(stale_indexes)}? [yes/NO] ")
|
|
137
|
+
if answer.lower() in {"y", "yes"}:
|
|
138
|
+
deleted_count = delete_duplicates(collection, document_id, stale_indexes)
|
|
139
|
+
copies_deleted += deleted_count
|
|
140
|
+
print(f"Deleted {deleted_count} copy/copies of '{document_id}'.")
|
|
141
|
+
except KeyboardInterrupt:
|
|
142
|
+
print("\nInterrupted; all completed deletions have been retained.", file=sys.stderr)
|
|
143
|
+
return 130
|
|
144
|
+
finally:
|
|
145
|
+
datastore.ds.close()
|
|
146
|
+
|
|
147
|
+
print(f"Scan complete. Found {duplicates_found} duplicate ID(s); deleted {copies_deleted} stale copy/copies.")
|
|
148
|
+
return 0
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
if __name__ == "__main__":
|
|
152
|
+
sys.exit(main())
|
{howler_api-4.1.0.dev1405 → howler_api-4.1.0.dev1430}/howler/services/correlation_service.py
RENAMED
|
@@ -345,7 +345,7 @@ def run_worker() -> None: # pragma: no cover – long-running loop, tested via
|
|
|
345
345
|
if item is not None:
|
|
346
346
|
batch.append(item)
|
|
347
347
|
|
|
348
|
-
if len(batch)
|
|
348
|
+
if len(batch) % 10 == 0 or len(batch) >= BATCH_SIZE:
|
|
349
349
|
logger.info("Batch size: %s", len(batch))
|
|
350
350
|
|
|
351
351
|
if len(batch) >= BATCH_SIZE or (item is None and batch):
|
|
@@ -549,7 +549,7 @@ def _update_hit(
|
|
|
549
549
|
user: Optional[str] = None,
|
|
550
550
|
version: Optional[str] = None,
|
|
551
551
|
refresh: str | None = None,
|
|
552
|
-
) -> tuple[
|
|
552
|
+
) -> tuple[Hit | None, str | None]:
|
|
553
553
|
"""Internal function to update a hit with proper logging and event emission.
|
|
554
554
|
|
|
555
555
|
This function applies update operations to a hit, automatically adding worklog entries
|
|
@@ -573,7 +573,11 @@ def _update_hit(
|
|
|
573
573
|
if user and not isinstance(user, str):
|
|
574
574
|
raise HowlerValueError("User must be of type string")
|
|
575
575
|
|
|
576
|
-
|
|
576
|
+
if version is None:
|
|
577
|
+
fetched_hit, version = datastore().hit.get(hit_id, as_obj=True, version=True)
|
|
578
|
+
current_hit = cast(Hit, fetched_hit)
|
|
579
|
+
else:
|
|
580
|
+
current_hit = cast(Hit, get_hit(hit_id, as_odm=True))
|
|
577
581
|
|
|
578
582
|
for operation in operations:
|
|
579
583
|
if not operation:
|
|
@@ -590,15 +594,16 @@ def _update_hit(
|
|
|
590
594
|
is_list = current_hit.flat_fields()[key].multivalued
|
|
591
595
|
previous_value = "list"
|
|
592
596
|
|
|
593
|
-
operation_type = ""
|
|
594
597
|
if is_list:
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
598
|
+
operation_type = (
|
|
599
|
+
HitOperationType.APPENDED
|
|
600
|
+
if operation.operation
|
|
601
|
+
in (
|
|
602
|
+
ESCollection.UPDATE_APPEND,
|
|
603
|
+
ESCollection.UPDATE_APPEND_IF_MISSING,
|
|
604
|
+
)
|
|
605
|
+
else HitOperationType.REMOVED
|
|
606
|
+
)
|
|
602
607
|
else:
|
|
603
608
|
operation_type = HitOperationType.SET
|
|
604
609
|
|
|
@@ -625,9 +630,9 @@ def _update_hit(
|
|
|
625
630
|
|
|
626
631
|
datastore().hit.update(hit_id, final_operations, version, refresh=refresh)
|
|
627
632
|
# Need to fetch the new data of the hit for the comms_service
|
|
628
|
-
data, _version = datastore().hit.get(hit_id, as_obj=
|
|
633
|
+
data, _version = datastore().hit.get(hit_id, as_obj=True, version=True) or (None, None)
|
|
629
634
|
if data and _version:
|
|
630
|
-
comms_service.emit("hits", {"hit": data, "version": _version})
|
|
635
|
+
comms_service.emit("hits", {"hit": data.as_primitives(), "version": _version})
|
|
631
636
|
|
|
632
637
|
return data, _version
|
|
633
638
|
|
|
@@ -652,7 +657,7 @@ def transition_hit(
|
|
|
652
657
|
version: Optional[str] = None,
|
|
653
658
|
refresh: str | None = None,
|
|
654
659
|
**kwargs,
|
|
655
|
-
):
|
|
660
|
+
) -> tuple[Hit | None, str | None]:
|
|
656
661
|
"""Transition a hit from one status to another while updating related properties.
|
|
657
662
|
|
|
658
663
|
For certain transitions (PROMOTE, DEMOTE, ASSESS, RE_EVALUATE), it also executes bulk actions and emits events.
|
|
@@ -664,6 +669,9 @@ def transition_hit(
|
|
|
664
669
|
version: Optional version to validate against. The transition will not run if the version doesn't match.
|
|
665
670
|
**kwargs: Additional arguments including potential 'hit' object and 'assessment' value
|
|
666
671
|
|
|
672
|
+
Returns:
|
|
673
|
+
A tuple containing the updated hit and the optimistic-lock version token after the transition.
|
|
674
|
+
|
|
667
675
|
Raises:
|
|
668
676
|
NotFoundException: If the hit does not exist
|
|
669
677
|
"""
|
|
@@ -684,10 +692,12 @@ def transition_hit(
|
|
|
684
692
|
|
|
685
693
|
# Apply the workflow transition to get required updates
|
|
686
694
|
updates = workflow.transition(hit_status, transition, user=user, hit=hit, **kwargs)
|
|
695
|
+
new_version = version
|
|
687
696
|
|
|
688
697
|
# Apply updates if any were generated by the workflow
|
|
698
|
+
updated_hit: Hit | None = None
|
|
689
699
|
if updates:
|
|
690
|
-
_update_hit(hit_id, updates, user.uname, version=version, refresh=refresh)
|
|
700
|
+
updated_hit, new_version = _update_hit(hit_id, updates, user.uname, version=version, refresh=refresh)
|
|
691
701
|
|
|
692
702
|
# Execute bulk actions for transitions that require them
|
|
693
703
|
# These transitions need additional processing beyond the workflow
|
|
@@ -720,8 +730,13 @@ def transition_hit(
|
|
|
720
730
|
action_service.enqueue_action_execution([hit_id], trigger=trigger, user=user)
|
|
721
731
|
|
|
722
732
|
# Emit events for processed hit to notify other systems
|
|
723
|
-
|
|
724
|
-
comms_service.emit(
|
|
733
|
+
updated_hit, new_version = datastore().hit.get(hit_id, as_obj=True, version=True)
|
|
734
|
+
comms_service.emit(
|
|
735
|
+
"hits", {"hit": updated_hit.as_primitives() if updated_hit else None, "version": new_version}
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
updated_hit, new_version = datastore().hit.get(hit_id, as_obj=True, version=True)
|
|
739
|
+
return updated_hit, new_version
|
|
725
740
|
|
|
726
741
|
|
|
727
742
|
DELETED_HITS = Counter(f"{APP_NAME.replace('-', '_')}_deleted_hits_total", "The number of deleted hits")
|
|
@@ -152,7 +152,7 @@ suppress-none-returning = true
|
|
|
152
152
|
[tool.poetry]
|
|
153
153
|
package-mode = true
|
|
154
154
|
name = "howler-api"
|
|
155
|
-
version = "4.1.0.
|
|
155
|
+
version = "4.1.0.dev1430"
|
|
156
156
|
description = "Howler - API server"
|
|
157
157
|
authors = [
|
|
158
158
|
"Canadian Centre for Cyber Security <howler@cyber.gc.ca>",
|
|
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
|
|
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
|