howler-api 4.0.0.dev978__tar.gz → 4.0.0.dev993__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.0.0.dev978 → howler_api-4.0.0.dev993}/PKG-INFO +1 -1
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/actions/demote.py +2 -1
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/actions/promote.py +6 -1
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/v1/search.py +0 -29
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/v1/tool.py +42 -15
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/datastore/collection.py +250 -19
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/datastore/howler_store.py +2 -1
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/datastore/store.py +12 -3
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/helper/hit.py +10 -1
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/helper.py +1 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/config.py +50 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/howler_data.py +6 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/random_data.py +1 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/services/hit_service.py +1 -1
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/pyproject.toml +1 -1
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/README.md +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/__init__.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/actions/__init__.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/actions/add_label.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/actions/add_to_bundle.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/actions/change_field.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/actions/example_plugin.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/actions/prioritization.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/actions/remove_from_bundle.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/actions/remove_label.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/actions/transition.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/__init__.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/base.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/socket.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/v1/__init__.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/v1/action.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/v1/analytic.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/v1/auth.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/v1/clue.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/v1/configs.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/v1/dossier.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/v1/help.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/v1/hit.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/v1/notebook.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/v1/overview.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/v1/template.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/v1/user.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/v1/utils/__init__.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/v1/utils/etag.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/v1/view.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/app.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/common/README.md +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/common/__init__.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/common/classification.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/common/classification.yml +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/common/exceptions.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/common/loader.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/common/logging/__init__.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/common/logging/audit.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/common/logging/format.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/common/net.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/common/net_static.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/common/random_user.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/common/swagger.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/config.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/cronjobs/__init__.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/cronjobs/action_queue_worker.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/cronjobs/retention.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/cronjobs/rules.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/cronjobs/view_cleanup.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/datastore/README.md +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/datastore/__init__.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/datastore/bulk.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/datastore/constants.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/datastore/exceptions.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/datastore/migrations/fix_process.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/datastore/operations.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/datastore/schemas.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/datastore/support/__init__.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/datastore/support/build.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/datastore/support/schemas.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/datastore/types.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/error.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/external/README.md +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/external/__init__.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/external/generate_mitre.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/external/generate_sigma_rules.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/external/generate_tlds.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/external/reindex_data.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/external/wipe_databases.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/gunicorn_config.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/healthz.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/helper/__init__.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/helper/azure.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/helper/discover.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/helper/oauth.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/helper/search.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/helper/workflow.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/helper/ws.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/README.md +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/__init__.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/base.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/charter.txt +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/howler_enum.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/__init__.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/action.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/analytic.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/assemblyline.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/aws.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/azure.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/cbs.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/clue.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/dossier.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/__init__.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/agent.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/autonomous_system.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/client.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/cloud.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/code_signature.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/container.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/dns.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/egress.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/elf.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/email.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/error.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/event.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/faas.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/file.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/geo.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/group.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/hash.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/host.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/http.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/ingress.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/interface.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/network.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/observer.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/organization.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/os.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/pe.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/process.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/registry.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/related.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/rule.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/server.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/threat.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/tls.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/url.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/user.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/user_agent.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/vulnerability.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/gcp.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/hit.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/lead.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/localized_label.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/overview.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/pivot.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/template.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/user.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/view.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/randomizer.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/patched.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/plugins/__init__.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/plugins/config.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/remote/__init__.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/remote/datatypes/README.md +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/remote/datatypes/__init__.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/remote/datatypes/counters.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/remote/datatypes/events.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/remote/datatypes/hash.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/remote/datatypes/lock.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/remote/datatypes/queues/__init__.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/remote/datatypes/queues/comms.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/remote/datatypes/queues/multi.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/remote/datatypes/queues/named.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/remote/datatypes/queues/priority.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/remote/datatypes/set.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/remote/datatypes/user_quota_tracker.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/security/__init__.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/security/socket.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/security/utils.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/services/__init__.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/services/action_service.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/services/analytic_service.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/services/auth_service.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/services/config_service.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/services/dossier_service.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/services/event_service.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/services/jwt_service.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/services/lucene_service.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/services/notebook_service.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/services/overview_service.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/services/template_service.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/services/user_service.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/telemetry.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/utils/__init__.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/utils/annotations.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/utils/chunk.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/utils/compat.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/utils/constants.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/utils/dict_utils.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/utils/isotime.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/utils/list_utils.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/utils/lucene.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/utils/path.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/utils/socket_utils.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/utils/str_utils.py +0 -0
- {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/utils/uid.py +0 -0
|
@@ -76,6 +76,7 @@ def execute(
|
|
|
76
76
|
*hit_helper.demote_hit(escalation=escalation),
|
|
77
77
|
odm_helper.update("howler.assessment", None),
|
|
78
78
|
odm_helper.update("howler.rationale", None),
|
|
79
|
+
odm_helper.update("howler.assignment", None),
|
|
79
80
|
],
|
|
80
81
|
)
|
|
81
82
|
else:
|
|
@@ -93,7 +94,7 @@ def execute(
|
|
|
93
94
|
ds.hit.update_by_query(
|
|
94
95
|
query,
|
|
95
96
|
[
|
|
96
|
-
*hit_helper.assess_hit(assessment, rationale),
|
|
97
|
+
*hit_helper.assess_hit(assessment, rationale, user=(user if user else "automation")),
|
|
97
98
|
odm_helper.update(
|
|
98
99
|
"howler.assignment",
|
|
99
100
|
user.get("uname", "automation") if user else "automation",
|
|
@@ -10,6 +10,7 @@ from howler.odm.models.howler_data import (
|
|
|
10
10
|
AssessmentEscalationMap,
|
|
11
11
|
Escalation,
|
|
12
12
|
)
|
|
13
|
+
from howler.odm.models.user import User
|
|
13
14
|
from howler.utils.str_utils import sanitize_lucene_query
|
|
14
15
|
|
|
15
16
|
OPERATION_ID = "promote"
|
|
@@ -26,6 +27,7 @@ def execute(
|
|
|
26
27
|
escalation: Escalation = Escalation.ALERT,
|
|
27
28
|
assessment: Optional[str] = None,
|
|
28
29
|
rationale: Optional[str] = None,
|
|
30
|
+
user: Optional[User] = None,
|
|
29
31
|
**kwargs,
|
|
30
32
|
):
|
|
31
33
|
"""Promote a hit.
|
|
@@ -73,6 +75,7 @@ def execute(
|
|
|
73
75
|
*hit_helper.promote_hit(escalation=escalation),
|
|
74
76
|
odm_helper.update("howler.assessment", None),
|
|
75
77
|
odm_helper.update("howler.rationale", None),
|
|
78
|
+
odm_helper.update("howler.assignment", None),
|
|
76
79
|
],
|
|
77
80
|
)
|
|
78
81
|
else:
|
|
@@ -87,7 +90,9 @@ def execute(
|
|
|
87
90
|
)
|
|
88
91
|
return report
|
|
89
92
|
|
|
90
|
-
ds.hit.update_by_query(
|
|
93
|
+
ds.hit.update_by_query(
|
|
94
|
+
query, hit_helper.assess_hit(assessment, rationale, user=(user if user else "automation"))
|
|
95
|
+
)
|
|
91
96
|
|
|
92
97
|
report.append(
|
|
93
98
|
{
|
|
@@ -78,7 +78,6 @@ def search(index, **kwargs):
|
|
|
78
78
|
sort => How to sort the results (not available in deep paging)
|
|
79
79
|
fl => List of fields to return
|
|
80
80
|
timeout => Maximum execution time (ms)
|
|
81
|
-
use_archive => Allow access to the datastore achive (Default: False)
|
|
82
81
|
track_total_hits => Track the total number of query matches, instead of stopping at 10000 (Default: False)
|
|
83
82
|
metadata => A list of additional features to be added to the result alongside the raw results
|
|
84
83
|
|
|
@@ -118,18 +117,9 @@ def search(index, **kwargs):
|
|
|
118
117
|
"track_total_hits",
|
|
119
118
|
]
|
|
120
119
|
multi_fields = ["filters", "metadata"]
|
|
121
|
-
boolean_fields = ["use_archive"]
|
|
122
120
|
|
|
123
121
|
params, req_data = generate_params(request, fields, multi_fields)
|
|
124
122
|
|
|
125
|
-
params.update(
|
|
126
|
-
{
|
|
127
|
-
k: str(req_data.get(k, "false")).lower() in ["true", ""]
|
|
128
|
-
for k in boolean_fields
|
|
129
|
-
if req_data.get(k, None) is not None
|
|
130
|
-
}
|
|
131
|
-
)
|
|
132
|
-
|
|
133
123
|
if has_access_control(index):
|
|
134
124
|
params.update({"access_control": user["access_control"]})
|
|
135
125
|
|
|
@@ -350,18 +340,9 @@ def sigma_search(index, **kwargs):
|
|
|
350
340
|
"track_total_hits",
|
|
351
341
|
]
|
|
352
342
|
multi_fields = ["filters"]
|
|
353
|
-
boolean_fields = ["use_archive"]
|
|
354
343
|
|
|
355
344
|
params, req_data = generate_params(request, fields, multi_fields)
|
|
356
345
|
|
|
357
|
-
params.update(
|
|
358
|
-
{
|
|
359
|
-
k: str(req_data.get(k, "false")).lower() in ["true", ""]
|
|
360
|
-
for k in boolean_fields
|
|
361
|
-
if req_data.get(k, None) is not None
|
|
362
|
-
}
|
|
363
|
-
)
|
|
364
|
-
|
|
365
346
|
if has_access_control(index):
|
|
366
347
|
params.update({"access_control": user["access_control"]})
|
|
367
348
|
|
|
@@ -520,7 +501,6 @@ def count(index, **kwargs):
|
|
|
520
501
|
Optional Arguments:
|
|
521
502
|
filters => List of additional filter queries limit the data
|
|
522
503
|
timeout => Maximum execution time (ms)
|
|
523
|
-
use_archive => Allow access to the datastore achive (Default: False)
|
|
524
504
|
|
|
525
505
|
Data Block:
|
|
526
506
|
# Note that the data block is for POST requests only!
|
|
@@ -543,15 +523,6 @@ def count(index, **kwargs):
|
|
|
543
523
|
|
|
544
524
|
params, req_data = generate_params(request, [], [])
|
|
545
525
|
|
|
546
|
-
boolean_fields = ["use_archive"]
|
|
547
|
-
params.update(
|
|
548
|
-
{
|
|
549
|
-
k: str(req_data.get(k, "false")).lower() in ["true", ""]
|
|
550
|
-
for k in boolean_fields
|
|
551
|
-
if req_data.get(k, None) is not None
|
|
552
|
-
}
|
|
553
|
-
)
|
|
554
|
-
|
|
555
526
|
if has_access_control(index):
|
|
556
527
|
params.update({"access_control": user["access_control"]})
|
|
557
528
|
|
|
@@ -59,6 +59,9 @@ def create_one_or_many_hits(tool_name: str, user: User, **kwargs): # noqa: C901
|
|
|
59
59
|
{'id': None, 'error': "Error message"},
|
|
60
60
|
]
|
|
61
61
|
}
|
|
62
|
+
|
|
63
|
+
.. deprecated::
|
|
64
|
+
Use POST /api/v1/hit/ directly with pre-mapped hit data instead.
|
|
62
65
|
"""
|
|
63
66
|
data = request.json
|
|
64
67
|
if not isinstance(data, dict):
|
|
@@ -74,7 +77,10 @@ def create_one_or_many_hits(tool_name: str, user: User, **kwargs): # noqa: C901
|
|
|
74
77
|
|
|
75
78
|
if not isinstance(hits, list):
|
|
76
79
|
return bad_request(err="Invalid: 'hits' field is missing or invalid.")
|
|
77
|
-
warnings = [
|
|
80
|
+
warnings = [
|
|
81
|
+
"This endpoint is deprecated and will be removed in a future version. "
|
|
82
|
+
"Use POST /api/v1/hit/ directly with pre-mapped hit data instead."
|
|
83
|
+
]
|
|
78
84
|
# Validate field_map targets
|
|
79
85
|
hit_fields = Hit.flat_fields()
|
|
80
86
|
for targets in field_map.values():
|
|
@@ -157,27 +163,48 @@ def create_one_or_many_hits(tool_name: str, user: User, **kwargs): # noqa: C901
|
|
|
157
163
|
logger.warning(e)
|
|
158
164
|
|
|
159
165
|
out.append({"id": None, "error": str(e)})
|
|
166
|
+
|
|
167
|
+
# Deduplicate by hash: skip hits whose hash already exists in the datastore
|
|
168
|
+
if odms:
|
|
169
|
+
hashes = [odm.howler.hash for odm in odms]
|
|
170
|
+
existing_hashes: dict[str, int] = datastore().hit.facet(
|
|
171
|
+
"howler.hash",
|
|
172
|
+
query=f"howler.hash:({' OR '.join(hashes)})",
|
|
173
|
+
rows=len(hashes),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
deduplicated_odms = []
|
|
177
|
+
for odm in odms:
|
|
178
|
+
if odm.howler.hash in existing_hashes:
|
|
179
|
+
logger.warning("Hit with hash %s already exists in the DB, skipping", odm.howler.hash)
|
|
180
|
+
warnings.append(f"Hit with hash {odm.howler.hash} already exists in the DB and was skipped.")
|
|
181
|
+
out[:] = [entry for entry in out if entry["id"] != odm.howler.id]
|
|
182
|
+
else:
|
|
183
|
+
deduplicated_odms.append(odm)
|
|
184
|
+
|
|
185
|
+
odms = deduplicated_odms
|
|
186
|
+
|
|
160
187
|
# If there are any errors...
|
|
161
188
|
if any([obj["error"] for obj in out]):
|
|
162
189
|
return bad_request(out, warnings=warnings, err="No valid hits were provided")
|
|
163
|
-
else:
|
|
164
|
-
for odm in odms:
|
|
165
|
-
if bundle_hit is not None:
|
|
166
|
-
bundle_hit.howler.hits.append(odm.howler.id)
|
|
167
|
-
bundle_hit.howler.bundle_size += 1
|
|
168
|
-
odm.howler.bundles.append(bundle_hit.howler.id)
|
|
169
190
|
|
|
170
|
-
|
|
191
|
+
for odm in odms:
|
|
192
|
+
if bundle_hit is not None:
|
|
193
|
+
bundle_hit.howler.hits.append(odm.howler.id)
|
|
194
|
+
bundle_hit.howler.bundle_size += 1
|
|
195
|
+
odm.howler.bundles.append(bundle_hit.howler.id)
|
|
196
|
+
|
|
197
|
+
hit_service.create_hit(odm.howler.id, odm, user=user.uname)
|
|
171
198
|
|
|
172
|
-
|
|
199
|
+
analytic_service.save_from_hit(odm, user)
|
|
173
200
|
|
|
174
|
-
|
|
175
|
-
|
|
201
|
+
if bundle_hit:
|
|
202
|
+
hit_service.create_hit(bundle_hit.howler.id, bundle_hit, user=user.uname)
|
|
176
203
|
|
|
177
|
-
|
|
204
|
+
analytic_service.save_from_hit(bundle_hit, user)
|
|
178
205
|
|
|
179
|
-
|
|
206
|
+
datastore().hit.commit()
|
|
180
207
|
|
|
181
|
-
|
|
208
|
+
action_service.enqueue_action_execution([entry["id"] for entry in out], trigger="create", user=user)
|
|
182
209
|
|
|
183
|
-
|
|
210
|
+
return created(out, warnings=warnings)
|
|
@@ -217,7 +217,7 @@ class ESCollection(Generic[ModelType]):
|
|
|
217
217
|
ENSURE_COLLECTION_WARNED: bool = False
|
|
218
218
|
CUSTOM_AGG_PREFIX: str = "_custom_agg__"
|
|
219
219
|
|
|
220
|
-
def __init__(self, datastore: ESStore, name, model_class=None, validate=True, max_attempts=10):
|
|
220
|
+
def __init__(self, datastore: ESStore, name, model_class=None, validate=True, max_attempts=10, ilm_config=None):
|
|
221
221
|
self.replicas = int(
|
|
222
222
|
environ.get(
|
|
223
223
|
f"ELASTIC_{name.upper()}_REPLICAS",
|
|
@@ -229,6 +229,7 @@ class ESCollection(Generic[ModelType]):
|
|
|
229
229
|
|
|
230
230
|
self.datastore = datastore
|
|
231
231
|
self.name = f"{APP_NAME}-{name}"
|
|
232
|
+
self.ilm_config = ilm_config
|
|
232
233
|
self.index_name = f"{self.name}_hot"
|
|
233
234
|
self.model_class = model_class
|
|
234
235
|
self.validate = validate
|
|
@@ -1627,7 +1628,7 @@ class ESCollection(Generic[ModelType]):
|
|
|
1627
1628
|
|
|
1628
1629
|
return prune(source_data, fields, self.stored_fields, mapping_class=Mapping)
|
|
1629
1630
|
|
|
1630
|
-
def _search(self, args=None, deep_paging_id=None,
|
|
1631
|
+
def _search(self, args=None, deep_paging_id=None, track_total_hits=None):
|
|
1631
1632
|
if args is None:
|
|
1632
1633
|
args = []
|
|
1633
1634
|
|
|
@@ -1804,7 +1805,6 @@ class ESCollection(Generic[ModelType]):
|
|
|
1804
1805
|
filters: list[str] | str | None = None,
|
|
1805
1806
|
access_control: typing.Any = None,
|
|
1806
1807
|
deep_paging_id: str | None = None,
|
|
1807
|
-
use_archive: bool = False,
|
|
1808
1808
|
track_total_hits: bool = False,
|
|
1809
1809
|
script_fields: list[str] = [],
|
|
1810
1810
|
*,
|
|
@@ -1824,7 +1824,6 @@ class ESCollection(Generic[ModelType]):
|
|
|
1824
1824
|
filters: list[str] | str | None = None,
|
|
1825
1825
|
access_control: typing.Any = None,
|
|
1826
1826
|
deep_paging_id: str | None = None,
|
|
1827
|
-
use_archive: bool = False,
|
|
1828
1827
|
track_total_hits: bool = False,
|
|
1829
1828
|
script_fields: list[str] = [],
|
|
1830
1829
|
*,
|
|
@@ -1844,7 +1843,6 @@ class ESCollection(Generic[ModelType]):
|
|
|
1844
1843
|
filters: list[str] | str | None = None,
|
|
1845
1844
|
access_control: typing.Any = None,
|
|
1846
1845
|
deep_paging_id: str | None = None,
|
|
1847
|
-
use_archive: bool = False,
|
|
1848
1846
|
track_total_hits: bool = False,
|
|
1849
1847
|
script_fields: list[str] = [],
|
|
1850
1848
|
*,
|
|
@@ -1864,7 +1862,6 @@ class ESCollection(Generic[ModelType]):
|
|
|
1864
1862
|
filters: list[str] | str | None = None,
|
|
1865
1863
|
access_control: typing.Any = None,
|
|
1866
1864
|
deep_paging_id: str | None = None,
|
|
1867
|
-
use_archive: bool = False,
|
|
1868
1865
|
track_total_hits: bool = False,
|
|
1869
1866
|
script_fields: list[str] = [],
|
|
1870
1867
|
*,
|
|
@@ -1883,7 +1880,6 @@ class ESCollection(Generic[ModelType]):
|
|
|
1883
1880
|
filters=None,
|
|
1884
1881
|
access_control=None,
|
|
1885
1882
|
deep_paging_id=None,
|
|
1886
|
-
use_archive=False,
|
|
1887
1883
|
track_total_hits=None,
|
|
1888
1884
|
script_fields=[],
|
|
1889
1885
|
*,
|
|
@@ -1914,7 +1910,6 @@ class ESCollection(Generic[ModelType]):
|
|
|
1914
1910
|
|
|
1915
1911
|
:param script_fields: List of name/script tuple of fields to be evaluated at runtime
|
|
1916
1912
|
:param track_total_hits: Return to total matching document count
|
|
1917
|
-
:param use_archive: Query also the archive
|
|
1918
1913
|
:param deep_paging_id: ID of the next page during deep paging searches
|
|
1919
1914
|
:param as_obj: Return objects instead of dictionaries
|
|
1920
1915
|
:param query: lucene query to search for
|
|
@@ -1976,7 +1971,6 @@ class ESCollection(Generic[ModelType]):
|
|
|
1976
1971
|
result = self._search(
|
|
1977
1972
|
args,
|
|
1978
1973
|
deep_paging_id=deep_paging_id,
|
|
1979
|
-
use_archive=use_archive,
|
|
1980
1974
|
track_total_hits=track_total_hits,
|
|
1981
1975
|
)
|
|
1982
1976
|
|
|
@@ -2034,7 +2028,6 @@ class ESCollection(Generic[ModelType]):
|
|
|
2034
2028
|
access_control=None,
|
|
2035
2029
|
item_buffer_size=200,
|
|
2036
2030
|
as_obj=True,
|
|
2037
|
-
use_archive=False,
|
|
2038
2031
|
):
|
|
2039
2032
|
"""This function should perform a search through the datastore and stream
|
|
2040
2033
|
all related results as a dictionary of key value pair where each keys
|
|
@@ -2047,7 +2040,6 @@ class ESCollection(Generic[ModelType]):
|
|
|
2047
2040
|
>>> fl[x]: value
|
|
2048
2041
|
>>> }
|
|
2049
2042
|
|
|
2050
|
-
:param use_archive: Query also the archive
|
|
2051
2043
|
:param as_obj: Return objects instead of dictionaries
|
|
2052
2044
|
:param query: lucene query to search for
|
|
2053
2045
|
:param fl: list of fields to return from the search
|
|
@@ -2274,7 +2266,6 @@ class ESCollection(Generic[ModelType]):
|
|
|
2274
2266
|
mincount=None,
|
|
2275
2267
|
filters=None,
|
|
2276
2268
|
access_control=None,
|
|
2277
|
-
use_archive=False,
|
|
2278
2269
|
):
|
|
2279
2270
|
type_modifier = self._validate_steps_count(start, end, gap)
|
|
2280
2271
|
start = type_modifier(start)
|
|
@@ -2313,7 +2304,7 @@ class ESCollection(Generic[ModelType]):
|
|
|
2313
2304
|
if filters:
|
|
2314
2305
|
args.append(("filters", filters))
|
|
2315
2306
|
|
|
2316
|
-
result = self._search(args
|
|
2307
|
+
result = self._search(args)
|
|
2317
2308
|
|
|
2318
2309
|
# Convert the histogram into a dictionary
|
|
2319
2310
|
return {
|
|
@@ -2333,7 +2324,6 @@ class ESCollection(Generic[ModelType]):
|
|
|
2333
2324
|
mincount=None,
|
|
2334
2325
|
filters=None,
|
|
2335
2326
|
access_control=None,
|
|
2336
|
-
use_archive=False,
|
|
2337
2327
|
field_script=None,
|
|
2338
2328
|
):
|
|
2339
2329
|
if not query:
|
|
@@ -2366,7 +2356,7 @@ class ESCollection(Generic[ModelType]):
|
|
|
2366
2356
|
if field_script:
|
|
2367
2357
|
args.append(("field_script", field_script))
|
|
2368
2358
|
|
|
2369
|
-
result = self._search(args
|
|
2359
|
+
result = self._search(args)
|
|
2370
2360
|
|
|
2371
2361
|
# Convert the histogram into a dictionary
|
|
2372
2362
|
return {
|
|
@@ -2379,7 +2369,6 @@ class ESCollection(Generic[ModelType]):
|
|
|
2379
2369
|
query="id:*",
|
|
2380
2370
|
filters=None,
|
|
2381
2371
|
access_control=None,
|
|
2382
|
-
use_archive=False,
|
|
2383
2372
|
field_script=None,
|
|
2384
2373
|
):
|
|
2385
2374
|
if filters is None:
|
|
@@ -2403,7 +2392,7 @@ class ESCollection(Generic[ModelType]):
|
|
|
2403
2392
|
if field_script:
|
|
2404
2393
|
args.append(("field_script", field_script))
|
|
2405
2394
|
|
|
2406
|
-
result = self._search(args
|
|
2395
|
+
result = self._search(args)
|
|
2407
2396
|
return result["aggregations"][f"{field}_stats"]
|
|
2408
2397
|
|
|
2409
2398
|
def grouped_search(
|
|
@@ -2419,7 +2408,6 @@ class ESCollection(Generic[ModelType]):
|
|
|
2419
2408
|
filters=None,
|
|
2420
2409
|
access_control=None,
|
|
2421
2410
|
as_obj=True,
|
|
2422
|
-
use_archive=False,
|
|
2423
2411
|
track_total_hits=False,
|
|
2424
2412
|
):
|
|
2425
2413
|
if rows is None:
|
|
@@ -2462,7 +2450,7 @@ class ESCollection(Generic[ModelType]):
|
|
|
2462
2450
|
if filters:
|
|
2463
2451
|
args.append(("filters", filters))
|
|
2464
2452
|
|
|
2465
|
-
result = self._search(args,
|
|
2453
|
+
result = self._search(args, track_total_hits=track_total_hits)
|
|
2466
2454
|
|
|
2467
2455
|
return {
|
|
2468
2456
|
"offset": offset,
|
|
@@ -2598,6 +2586,83 @@ class ESCollection(Generic[ModelType]):
|
|
|
2598
2586
|
else:
|
|
2599
2587
|
return True
|
|
2600
2588
|
|
|
2589
|
+
def _create_ilm_policy(self, ilm_config):
|
|
2590
|
+
"""Create or update the ILM policy for this collection.
|
|
2591
|
+
|
|
2592
|
+
Builds an ILM policy with hot (rollover), optional warm (forcemerge),
|
|
2593
|
+
and optional cold phases. No delete phase — retention is handled by
|
|
2594
|
+
the retention cronjob.
|
|
2595
|
+
|
|
2596
|
+
The ``ilm_config`` parameter (global :class:`ILMConfig`) is used **only**
|
|
2597
|
+
for the hot phase rollover settings (``rollover_max_age`` and
|
|
2598
|
+
``rollover_max_size``). Warm and cold phase configuration is sourced
|
|
2599
|
+
exclusively from ``self.ilm_config`` (the per-index :class:`ILMIndexConfig`).
|
|
2600
|
+
|
|
2601
|
+
:param ilm_config: The global ILMConfig with rollover settings (hot phase only).
|
|
2602
|
+
"""
|
|
2603
|
+
phases: dict[str, Any] = {
|
|
2604
|
+
"hot": {
|
|
2605
|
+
"min_age": "0ms",
|
|
2606
|
+
"actions": {
|
|
2607
|
+
"rollover": {
|
|
2608
|
+
"max_age": ilm_config.rollover_max_age,
|
|
2609
|
+
"max_primary_shard_size": ilm_config.rollover_max_size,
|
|
2610
|
+
}
|
|
2611
|
+
},
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
if self.ilm_config and self.ilm_config.warm:
|
|
2616
|
+
warm_actions: dict[str, Any] = {}
|
|
2617
|
+
if self.ilm_config.warm_forcemerge_segments is not None:
|
|
2618
|
+
warm_actions["forcemerge"] = {"max_num_segments": self.ilm_config.warm_forcemerge_segments}
|
|
2619
|
+
phases["warm"] = {
|
|
2620
|
+
"min_age": self.ilm_config.warm,
|
|
2621
|
+
"actions": warm_actions,
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
if self.ilm_config and self.ilm_config.cold:
|
|
2625
|
+
# Note: forcemerge is NOT allowed in the cold phase by ES.
|
|
2626
|
+
# Cold phase is typically just for storage tier allocation.
|
|
2627
|
+
phases["cold"] = {
|
|
2628
|
+
"min_age": self.ilm_config.cold,
|
|
2629
|
+
"actions": {},
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
policy = {"phases": phases}
|
|
2633
|
+
|
|
2634
|
+
self.with_retries(
|
|
2635
|
+
self.datastore.client.ilm.put_lifecycle,
|
|
2636
|
+
name=f"{self.name}_policy",
|
|
2637
|
+
policy=policy,
|
|
2638
|
+
)
|
|
2639
|
+
logger.info("ILM policy %s_policy created/updated", self.name)
|
|
2640
|
+
|
|
2641
|
+
def _create_index_template(self, ilm_config):
|
|
2642
|
+
"""Create or update a composable index template for ILM-managed rollover.
|
|
2643
|
+
|
|
2644
|
+
The template matches '{name}-*' and includes the full ODM mappings
|
|
2645
|
+
so that rollover indices inherit the correct schema.
|
|
2646
|
+
|
|
2647
|
+
:param ilm_config: The global ILMConfig (unused directly but kept for symmetry).
|
|
2648
|
+
"""
|
|
2649
|
+
settings = self._get_index_settings()
|
|
2650
|
+
settings["index"]["lifecycle.name"] = f"{self.name}_policy"
|
|
2651
|
+
settings["index"]["lifecycle.rollover_alias"] = self.name
|
|
2652
|
+
|
|
2653
|
+
mappings = self._get_index_mappings()
|
|
2654
|
+
|
|
2655
|
+
self.with_retries(
|
|
2656
|
+
self.datastore.client.indices.put_index_template,
|
|
2657
|
+
name=f"{self.name}_template",
|
|
2658
|
+
index_patterns=[f"{self.name}-*"],
|
|
2659
|
+
template={
|
|
2660
|
+
"settings": settings,
|
|
2661
|
+
"mappings": mappings,
|
|
2662
|
+
},
|
|
2663
|
+
)
|
|
2664
|
+
logger.info("Index template %s_template created/updated", self.name)
|
|
2665
|
+
|
|
2601
2666
|
def _get_index_settings(self) -> dict:
|
|
2602
2667
|
default_stub: dict = deepcopy(default_index)
|
|
2603
2668
|
settings: dict = default_stub.pop("settings", {})
|
|
@@ -2714,8 +2779,15 @@ class ESCollection(Generic[ModelType]):
|
|
|
2714
2779
|
"""This function should test if the collection that you are trying to access does indeed exist
|
|
2715
2780
|
and should create it if it does not.
|
|
2716
2781
|
|
|
2782
|
+
When ILM is configured for this collection, it sets up the ILM policy,
|
|
2783
|
+
composable index template, and bootstraps a rollover alias instead of
|
|
2784
|
+
using the legacy _hot index naming.
|
|
2785
|
+
|
|
2717
2786
|
:return:
|
|
2718
2787
|
"""
|
|
2788
|
+
if self.ilm_config:
|
|
2789
|
+
return self._ensure_collection_ilm()
|
|
2790
|
+
|
|
2719
2791
|
# Create HOT index
|
|
2720
2792
|
if not self.with_retries(self.datastore.client.indices.exists, index=self.name):
|
|
2721
2793
|
logger.debug("Index %s does not exist. Creating it now...", self.name.upper())
|
|
@@ -2758,6 +2830,158 @@ class ESCollection(Generic[ModelType]):
|
|
|
2758
2830
|
|
|
2759
2831
|
self._check_fields()
|
|
2760
2832
|
|
|
2833
|
+
def _ensure_collection_ilm(self):
|
|
2834
|
+
"""Bootstrap an ILM-managed collection with rollover alias.
|
|
2835
|
+
|
|
2836
|
+
1. Create/update the ILM policy and composable index template.
|
|
2837
|
+
2. Bootstrap the initial index if needed:
|
|
2838
|
+
- If ILM indices already exist (pattern {name}-0*), skip.
|
|
2839
|
+
- If a legacy _hot index exists, migrate it to {name}-000001.
|
|
2840
|
+
- Otherwise, create {name}-000001 from scratch.
|
|
2841
|
+
"""
|
|
2842
|
+
from howler.odm.models.config import config as _config
|
|
2843
|
+
|
|
2844
|
+
ilm_global = _config.datastore.ilm
|
|
2845
|
+
|
|
2846
|
+
# Idempotent: create/update ILM policy and index template
|
|
2847
|
+
self._create_ilm_policy(ilm_global)
|
|
2848
|
+
self._create_index_template(ilm_global)
|
|
2849
|
+
|
|
2850
|
+
ilm_initial_index = f"{self.name}-000001"
|
|
2851
|
+
|
|
2852
|
+
# Check if any ILM-managed index already exists
|
|
2853
|
+
existing_ilm_indices = list(
|
|
2854
|
+
self.with_retries(
|
|
2855
|
+
self.datastore.client.indices.get, index=f"{self.name}-0*", ignore_unavailable=True
|
|
2856
|
+
).keys()
|
|
2857
|
+
)
|
|
2858
|
+
|
|
2859
|
+
if existing_ilm_indices:
|
|
2860
|
+
# ILM already bootstrapped — ensure the alias exists
|
|
2861
|
+
latest = sorted(existing_ilm_indices)[-1]
|
|
2862
|
+
if not self.with_retries(self.datastore.client.indices.exists_alias, name=self.name):
|
|
2863
|
+
# Find the latest index to set as write index
|
|
2864
|
+
self.with_retries(
|
|
2865
|
+
self.datastore.client.indices.put_alias,
|
|
2866
|
+
index=latest,
|
|
2867
|
+
name=self.name,
|
|
2868
|
+
is_write_index=True,
|
|
2869
|
+
)
|
|
2870
|
+
|
|
2871
|
+
self.index_name = latest
|
|
2872
|
+
logger.debug("ILM collection %s already bootstrapped", self.name.upper())
|
|
2873
|
+
elif self.with_retries(self.datastore.client.indices.exists, index=self.index_name):
|
|
2874
|
+
# Legacy _hot index exists — migrate to ILM
|
|
2875
|
+
logger.info("Migrating %s from legacy _hot index to ILM-managed rollover", self.name.upper())
|
|
2876
|
+
|
|
2877
|
+
# Block writes on the old index
|
|
2878
|
+
self.with_retries(
|
|
2879
|
+
self.datastore.client.indices.put_settings,
|
|
2880
|
+
index=self.index_name,
|
|
2881
|
+
settings=write_block_settings,
|
|
2882
|
+
)
|
|
2883
|
+
|
|
2884
|
+
# Everything after write-block must be wrapped in try/except to ensure
|
|
2885
|
+
# we unblock writes if migration fails — otherwise ingestion is stuck.
|
|
2886
|
+
try:
|
|
2887
|
+
# Clone the _hot index to the new ILM initial index
|
|
2888
|
+
self._safe_index_copy(self.datastore.client.indices.clone, self.index_name, ilm_initial_index)
|
|
2889
|
+
|
|
2890
|
+
# Apply ILM settings to the new index
|
|
2891
|
+
self.with_retries(
|
|
2892
|
+
self.datastore.client.indices.put_settings,
|
|
2893
|
+
index=ilm_initial_index,
|
|
2894
|
+
settings={
|
|
2895
|
+
"index.lifecycle.name": f"{self.name}_policy",
|
|
2896
|
+
"index.lifecycle.rollover_alias": self.name,
|
|
2897
|
+
"index.blocks.write": None,
|
|
2898
|
+
},
|
|
2899
|
+
)
|
|
2900
|
+
|
|
2901
|
+
# Swap alias: remove old _hot, add new ILM index as write index
|
|
2902
|
+
actions = [
|
|
2903
|
+
{"add": {"index": ilm_initial_index, "alias": self.name, "is_write_index": True}},
|
|
2904
|
+
]
|
|
2905
|
+
|
|
2906
|
+
# Remove old alias if it points to _hot
|
|
2907
|
+
if self.with_retries(self.datastore.client.indices.exists_alias, index=self.index_name, name=self.name):
|
|
2908
|
+
actions.append({"remove": {"index": self.index_name, "alias": self.name}})
|
|
2909
|
+
|
|
2910
|
+
self.with_retries(self.datastore.client.indices.update_aliases, actions=actions)
|
|
2911
|
+
|
|
2912
|
+
except Exception:
|
|
2913
|
+
# Migration failed — rollback to restore ingestion to the old index
|
|
2914
|
+
logger.exception(
|
|
2915
|
+
"Migration of %s to ILM failed. Rolling back write-block on %s.",
|
|
2916
|
+
self.name.upper(),
|
|
2917
|
+
self.index_name,
|
|
2918
|
+
)
|
|
2919
|
+
|
|
2920
|
+
# Unblock writes on the old index so ingestion can resume
|
|
2921
|
+
try:
|
|
2922
|
+
self.with_retries(
|
|
2923
|
+
self.datastore.client.indices.put_settings,
|
|
2924
|
+
index=self.index_name,
|
|
2925
|
+
settings=write_unblock_settings,
|
|
2926
|
+
)
|
|
2927
|
+
logger.info("Rollback successful: writes restored to %s", self.index_name)
|
|
2928
|
+
except Exception as rollback_err:
|
|
2929
|
+
logger.critical(
|
|
2930
|
+
"CRITICAL: Rollback failed for %s — index may be write-blocked! Error: %s",
|
|
2931
|
+
self.index_name,
|
|
2932
|
+
str(rollback_err),
|
|
2933
|
+
)
|
|
2934
|
+
|
|
2935
|
+
# Clean up partially-created ILM index if it exists
|
|
2936
|
+
try:
|
|
2937
|
+
if self.with_retries(self.datastore.client.indices.exists, index=ilm_initial_index):
|
|
2938
|
+
self.with_retries(self.datastore.client.indices.delete, index=ilm_initial_index)
|
|
2939
|
+
logger.info("Cleaned up partial ILM index %s", ilm_initial_index)
|
|
2940
|
+
except Exception:
|
|
2941
|
+
logger.warning(
|
|
2942
|
+
"Could not clean up partial ILM index %s — manual cleanup may be needed",
|
|
2943
|
+
ilm_initial_index,
|
|
2944
|
+
)
|
|
2945
|
+
|
|
2946
|
+
# Re-raise the original exception so startup fails clearly
|
|
2947
|
+
raise
|
|
2948
|
+
|
|
2949
|
+
# Unblock writes on the old index (it stays around until manually removed)
|
|
2950
|
+
self.with_retries(
|
|
2951
|
+
self.datastore.client.indices.put_settings,
|
|
2952
|
+
index=self.index_name,
|
|
2953
|
+
settings=write_unblock_settings,
|
|
2954
|
+
)
|
|
2955
|
+
|
|
2956
|
+
# Update index_name to point to the ILM initial index
|
|
2957
|
+
self.index_name = ilm_initial_index
|
|
2958
|
+
|
|
2959
|
+
logger.info("Migration of %s to ILM complete", self.name.upper())
|
|
2960
|
+
else:
|
|
2961
|
+
# Fresh install — create the initial ILM index with alias
|
|
2962
|
+
logger.debug("Creating ILM-managed index %s...", ilm_initial_index)
|
|
2963
|
+
settings = self._get_index_settings()
|
|
2964
|
+
settings["index"]["lifecycle.name"] = f"{self.name}_policy"
|
|
2965
|
+
settings["index"]["lifecycle.rollover_alias"] = self.name
|
|
2966
|
+
|
|
2967
|
+
try:
|
|
2968
|
+
self.with_retries(
|
|
2969
|
+
self.datastore.client.indices.create,
|
|
2970
|
+
index=ilm_initial_index,
|
|
2971
|
+
mappings=self._get_index_mappings(),
|
|
2972
|
+
settings=settings,
|
|
2973
|
+
aliases={self.name: {"is_write_index": True}},
|
|
2974
|
+
)
|
|
2975
|
+
except elasticsearch.exceptions.RequestError as e:
|
|
2976
|
+
if "resource_already_exists_exception" not in str(e):
|
|
2977
|
+
raise
|
|
2978
|
+
logger.warning("ILM index already exists: %s", ilm_initial_index)
|
|
2979
|
+
|
|
2980
|
+
# Update index_name to point to the ILM initial index
|
|
2981
|
+
self.index_name = ilm_initial_index
|
|
2982
|
+
|
|
2983
|
+
self._check_fields()
|
|
2984
|
+
|
|
2761
2985
|
def _add_fields(self, missing_fields: Dict):
|
|
2762
2986
|
no_fix = []
|
|
2763
2987
|
properties = {}
|
|
@@ -2795,6 +3019,13 @@ class ESCollection(Generic[ModelType]):
|
|
|
2795
3019
|
**recursive_update(current_template, {"mappings": {"properties": properties}}),
|
|
2796
3020
|
)
|
|
2797
3021
|
|
|
3022
|
+
# When ILM is enabled, also update the composable index template so
|
|
3023
|
+
# future rollover indices inherit the new field mappings.
|
|
3024
|
+
if self.ilm_config:
|
|
3025
|
+
from howler.odm.models.config import config as _config
|
|
3026
|
+
|
|
3027
|
+
self._create_index_template(_config.datastore.ilm)
|
|
3028
|
+
|
|
2798
3029
|
def wipe(self):
|
|
2799
3030
|
"""This function should completely delete the collection
|
|
2800
3031
|
|
|
@@ -51,7 +51,8 @@ class HowlerDatastore(object):
|
|
|
51
51
|
)
|
|
52
52
|
|
|
53
53
|
for _index, _odm in INDEXES:
|
|
54
|
-
|
|
54
|
+
ilm_index_config = config.datastore.ilm.indices.get(_index) if config.datastore.ilm.enabled else None
|
|
55
|
+
self.ds.register(_index, _odm, ilm_config=ilm_index_config)
|
|
55
56
|
|
|
56
57
|
def __enter__(self):
|
|
57
58
|
return self
|