howler-api 3.4.0.dev912__tar.gz → 3.4.0.dev927__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-3.4.0.dev912 → howler_api-3.4.0.dev927}/PKG-INFO +1 -1
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/api/v1/search.py +0 -29
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/datastore/collection.py +200 -17
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/datastore/howler_store.py +2 -1
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/datastore/store.py +12 -3
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/config.py +45 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/pyproject.toml +1 -1
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/README.md +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/actions/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/actions/add_label.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/actions/add_to_bundle.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/actions/change_field.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/actions/demote.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/actions/example_plugin.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/actions/prioritization.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/actions/promote.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/actions/remove_from_bundle.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/actions/remove_label.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/actions/transition.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/api/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/api/base.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/api/socket.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/api/v1/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/api/v1/action.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/api/v1/analytic.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/api/v1/auth.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/api/v1/clue.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/api/v1/configs.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/api/v1/dossier.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/api/v1/help.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/api/v1/hit.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/api/v1/notebook.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/api/v1/overview.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/api/v1/template.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/api/v1/tool.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/api/v1/user.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/api/v1/utils/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/api/v1/utils/etag.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/api/v1/view.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/app.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/common/README.md +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/common/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/common/classification.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/common/classification.yml +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/common/exceptions.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/common/loader.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/common/logging/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/common/logging/audit.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/common/logging/format.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/common/net.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/common/net_static.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/common/random_user.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/common/swagger.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/config.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/cronjobs/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/cronjobs/retention.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/cronjobs/rules.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/cronjobs/view_cleanup.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/datastore/README.md +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/datastore/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/datastore/bulk.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/datastore/constants.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/datastore/exceptions.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/datastore/migrations/fix_process.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/datastore/operations.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/datastore/schemas.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/datastore/support/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/datastore/support/build.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/datastore/support/schemas.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/datastore/types.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/error.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/external/README.md +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/external/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/external/generate_mitre.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/external/generate_sigma_rules.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/external/generate_tlds.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/external/reindex_data.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/external/wipe_databases.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/gunicorn_config.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/healthz.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/helper/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/helper/azure.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/helper/discover.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/helper/hit.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/helper/oauth.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/helper/search.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/helper/workflow.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/helper/ws.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/README.md +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/base.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/charter.txt +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/helper.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/howler_enum.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/action.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/analytic.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/assemblyline.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/aws.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/azure.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/cbs.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/clue.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/dossier.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/agent.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/autonomous_system.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/client.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/cloud.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/code_signature.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/container.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/dns.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/egress.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/elf.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/email.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/error.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/event.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/faas.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/file.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/geo.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/group.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/hash.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/host.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/http.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/ingress.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/interface.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/network.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/observer.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/organization.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/os.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/pe.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/process.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/registry.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/related.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/rule.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/server.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/threat.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/tls.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/url.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/user.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/user_agent.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/ecs/vulnerability.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/gcp.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/hit.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/howler_data.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/lead.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/localized_label.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/overview.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/pivot.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/template.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/user.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/models/view.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/random_data.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/odm/randomizer.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/patched.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/plugins/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/plugins/config.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/remote/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/remote/datatypes/README.md +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/remote/datatypes/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/remote/datatypes/counters.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/remote/datatypes/events.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/remote/datatypes/hash.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/remote/datatypes/lock.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/remote/datatypes/queues/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/remote/datatypes/queues/comms.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/remote/datatypes/queues/multi.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/remote/datatypes/queues/named.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/remote/datatypes/queues/priority.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/remote/datatypes/set.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/remote/datatypes/user_quota_tracker.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/security/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/security/socket.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/security/utils.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/services/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/services/action_service.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/services/analytic_service.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/services/auth_service.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/services/config_service.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/services/dossier_service.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/services/event_service.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/services/hit_service.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/services/jwt_service.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/services/lucene_service.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/services/notebook_service.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/services/overview_service.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/services/template_service.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/services/user_service.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/telemetry.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/utils/__init__.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/utils/annotations.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/utils/chunk.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/utils/compat.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/utils/constants.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/utils/dict_utils.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/utils/isotime.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/utils/list_utils.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/utils/lucene.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/utils/path.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/utils/socket_utils.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/utils/str_utils.py +0 -0
- {howler_api-3.4.0.dev912 → howler_api-3.4.0.dev927}/howler/utils/uid.py +0 -0
|
@@ -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
|
|
|
@@ -215,7 +215,7 @@ class ESCollection(Generic[ModelType]):
|
|
|
215
215
|
IGNORE_ENSURE_COLLECTION: bool = False
|
|
216
216
|
ENSURE_COLLECTION_WARNED: bool = False
|
|
217
217
|
|
|
218
|
-
def __init__(self, datastore: ESStore, name, model_class=None, validate=True, max_attempts=10):
|
|
218
|
+
def __init__(self, datastore: ESStore, name, model_class=None, validate=True, max_attempts=10, ilm_config=None):
|
|
219
219
|
self.replicas = int(
|
|
220
220
|
environ.get(
|
|
221
221
|
f"ELASTIC_{name.upper()}_REPLICAS",
|
|
@@ -227,6 +227,7 @@ class ESCollection(Generic[ModelType]):
|
|
|
227
227
|
|
|
228
228
|
self.datastore = datastore
|
|
229
229
|
self.name = f"{APP_NAME}-{name}"
|
|
230
|
+
self.ilm_config = ilm_config
|
|
230
231
|
self.index_name = f"{self.name}_hot"
|
|
231
232
|
self.model_class = model_class
|
|
232
233
|
self.validate = validate
|
|
@@ -1394,7 +1395,7 @@ class ESCollection(Generic[ModelType]):
|
|
|
1394
1395
|
|
|
1395
1396
|
return prune(source_data, fields, self.stored_fields, mapping_class=Mapping)
|
|
1396
1397
|
|
|
1397
|
-
def _search(self, args=None, deep_paging_id=None,
|
|
1398
|
+
def _search(self, args=None, deep_paging_id=None, track_total_hits=None):
|
|
1398
1399
|
if args is None:
|
|
1399
1400
|
args = []
|
|
1400
1401
|
|
|
@@ -1551,7 +1552,6 @@ class ESCollection(Generic[ModelType]):
|
|
|
1551
1552
|
filters: list[str] | str | None = None,
|
|
1552
1553
|
access_control: typing.Any = None,
|
|
1553
1554
|
deep_paging_id: str | None = None,
|
|
1554
|
-
use_archive: bool = False,
|
|
1555
1555
|
track_total_hits: bool = False,
|
|
1556
1556
|
script_fields: list[str] = [],
|
|
1557
1557
|
) -> SearchResult[ModelType]: ...
|
|
@@ -1569,7 +1569,6 @@ class ESCollection(Generic[ModelType]):
|
|
|
1569
1569
|
filters: list[str] | str | None = None,
|
|
1570
1570
|
access_control: typing.Any = None,
|
|
1571
1571
|
deep_paging_id: str | None = None,
|
|
1572
|
-
use_archive: bool = False,
|
|
1573
1572
|
track_total_hits: bool = False,
|
|
1574
1573
|
script_fields: list[str] = [],
|
|
1575
1574
|
) -> SearchResult[dict[str, typing.Any]]: ...
|
|
@@ -1586,7 +1585,6 @@ class ESCollection(Generic[ModelType]):
|
|
|
1586
1585
|
filters=None,
|
|
1587
1586
|
access_control=None,
|
|
1588
1587
|
deep_paging_id=None,
|
|
1589
|
-
use_archive=False,
|
|
1590
1588
|
track_total_hits=None,
|
|
1591
1589
|
script_fields=[],
|
|
1592
1590
|
):
|
|
@@ -1607,7 +1605,6 @@ class ESCollection(Generic[ModelType]):
|
|
|
1607
1605
|
|
|
1608
1606
|
:param script_fields: List of name/script tuple of fields to be evaluated at runtime
|
|
1609
1607
|
:param track_total_hits: Return to total matching document count
|
|
1610
|
-
:param use_archive: Query also the archive
|
|
1611
1608
|
:param deep_paging_id: ID of the next page during deep paging searches
|
|
1612
1609
|
:param as_obj: Return objects instead of dictionaries
|
|
1613
1610
|
:param query: lucene query to search for
|
|
@@ -1663,7 +1660,6 @@ class ESCollection(Generic[ModelType]):
|
|
|
1663
1660
|
result = self._search(
|
|
1664
1661
|
args,
|
|
1665
1662
|
deep_paging_id=deep_paging_id,
|
|
1666
|
-
use_archive=use_archive,
|
|
1667
1663
|
track_total_hits=track_total_hits,
|
|
1668
1664
|
)
|
|
1669
1665
|
|
|
@@ -1710,7 +1706,6 @@ class ESCollection(Generic[ModelType]):
|
|
|
1710
1706
|
access_control=None,
|
|
1711
1707
|
item_buffer_size=200,
|
|
1712
1708
|
as_obj=True,
|
|
1713
|
-
use_archive=False,
|
|
1714
1709
|
):
|
|
1715
1710
|
"""This function should perform a search through the datastore and stream
|
|
1716
1711
|
all related results as a dictionary of key value pair where each keys
|
|
@@ -1723,7 +1718,6 @@ class ESCollection(Generic[ModelType]):
|
|
|
1723
1718
|
>>> fl[x]: value
|
|
1724
1719
|
>>> }
|
|
1725
1720
|
|
|
1726
|
-
:param use_archive: Query also the archive
|
|
1727
1721
|
:param as_obj: Return objects instead of dictionaries
|
|
1728
1722
|
:param query: lucene query to search for
|
|
1729
1723
|
:param fl: list of fields to return from the search
|
|
@@ -1947,7 +1941,6 @@ class ESCollection(Generic[ModelType]):
|
|
|
1947
1941
|
mincount=None,
|
|
1948
1942
|
filters=None,
|
|
1949
1943
|
access_control=None,
|
|
1950
|
-
use_archive=False,
|
|
1951
1944
|
):
|
|
1952
1945
|
type_modifier = self._validate_steps_count(start, end, gap)
|
|
1953
1946
|
start = type_modifier(start)
|
|
@@ -1986,7 +1979,7 @@ class ESCollection(Generic[ModelType]):
|
|
|
1986
1979
|
if filters:
|
|
1987
1980
|
args.append(("filters", filters))
|
|
1988
1981
|
|
|
1989
|
-
result = self._search(args
|
|
1982
|
+
result = self._search(args)
|
|
1990
1983
|
|
|
1991
1984
|
# Convert the histogram into a dictionary
|
|
1992
1985
|
return {
|
|
@@ -2006,7 +1999,6 @@ class ESCollection(Generic[ModelType]):
|
|
|
2006
1999
|
mincount=None,
|
|
2007
2000
|
filters=None,
|
|
2008
2001
|
access_control=None,
|
|
2009
|
-
use_archive=False,
|
|
2010
2002
|
field_script=None,
|
|
2011
2003
|
):
|
|
2012
2004
|
if not query:
|
|
@@ -2039,7 +2031,7 @@ class ESCollection(Generic[ModelType]):
|
|
|
2039
2031
|
if field_script:
|
|
2040
2032
|
args.append(("field_script", field_script))
|
|
2041
2033
|
|
|
2042
|
-
result = self._search(args
|
|
2034
|
+
result = self._search(args)
|
|
2043
2035
|
|
|
2044
2036
|
# Convert the histogram into a dictionary
|
|
2045
2037
|
return {
|
|
@@ -2052,7 +2044,6 @@ class ESCollection(Generic[ModelType]):
|
|
|
2052
2044
|
query="id:*",
|
|
2053
2045
|
filters=None,
|
|
2054
2046
|
access_control=None,
|
|
2055
|
-
use_archive=False,
|
|
2056
2047
|
field_script=None,
|
|
2057
2048
|
):
|
|
2058
2049
|
if filters is None:
|
|
@@ -2076,7 +2067,7 @@ class ESCollection(Generic[ModelType]):
|
|
|
2076
2067
|
if field_script:
|
|
2077
2068
|
args.append(("field_script", field_script))
|
|
2078
2069
|
|
|
2079
|
-
result = self._search(args
|
|
2070
|
+
result = self._search(args)
|
|
2080
2071
|
return result["aggregations"][f"{field}_stats"]
|
|
2081
2072
|
|
|
2082
2073
|
def grouped_search(
|
|
@@ -2092,7 +2083,6 @@ class ESCollection(Generic[ModelType]):
|
|
|
2092
2083
|
filters=None,
|
|
2093
2084
|
access_control=None,
|
|
2094
2085
|
as_obj=True,
|
|
2095
|
-
use_archive=False,
|
|
2096
2086
|
track_total_hits=False,
|
|
2097
2087
|
):
|
|
2098
2088
|
if rows is None:
|
|
@@ -2134,7 +2124,7 @@ class ESCollection(Generic[ModelType]):
|
|
|
2134
2124
|
if filters:
|
|
2135
2125
|
args.append(("filters", filters))
|
|
2136
2126
|
|
|
2137
|
-
result = self._search(args,
|
|
2127
|
+
result = self._search(args, track_total_hits=track_total_hits)
|
|
2138
2128
|
|
|
2139
2129
|
return {
|
|
2140
2130
|
"offset": offset,
|
|
@@ -2270,6 +2260,75 @@ class ESCollection(Generic[ModelType]):
|
|
|
2270
2260
|
else:
|
|
2271
2261
|
return True
|
|
2272
2262
|
|
|
2263
|
+
def _create_ilm_policy(self, ilm_config):
|
|
2264
|
+
"""Create or update the ILM policy for this collection.
|
|
2265
|
+
|
|
2266
|
+
Builds an ILM policy with hot (rollover), optional warm (forcemerge),
|
|
2267
|
+
and optional cold phases. No delete phase — retention is handled by
|
|
2268
|
+
the retention cronjob.
|
|
2269
|
+
|
|
2270
|
+
:param ilm_config: The global ILMConfig with rollover settings.
|
|
2271
|
+
"""
|
|
2272
|
+
phases: dict[str, Any] = {
|
|
2273
|
+
"hot": {
|
|
2274
|
+
"min_age": "0ms",
|
|
2275
|
+
"actions": {
|
|
2276
|
+
"rollover": {
|
|
2277
|
+
"max_age": ilm_config.rollover_max_age,
|
|
2278
|
+
"max_primary_shard_size": ilm_config.rollover_max_size,
|
|
2279
|
+
}
|
|
2280
|
+
},
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
if self.ilm_config and self.ilm_config.warm:
|
|
2285
|
+
phases["warm"] = {
|
|
2286
|
+
"min_age": self.ilm_config.warm,
|
|
2287
|
+
"actions": {
|
|
2288
|
+
"forcemerge": {"max_num_segments": 1},
|
|
2289
|
+
},
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
if self.ilm_config and self.ilm_config.cold:
|
|
2293
|
+
phases["cold"] = {
|
|
2294
|
+
"min_age": self.ilm_config.cold,
|
|
2295
|
+
"actions": {},
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
policy = {"phases": phases}
|
|
2299
|
+
|
|
2300
|
+
self.with_retries(
|
|
2301
|
+
self.datastore.client.ilm.put_lifecycle,
|
|
2302
|
+
name=f"{self.name}_policy",
|
|
2303
|
+
policy=policy,
|
|
2304
|
+
)
|
|
2305
|
+
logger.info("ILM policy %s_policy created/updated", self.name)
|
|
2306
|
+
|
|
2307
|
+
def _create_index_template(self, ilm_config):
|
|
2308
|
+
"""Create or update a composable index template for ILM-managed rollover.
|
|
2309
|
+
|
|
2310
|
+
The template matches '{name}-*' and includes the full ODM mappings
|
|
2311
|
+
so that rollover indices inherit the correct schema.
|
|
2312
|
+
|
|
2313
|
+
:param ilm_config: The global ILMConfig (unused directly but kept for symmetry).
|
|
2314
|
+
"""
|
|
2315
|
+
settings = self._get_index_settings()
|
|
2316
|
+
settings["index"]["lifecycle.name"] = f"{self.name}_policy"
|
|
2317
|
+
settings["index"]["lifecycle.rollover_alias"] = self.name
|
|
2318
|
+
|
|
2319
|
+
mappings = self._get_index_mappings()
|
|
2320
|
+
|
|
2321
|
+
self.with_retries(
|
|
2322
|
+
self.datastore.client.indices.put_index_template,
|
|
2323
|
+
name=f"{self.name}_template",
|
|
2324
|
+
index_patterns=[f"{self.name}-*"],
|
|
2325
|
+
template={
|
|
2326
|
+
"settings": settings,
|
|
2327
|
+
"mappings": mappings,
|
|
2328
|
+
},
|
|
2329
|
+
)
|
|
2330
|
+
logger.info("Index template %s_template created/updated", self.name)
|
|
2331
|
+
|
|
2273
2332
|
def _get_index_settings(self) -> dict:
|
|
2274
2333
|
default_stub: dict = deepcopy(default_index)
|
|
2275
2334
|
settings: dict = default_stub.pop("settings", {})
|
|
@@ -2386,8 +2445,15 @@ class ESCollection(Generic[ModelType]):
|
|
|
2386
2445
|
"""This function should test if the collection that you are trying to access does indeed exist
|
|
2387
2446
|
and should create it if it does not.
|
|
2388
2447
|
|
|
2448
|
+
When ILM is configured for this collection, it sets up the ILM policy,
|
|
2449
|
+
composable index template, and bootstraps a rollover alias instead of
|
|
2450
|
+
using the legacy _hot index naming.
|
|
2451
|
+
|
|
2389
2452
|
:return:
|
|
2390
2453
|
"""
|
|
2454
|
+
if self.ilm_config:
|
|
2455
|
+
return self._ensure_collection_ilm()
|
|
2456
|
+
|
|
2391
2457
|
# Create HOT index
|
|
2392
2458
|
if not self.with_retries(self.datastore.client.indices.exists, index=self.name):
|
|
2393
2459
|
logger.debug("Index %s does not exist. Creating it now...", self.name.upper())
|
|
@@ -2430,6 +2496,116 @@ class ESCollection(Generic[ModelType]):
|
|
|
2430
2496
|
|
|
2431
2497
|
self._check_fields()
|
|
2432
2498
|
|
|
2499
|
+
def _ensure_collection_ilm(self):
|
|
2500
|
+
"""Bootstrap an ILM-managed collection with rollover alias.
|
|
2501
|
+
|
|
2502
|
+
1. Create/update the ILM policy and composable index template.
|
|
2503
|
+
2. Bootstrap the initial index if needed:
|
|
2504
|
+
- If ILM indices already exist (pattern {name}-0*), skip.
|
|
2505
|
+
- If a legacy _hot index exists, migrate it to {name}-000001.
|
|
2506
|
+
- Otherwise, create {name}-000001 from scratch.
|
|
2507
|
+
"""
|
|
2508
|
+
from howler.odm.models.config import config as _config
|
|
2509
|
+
|
|
2510
|
+
ilm_global = _config.datastore.ilm
|
|
2511
|
+
|
|
2512
|
+
# Idempotent: create/update ILM policy and index template
|
|
2513
|
+
self._create_ilm_policy(ilm_global)
|
|
2514
|
+
self._create_index_template(ilm_global)
|
|
2515
|
+
|
|
2516
|
+
ilm_initial_index = f"{self.name}-000001"
|
|
2517
|
+
|
|
2518
|
+
# Check if any ILM-managed index already exists
|
|
2519
|
+
existing_ilm_indices = list(
|
|
2520
|
+
self.with_retries(
|
|
2521
|
+
self.datastore.client.indices.get, index=f"{self.name}-0*", ignore_unavailable=True
|
|
2522
|
+
).keys()
|
|
2523
|
+
)
|
|
2524
|
+
|
|
2525
|
+
if existing_ilm_indices:
|
|
2526
|
+
# ILM already bootstrapped — ensure the alias exists
|
|
2527
|
+
if not self.with_retries(self.datastore.client.indices.exists_alias, name=self.name):
|
|
2528
|
+
# Find the latest index to set as write index
|
|
2529
|
+
latest = sorted(existing_ilm_indices)[-1]
|
|
2530
|
+
self.with_retries(
|
|
2531
|
+
self.datastore.client.indices.put_alias,
|
|
2532
|
+
index=latest,
|
|
2533
|
+
name=self.name,
|
|
2534
|
+
is_write_index=True,
|
|
2535
|
+
)
|
|
2536
|
+
logger.debug("ILM collection %s already bootstrapped", self.name.upper())
|
|
2537
|
+
elif self.with_retries(self.datastore.client.indices.exists, index=self.index_name):
|
|
2538
|
+
# Legacy _hot index exists — migrate to ILM
|
|
2539
|
+
logger.info("Migrating %s from legacy _hot index to ILM-managed rollover", self.name.upper())
|
|
2540
|
+
|
|
2541
|
+
# Block writes on the old index
|
|
2542
|
+
self.with_retries(
|
|
2543
|
+
self.datastore.client.indices.put_settings,
|
|
2544
|
+
index=self.index_name,
|
|
2545
|
+
settings=write_block_settings,
|
|
2546
|
+
)
|
|
2547
|
+
|
|
2548
|
+
# Clone the _hot index to the new ILM initial index
|
|
2549
|
+
self._safe_index_copy(self.datastore.client.indices.clone, self.index_name, ilm_initial_index)
|
|
2550
|
+
|
|
2551
|
+
# Apply ILM settings to the new index
|
|
2552
|
+
self.with_retries(
|
|
2553
|
+
self.datastore.client.indices.put_settings,
|
|
2554
|
+
index=ilm_initial_index,
|
|
2555
|
+
settings={
|
|
2556
|
+
"index.lifecycle.name": f"{self.name}_policy",
|
|
2557
|
+
"index.lifecycle.rollover_alias": self.name,
|
|
2558
|
+
"index.blocks.write": None,
|
|
2559
|
+
},
|
|
2560
|
+
)
|
|
2561
|
+
|
|
2562
|
+
# Swap alias: remove old _hot, add new ILM index as write index
|
|
2563
|
+
actions = [
|
|
2564
|
+
{"add": {"index": ilm_initial_index, "alias": self.name, "is_write_index": True}},
|
|
2565
|
+
]
|
|
2566
|
+
|
|
2567
|
+
# Remove old alias if it points to _hot
|
|
2568
|
+
if self.with_retries(self.datastore.client.indices.exists_alias, index=self.index_name, name=self.name):
|
|
2569
|
+
actions.append({"remove": {"index": self.index_name, "alias": self.name}})
|
|
2570
|
+
|
|
2571
|
+
self.with_retries(self.datastore.client.indices.update_aliases, actions=actions)
|
|
2572
|
+
|
|
2573
|
+
# Unblock writes on the old index (it stays around until manually removed)
|
|
2574
|
+
self.with_retries(
|
|
2575
|
+
self.datastore.client.indices.put_settings,
|
|
2576
|
+
index=self.index_name,
|
|
2577
|
+
settings=write_unblock_settings,
|
|
2578
|
+
)
|
|
2579
|
+
|
|
2580
|
+
# Update index_name to point to the ILM initial index
|
|
2581
|
+
self.index_name = ilm_initial_index
|
|
2582
|
+
|
|
2583
|
+
logger.info("Migration of %s to ILM complete", self.name.upper())
|
|
2584
|
+
else:
|
|
2585
|
+
# Fresh install — create the initial ILM index with alias
|
|
2586
|
+
logger.debug("Creating ILM-managed index %s...", ilm_initial_index)
|
|
2587
|
+
settings = self._get_index_settings()
|
|
2588
|
+
settings["index"]["lifecycle.name"] = f"{self.name}_policy"
|
|
2589
|
+
settings["index"]["lifecycle.rollover_alias"] = self.name
|
|
2590
|
+
|
|
2591
|
+
try:
|
|
2592
|
+
self.with_retries(
|
|
2593
|
+
self.datastore.client.indices.create,
|
|
2594
|
+
index=ilm_initial_index,
|
|
2595
|
+
mappings=self._get_index_mappings(),
|
|
2596
|
+
settings=settings,
|
|
2597
|
+
aliases={self.name: {"is_write_index": True}},
|
|
2598
|
+
)
|
|
2599
|
+
except elasticsearch.exceptions.RequestError as e:
|
|
2600
|
+
if "resource_already_exists_exception" not in str(e):
|
|
2601
|
+
raise
|
|
2602
|
+
logger.warning("ILM index already exists: %s", ilm_initial_index)
|
|
2603
|
+
|
|
2604
|
+
# Update index_name to point to the ILM initial index
|
|
2605
|
+
self.index_name = ilm_initial_index
|
|
2606
|
+
|
|
2607
|
+
self._check_fields()
|
|
2608
|
+
|
|
2433
2609
|
def _add_fields(self, missing_fields: Dict):
|
|
2434
2610
|
no_fix = []
|
|
2435
2611
|
properties = {}
|
|
@@ -2467,6 +2643,13 @@ class ESCollection(Generic[ModelType]):
|
|
|
2467
2643
|
**recursive_update(current_template, {"mappings": {"properties": properties}}),
|
|
2468
2644
|
)
|
|
2469
2645
|
|
|
2646
|
+
# When ILM is enabled, also update the composable index template so
|
|
2647
|
+
# future rollover indices inherit the new field mappings.
|
|
2648
|
+
if self.ilm_config:
|
|
2649
|
+
from howler.odm.models.config import config as _config
|
|
2650
|
+
|
|
2651
|
+
self._create_index_template(_config.datastore.ilm)
|
|
2652
|
+
|
|
2470
2653
|
def wipe(self):
|
|
2471
2654
|
"""This function should completely delete the collection
|
|
2472
2655
|
|
|
@@ -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
|
|
@@ -166,10 +166,14 @@ class ESStore(object):
|
|
|
166
166
|
KeyError: If *name* has not been registered via ``register``.
|
|
167
167
|
"""
|
|
168
168
|
if not self.validate:
|
|
169
|
-
|
|
169
|
+
ilm_cfg = self.__dict__.get("_ilm_configs", {}).get(name)
|
|
170
|
+
return ESCollection(self, name, model_class=self._models[name], validate=self.validate, ilm_config=ilm_cfg)
|
|
170
171
|
|
|
171
172
|
if name not in self._collections:
|
|
172
|
-
|
|
173
|
+
ilm_cfg = self.__dict__.get("_ilm_configs", {}).get(name)
|
|
174
|
+
self._collections[name] = ESCollection(
|
|
175
|
+
self, name, model_class=self._models[name], validate=self.validate, ilm_config=ilm_cfg
|
|
176
|
+
)
|
|
173
177
|
|
|
174
178
|
return self._collections[name]
|
|
175
179
|
|
|
@@ -307,7 +311,7 @@ class ESStore(object):
|
|
|
307
311
|
"""
|
|
308
312
|
return self.client.ping()
|
|
309
313
|
|
|
310
|
-
def register(self, name: str, model_class=None):
|
|
314
|
+
def register(self, name: str, model_class=None, ilm_config=None):
|
|
311
315
|
"""Register a collection (index) name and its optional ODM model class.
|
|
312
316
|
|
|
313
317
|
Args:
|
|
@@ -315,6 +319,7 @@ class ESStore(object):
|
|
|
315
319
|
and underscores.
|
|
316
320
|
model_class: ODM model class used for validation and serialisation.
|
|
317
321
|
``None`` disables model-level validation for this collection.
|
|
322
|
+
ilm_config: Optional per-index ILM configuration (ILMIndexConfig).
|
|
318
323
|
|
|
319
324
|
Raises:
|
|
320
325
|
DataStoreException: If *name* contains invalid characters.
|
|
@@ -326,6 +331,10 @@ class ESStore(object):
|
|
|
326
331
|
)
|
|
327
332
|
|
|
328
333
|
self._models[name] = model_class
|
|
334
|
+
if ilm_config is not None:
|
|
335
|
+
if "_ilm_configs" not in self.__dict__:
|
|
336
|
+
self._ilm_configs: dict = {}
|
|
337
|
+
self._ilm_configs[name] = ilm_config
|
|
329
338
|
|
|
330
339
|
def to_pydatemath(self, value):
|
|
331
340
|
"""Convert an internal date-math expression to ES date-math syntax.
|
|
@@ -93,6 +93,47 @@ class Host(BaseModel):
|
|
|
93
93
|
return self.__repr__()
|
|
94
94
|
|
|
95
95
|
|
|
96
|
+
class ILMIndexConfig(BaseModel):
|
|
97
|
+
"""Per-index ILM phase configuration.
|
|
98
|
+
|
|
99
|
+
Controls when an index transitions to warm and cold phases.
|
|
100
|
+
Values are Elasticsearch age strings (e.g. "30d", "90d").
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
warm: Optional[str] = Field(
|
|
104
|
+
default=None,
|
|
105
|
+
description="Min age before the index enters the warm phase (e.g. '30d'). None to skip.",
|
|
106
|
+
)
|
|
107
|
+
cold: Optional[str] = Field(
|
|
108
|
+
default=None,
|
|
109
|
+
description="Min age before the index enters the cold phase (e.g. '90d'). None to skip.",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class ILMConfig(BaseModel):
|
|
114
|
+
"""Index Lifecycle Management configuration.
|
|
115
|
+
|
|
116
|
+
When enabled, Howler uses Elasticsearch ILM policies and rollover aliases
|
|
117
|
+
to split large indices into time-based segments. This cooperates with the
|
|
118
|
+
existing retention cronjob — ILM handles rollover and phase transitions,
|
|
119
|
+
while the retention job handles document deletion.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
enabled: bool = Field(default=False, description="Enable ILM-based index rollover")
|
|
123
|
+
rollover_max_age: str = Field(
|
|
124
|
+
default="30d",
|
|
125
|
+
description="Maximum age of the write index before rollover (e.g. '30d')",
|
|
126
|
+
)
|
|
127
|
+
rollover_max_size: str = Field(
|
|
128
|
+
default="50gb",
|
|
129
|
+
description="Maximum primary shard size before rollover (e.g. '50gb')",
|
|
130
|
+
)
|
|
131
|
+
indices: dict[str, ILMIndexConfig] = Field(
|
|
132
|
+
default={},
|
|
133
|
+
description="Per-index ILM configuration, keyed by collection name (e.g. 'hit')",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
96
137
|
class Datastore(BaseModel):
|
|
97
138
|
"""Datastore configuration for Howler.
|
|
98
139
|
|
|
@@ -107,6 +148,10 @@ class Datastore(BaseModel):
|
|
|
107
148
|
type: Literal["elasticsearch"] = Field(
|
|
108
149
|
default="elasticsearch", description="Type of application used for the datastore"
|
|
109
150
|
)
|
|
151
|
+
ilm: ILMConfig = Field(
|
|
152
|
+
default_factory=ILMConfig,
|
|
153
|
+
description="Index Lifecycle Management configuration",
|
|
154
|
+
)
|
|
110
155
|
|
|
111
156
|
|
|
112
157
|
class Logging(BaseModel):
|
|
@@ -152,7 +152,7 @@ suppress-none-returning = true
|
|
|
152
152
|
[tool.poetry]
|
|
153
153
|
package-mode = true
|
|
154
154
|
name = "howler-api"
|
|
155
|
-
version = "3.4.0.
|
|
155
|
+
version = "3.4.0.dev927"
|
|
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
|