howler-api 4.0.0.dev1299__tar.gz → 4.0.0.dev1311__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.dev1299 → howler_api-4.0.0.dev1311}/PKG-INFO +1 -1
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/hit.py +9 -11
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/tool.py +14 -11
- howler_api-4.0.0.dev1311/howler/datastore/bulk.py +148 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/datastore/collection.py +113 -22
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/external/reindex_data.py +1 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/base.py +3 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/config.py +11 -1
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/random_data.py +5 -1
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/analytic_service.py +4 -4
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/hit_service.py +40 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/utils/constants.py +1 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/pyproject.toml +2 -2
- howler_api-4.0.0.dev1299/howler/datastore/bulk.py +0 -72
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/README.md +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/__init__.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/actions/__init__.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/actions/add_label.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/actions/add_to_bundle.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/actions/add_to_case.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/actions/change_field.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/actions/demote.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/actions/example_plugin.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/actions/prioritization.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/actions/promote.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/actions/remove_from_bundle.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/actions/remove_label.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/actions/transition.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/__init__.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/base.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/socket.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/__init__.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/action.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/analytic.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/auth.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/clue.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/configs.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/dossier.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/help.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/notebook.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/overview.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/search.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/template.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/user.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/utils/__init__.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/utils/etag.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/utils/params.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/view.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v2/__init__.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v2/case.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v2/fuzzy.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v2/ingest.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v2/search.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/app.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/common/README.md +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/common/__init__.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/common/classification.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/common/classification.yml +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/common/exceptions.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/common/loader.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/common/logging/__init__.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/common/logging/audit.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/common/logging/format.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/common/net.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/common/net_static.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/common/random_user.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/common/swagger.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/config.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/cronjobs/__init__.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/cronjobs/action_queue_worker.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/cronjobs/correlation.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/cronjobs/retention.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/cronjobs/view_cleanup.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/datastore/README.md +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/datastore/__init__.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/datastore/constants.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/datastore/exceptions.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/datastore/howler_store.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/datastore/migrations/fix_process.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/datastore/operations.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/datastore/schemas.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/datastore/store.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/datastore/support/__init__.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/datastore/support/build.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/datastore/support/schemas.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/datastore/types.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/error.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/external/README.md +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/external/__init__.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/external/generate_mitre.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/external/generate_sigma_rules.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/external/generate_tlds.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/external/wipe_databases.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/gunicorn_config.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/healthz.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/helper/__init__.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/helper/azure.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/helper/discover.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/helper/hit.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/helper/oauth.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/helper/search.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/helper/workflow.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/helper/ws.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/README.md +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/__init__.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/charter.txt +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/constants.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/helper.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/howler_enum.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/mixins.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/__init__.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/action.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/analytic.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/assemblyline.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/aws.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/azure.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/case.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/cbs.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/clue.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/dossier.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/__init__.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/agent.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/autonomous_system.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/client.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/cloud.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/code_signature.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/container.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/dns.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/egress.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/elf.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/email.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/error.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/event.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/faas.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/file.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/geo.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/group.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/hash.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/host.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/http.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/ingress.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/interface.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/network.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/observer.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/organization.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/os.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/pe.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/process.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/registry.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/related.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/rule.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/server.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/threat.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/tls.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/url.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/user.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/user_agent.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/vulnerability.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/event.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/gcp.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/hit.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/howler_data.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/lead.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/localized_label.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/overview.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/pivot.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/record.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/template.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/user.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/view.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/randomizer.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/patched.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/plugins/__init__.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/plugins/config.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/remote/__init__.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/remote/datatypes/README.md +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/remote/datatypes/__init__.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/remote/datatypes/counters.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/remote/datatypes/events.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/remote/datatypes/hash.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/remote/datatypes/lock.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/remote/datatypes/queues/__init__.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/remote/datatypes/queues/comms.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/remote/datatypes/queues/multi.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/remote/datatypes/queues/named.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/remote/datatypes/queues/priority.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/remote/datatypes/set.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/remote/datatypes/user_quota_tracker.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/security/__init__.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/security/socket.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/security/utils.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/__init__.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/action_service.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/auth_service.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/bundle_compat_service.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/case_service.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/comms_service.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/config_service.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/correlation_service.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/docs_service.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/dossier_service.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/event_service.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/fuzzy_service.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/jwt_service.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/lucene_service.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/notebook_service.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/overview_service.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/search_service.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/template_service.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/user_service.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/viewer_service.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/telemetry.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/utils/__init__.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/utils/annotations.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/utils/chunk.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/utils/compat.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/utils/dict_utils.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/utils/indexes.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/utils/isotime.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/utils/list_utils.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/utils/lucene.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/utils/net_utils.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/utils/path.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/utils/socket_utils.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/utils/str_utils.py +0 -0
- {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/utils/uid.py +0 -0
|
@@ -31,6 +31,7 @@ from howler.odm.models.howler_data import Comment, HitOperationType, HitStatusTr
|
|
|
31
31
|
from howler.odm.models.user import User
|
|
32
32
|
from howler.security import api_login
|
|
33
33
|
from howler.services import action_service, analytic_service, comms_service, hit_service
|
|
34
|
+
from howler.utils.constants import DEBUG_FORCE_REFRESH
|
|
34
35
|
from howler.utils.str_utils import sanitize_lucene_query
|
|
35
36
|
|
|
36
37
|
MAX_COMMENT_LEN = 5000
|
|
@@ -60,7 +61,8 @@ def create_hits(user: User, **kwargs):
|
|
|
60
61
|
None
|
|
61
62
|
|
|
62
63
|
Optional Arguments:
|
|
63
|
-
refresh => ('true') Whether to refresh
|
|
64
|
+
refresh => ('true' | 'false' | 'wait_for') Whether to refresh the datastore before returning.
|
|
65
|
+
'wait_for' will wait for the change to be visible in search.
|
|
64
66
|
|
|
65
67
|
Data Block:
|
|
66
68
|
{
|
|
@@ -121,17 +123,12 @@ def create_hits(user: User, **kwargs):
|
|
|
121
123
|
|
|
122
124
|
if len(response_body["invalid"]) == 0:
|
|
123
125
|
if len(odms) > 0:
|
|
124
|
-
for odm in odms:
|
|
126
|
+
for odm in odms:
|
|
125
127
|
# Ensure all ids are consistent
|
|
126
128
|
if odm.event is not None:
|
|
127
129
|
odm.event.id = odm.howler.id
|
|
128
|
-
# TODO keeping the commit approach until we can batch create hits
|
|
129
|
-
# passing refresh to each one is potentially even more inefficient
|
|
130
|
-
hit_service.create_hit(odm.howler.id, odm, user=user.uname, refresh="false")
|
|
131
|
-
|
|
132
|
-
if refresh == "true":
|
|
133
|
-
datastore().hit.commit()
|
|
134
130
|
|
|
131
|
+
hit_service.create_hits(odms, user=user.uname, refresh="true" if DEBUG_FORCE_REFRESH else refresh)
|
|
135
132
|
analytic_service.save_from_hits(odms, user, refresh=refresh)
|
|
136
133
|
action_service.enqueue_action_execution([odm.howler.id for odm in odms], trigger="create", user=user)
|
|
137
134
|
|
|
@@ -577,7 +574,7 @@ def add_label(id, label_set, user, **kwargs):
|
|
|
577
574
|
id,
|
|
578
575
|
[hit_helper.list_add(f"howler.labels.{label_set}", label) for label in labels],
|
|
579
576
|
user["uname"],
|
|
580
|
-
refresh=refresh,
|
|
577
|
+
refresh="true" if DEBUG_FORCE_REFRESH else refresh,
|
|
581
578
|
)
|
|
582
579
|
|
|
583
580
|
action_service.enqueue_action_execution([id], trigger="add_label", user=user)
|
|
@@ -634,7 +631,7 @@ def remove_labels(id, label_set, user, **kwargs):
|
|
|
634
631
|
id,
|
|
635
632
|
[hit_helper.list_remove(f"howler.labels.{label_set}", label) for label in labels],
|
|
636
633
|
user["uname"],
|
|
637
|
-
refresh=refresh,
|
|
634
|
+
refresh="true" if DEBUG_FORCE_REFRESH else refresh,
|
|
638
635
|
)
|
|
639
636
|
|
|
640
637
|
action_service.enqueue_action_execution([id], trigger="remove_label", user=user)
|
|
@@ -1042,7 +1039,8 @@ def create_bundle(user: User, **kwargs):
|
|
|
1042
1039
|
None
|
|
1043
1040
|
|
|
1044
1041
|
Optional Arguments:
|
|
1045
|
-
refresh => ('true') Whether to refresh
|
|
1042
|
+
refresh => ('true' | 'false' | 'wait_for') Whether to refresh the datastore before returning.
|
|
1043
|
+
'wait_for' will wait for the change to be visible in search.
|
|
1046
1044
|
|
|
1047
1045
|
Data Block:
|
|
1048
1046
|
{
|
|
@@ -3,6 +3,7 @@ from typing import Any, Optional
|
|
|
3
3
|
from flask import request
|
|
4
4
|
|
|
5
5
|
from howler.api import bad_request, created, make_subapi_blueprint
|
|
6
|
+
from howler.api.v1.utils.params import parse_parameters, parse_refresh
|
|
6
7
|
from howler.common.exceptions import HowlerException
|
|
7
8
|
from howler.common.loader import datastore
|
|
8
9
|
from howler.common.logging import get_logger
|
|
@@ -13,6 +14,7 @@ from howler.odm.models.hit import Hit
|
|
|
13
14
|
from howler.odm.models.user import User
|
|
14
15
|
from howler.security import api_login
|
|
15
16
|
from howler.services import action_service, analytic_service, hit_service
|
|
17
|
+
from howler.utils.constants import DEBUG_FORCE_REFRESH
|
|
16
18
|
from howler.utils.dict_utils import flatten
|
|
17
19
|
from howler.utils.isotime import now_as_iso
|
|
18
20
|
from howler.utils.str_utils import get_parent_key
|
|
@@ -30,6 +32,7 @@ hit_helper = OdmHelper(Hit)
|
|
|
30
32
|
@generate_swagger_docs()
|
|
31
33
|
@tool_api.route("/<tool_name>/hits", methods=["POST", "PUT"])
|
|
32
34
|
@api_login(required_priv=["W"])
|
|
35
|
+
@parse_parameters(refresh=parse_refresh)
|
|
33
36
|
def create_one_or_many_hits(tool_name: str, user: User, **kwargs): # noqa: C901
|
|
34
37
|
"""Create one or many hits for a tool using field mapping.
|
|
35
38
|
|
|
@@ -39,6 +42,10 @@ def create_one_or_many_hits(tool_name: str, user: User, **kwargs): # noqa: C901
|
|
|
39
42
|
Arguments:
|
|
40
43
|
None
|
|
41
44
|
|
|
45
|
+
Optional Arguments:
|
|
46
|
+
refresh => ('true' | 'false' | 'wait_for') Whether to refresh the datastore before returning.
|
|
47
|
+
'wait_for' will wait for the change to be visible in search.
|
|
48
|
+
|
|
42
49
|
Data Block:
|
|
43
50
|
{
|
|
44
51
|
"map": { # For each field in the hit, list of field data will be copied to
|
|
@@ -64,6 +71,7 @@ def create_one_or_many_hits(tool_name: str, user: User, **kwargs): # noqa: C901
|
|
|
64
71
|
Use POST /api/v1/hit/ directly with pre-mapped hit data instead.
|
|
65
72
|
"""
|
|
66
73
|
data = request.json
|
|
74
|
+
refresh = kwargs.get("refresh", None)
|
|
67
75
|
if not isinstance(data, dict):
|
|
68
76
|
return bad_request(err="Invalid data format")
|
|
69
77
|
|
|
@@ -204,10 +212,9 @@ def create_one_or_many_hits(tool_name: str, user: User, **kwargs): # noqa: C901
|
|
|
204
212
|
if bundle_odm is not None:
|
|
205
213
|
child_odms = [odm for odm in odms if odm.howler.id != bundle_id]
|
|
206
214
|
|
|
207
|
-
|
|
208
|
-
|
|
215
|
+
hit_service.create_hits(child_odms, user=user.uname, refresh="true" if DEBUG_FORCE_REFRESH else refresh)
|
|
216
|
+
analytic_service.save_from_hits(odms, user, refresh=refresh)
|
|
209
217
|
|
|
210
|
-
analytic_service.save_from_hits(odms, user)
|
|
211
218
|
child_ids = [odm.howler.id for odm in child_odms]
|
|
212
219
|
bundle_data = bundle_odm.as_primitives()
|
|
213
220
|
result = bundle_compat_service.create_bundle(bundle_data, child_ids, user=user, refresh="wait_for")
|
|
@@ -220,15 +227,11 @@ def create_one_or_many_hits(tool_name: str, user: User, **kwargs): # noqa: C901
|
|
|
220
227
|
entry["_case_id"] = result.get("_case_id")
|
|
221
228
|
else:
|
|
222
229
|
# The bundle hit may have been dropped by de-duplication; ingest remaining hits directly.
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
analytic_service.save_from_hits(odms, user)
|
|
230
|
+
hit_service.create_hits(odms, user=user.uname, refresh="true" if DEBUG_FORCE_REFRESH else refresh)
|
|
231
|
+
analytic_service.save_from_hits(odms, user, refresh=refresh)
|
|
226
232
|
else:
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
analytic_service.save_from_hits(odms, user)
|
|
230
|
-
|
|
231
|
-
datastore().hit.commit()
|
|
233
|
+
hit_service.create_hits(odms, user=user.uname, refresh="true" if DEBUG_FORCE_REFRESH else refresh)
|
|
234
|
+
analytic_service.save_from_hits(odms, user, refresh=refresh)
|
|
232
235
|
|
|
233
236
|
action_service.enqueue_action_execution([entry["id"] for entry in out], trigger="create", user=user)
|
|
234
237
|
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import warnings
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
|
|
6
|
+
from howler import odm
|
|
7
|
+
from howler.config import config
|
|
8
|
+
|
|
9
|
+
_OPERATION_GROUP = tuple[str] | tuple[str, str]
|
|
10
|
+
|
|
11
|
+
ELASTIC_MAX_REQUEST_SIZE = config.datastore.max_request_size
|
|
12
|
+
DEFAULT_BATCH_SIZE = config.datastore.request_batch_size
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ElasticBulkPlan(object):
|
|
16
|
+
def __init__(self, indexes: List[str], model: Optional[type[odm.Model]] = None):
|
|
17
|
+
self.indexes = indexes
|
|
18
|
+
self.model = model
|
|
19
|
+
self.operations: list[_OPERATION_GROUP] = []
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def empty(self):
|
|
23
|
+
return len(self.operations) == 0
|
|
24
|
+
|
|
25
|
+
def add_delete_operation(self, doc_id, index=None):
|
|
26
|
+
if index:
|
|
27
|
+
self.operations.append((json.dumps({"delete": {"_index": index, "_id": doc_id}}),))
|
|
28
|
+
else:
|
|
29
|
+
for cur_index in self.indexes:
|
|
30
|
+
self.operations.append((json.dumps({"delete": {"_index": cur_index, "_id": doc_id}}),))
|
|
31
|
+
|
|
32
|
+
def add_insert_operation(self, doc_id: str, doc, index=None):
|
|
33
|
+
if self.model and isinstance(doc, self.model):
|
|
34
|
+
saved_doc = doc.as_primitives(hidden_fields=True)
|
|
35
|
+
elif self.model:
|
|
36
|
+
saved_doc = self.model(doc).as_primitives(hidden_fields=True)
|
|
37
|
+
else:
|
|
38
|
+
if not isinstance(doc, dict):
|
|
39
|
+
saved_doc = {"__non_doc_raw__": doc}
|
|
40
|
+
else:
|
|
41
|
+
saved_doc = deepcopy(doc)
|
|
42
|
+
saved_doc["id"] = doc_id
|
|
43
|
+
|
|
44
|
+
self.operations.append(
|
|
45
|
+
(
|
|
46
|
+
json.dumps({"create": {"_index": index or self.indexes[0], "_id": doc_id}}),
|
|
47
|
+
json.dumps(saved_doc),
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def add_index_operation(self, doc_id, doc, index=None):
|
|
52
|
+
if self.model and isinstance(doc, self.model):
|
|
53
|
+
saved_doc = doc.as_primitives(hidden_fields=True)
|
|
54
|
+
elif self.model:
|
|
55
|
+
saved_doc = self.model(doc).as_primitives(hidden_fields=True)
|
|
56
|
+
else:
|
|
57
|
+
if not isinstance(doc, dict):
|
|
58
|
+
saved_doc = {"__non_doc_raw__": doc}
|
|
59
|
+
else:
|
|
60
|
+
saved_doc = deepcopy(doc)
|
|
61
|
+
saved_doc["id"] = doc_id
|
|
62
|
+
|
|
63
|
+
self.operations.append(
|
|
64
|
+
(
|
|
65
|
+
json.dumps({"index": {"_index": index or self.indexes[0], "_id": doc_id}}),
|
|
66
|
+
json.dumps(saved_doc),
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def add_upsert_operation(self, doc_id, doc, index=None):
|
|
71
|
+
if self.model and isinstance(doc, self.model):
|
|
72
|
+
saved_doc = doc.as_primitives(hidden_fields=True)
|
|
73
|
+
elif self.model:
|
|
74
|
+
saved_doc = self.model(doc).as_primitives(hidden_fields=True)
|
|
75
|
+
else:
|
|
76
|
+
if not isinstance(doc, dict):
|
|
77
|
+
saved_doc = {"__non_doc_raw__": doc}
|
|
78
|
+
else:
|
|
79
|
+
saved_doc = deepcopy(doc)
|
|
80
|
+
saved_doc["id"] = doc_id
|
|
81
|
+
|
|
82
|
+
self.operations.append(
|
|
83
|
+
(
|
|
84
|
+
json.dumps({"update": {"_index": index or self.indexes[0], "_id": doc_id}}),
|
|
85
|
+
json.dumps({"doc": saved_doc, "doc_as_upsert": True}),
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def add_update_operation(self, doc_id, doc, index=None):
|
|
90
|
+
if self.model and isinstance(doc, self.model):
|
|
91
|
+
saved_doc = doc.as_primitives(hidden_fields=True)
|
|
92
|
+
elif self.model:
|
|
93
|
+
saved_doc = self.model(doc, mask=list(doc.keys())).as_primitives(hidden_fields=True)
|
|
94
|
+
else:
|
|
95
|
+
if not isinstance(doc, dict):
|
|
96
|
+
saved_doc = {"__non_doc_raw__": doc}
|
|
97
|
+
else:
|
|
98
|
+
saved_doc = deepcopy(doc)
|
|
99
|
+
|
|
100
|
+
if index:
|
|
101
|
+
self.operations.append(
|
|
102
|
+
(
|
|
103
|
+
json.dumps({"update": {"_index": index, "_id": doc_id}}),
|
|
104
|
+
json.dumps({"doc": saved_doc}),
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
else:
|
|
108
|
+
for cur_index in self.indexes:
|
|
109
|
+
self.operations.append(
|
|
110
|
+
(
|
|
111
|
+
json.dumps({"update": {"_index": cur_index, "_id": doc_id}}),
|
|
112
|
+
json.dumps({"doc": saved_doc}),
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def get_plan_data(self):
|
|
117
|
+
"""Construct the bulk request from the current operations"""
|
|
118
|
+
return self._get_plan_for_operations()
|
|
119
|
+
|
|
120
|
+
def get_plan_batches(self, batch_size: int | None = DEFAULT_BATCH_SIZE):
|
|
121
|
+
"""Yield plan data in batches"""
|
|
122
|
+
if batch_size is None:
|
|
123
|
+
batch_size = len(self.operations)
|
|
124
|
+
for ptr in range(0, len(self.operations), batch_size):
|
|
125
|
+
yield self._get_plan_for_operations(self.operations[ptr : ptr + batch_size])
|
|
126
|
+
|
|
127
|
+
def _flatten_operations(self, batch: List[_OPERATION_GROUP] | None = None) -> List[str]:
|
|
128
|
+
"""Flatten the (operation, data) tuples for a batch or the full list of operations if no batch provided"""
|
|
129
|
+
if not batch:
|
|
130
|
+
batch = self.operations
|
|
131
|
+
|
|
132
|
+
flattened: list[str] = []
|
|
133
|
+
for op in batch:
|
|
134
|
+
flattened.extend(op)
|
|
135
|
+
|
|
136
|
+
return flattened
|
|
137
|
+
|
|
138
|
+
def _get_plan_for_operations(self, batch: List[_OPERATION_GROUP] | None = None) -> str:
|
|
139
|
+
"""Get the bulk plan string for a batch or the full list of operations if no batch provided"""
|
|
140
|
+
plan = "\n".join(self._flatten_operations(batch)) + "\n"
|
|
141
|
+
|
|
142
|
+
if ELASTIC_MAX_REQUEST_SIZE and len(plan.encode("utf-8")) > ELASTIC_MAX_REQUEST_SIZE:
|
|
143
|
+
warnings.warn(
|
|
144
|
+
f"Bulk plan exceeds maximum request size of {ELASTIC_MAX_REQUEST_SIZE} bytes. "
|
|
145
|
+
f"Current size: {len(plan.encode('utf-8'))} bytes."
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return plan
|
|
@@ -22,6 +22,7 @@ from howler import odm
|
|
|
22
22
|
from howler.common.exceptions import HowlerRuntimeError, HowlerValueError, NonRecoverableError
|
|
23
23
|
from howler.common.loader import DATASTORE_INDEX_PREFIX
|
|
24
24
|
from howler.common.logging.format import HWL_DATE_FORMAT, HWL_LOG_FORMAT
|
|
25
|
+
from howler.datastore.bulk import ElasticBulkPlan
|
|
25
26
|
from howler.datastore.constants import BACK_MAPPING, TYPE_MAPPING
|
|
26
27
|
from howler.datastore.exceptions import (
|
|
27
28
|
DataStoreException,
|
|
@@ -251,19 +252,50 @@ class ESCollection(Generic[ModelType]):
|
|
|
251
252
|
|
|
252
253
|
@property
|
|
253
254
|
def index_list_full(self):
|
|
255
|
+
"""Return every physical index for this collection, newest ILM index first.
|
|
256
|
+
|
|
257
|
+
Maintenance operations that must visit historical rollover indexes should use this
|
|
258
|
+
property. The active index is included even when no ILM rollover index exists.
|
|
259
|
+
"""
|
|
254
260
|
if not self._index_list:
|
|
255
|
-
self._index_list =
|
|
261
|
+
self._index_list = self._get_ilm_index_names()
|
|
256
262
|
|
|
257
|
-
return [self.index_name
|
|
263
|
+
return list(dict.fromkeys([self.index_name, *sorted(self._index_list, reverse=True)]))
|
|
258
264
|
|
|
259
265
|
@property
|
|
260
266
|
def index_list(self):
|
|
261
|
-
"""
|
|
267
|
+
"""Return the active physical write index for this collection.
|
|
262
268
|
|
|
263
|
-
|
|
269
|
+
Historical ILM rollover indexes are intentionally excluded. Bulk plans use this
|
|
270
|
+
list for default write, update, and delete targets, and widening it would fan those
|
|
271
|
+
operations out across historical indexes.
|
|
264
272
|
"""
|
|
265
273
|
return [self.index_name]
|
|
266
274
|
|
|
275
|
+
def _get_ilm_index_names(self) -> list[str]:
|
|
276
|
+
"""Return physical ILM rollover indexes, excluding temporary maintenance indexes."""
|
|
277
|
+
try:
|
|
278
|
+
indices = self.datastore.client.indices.get(
|
|
279
|
+
index=f"{self.name}-0*",
|
|
280
|
+
ignore_unavailable=True,
|
|
281
|
+
)
|
|
282
|
+
except elasticsearch.exceptions.NotFoundError:
|
|
283
|
+
return []
|
|
284
|
+
|
|
285
|
+
index_pattern = re.compile(rf"^{re.escape(self.name)}-\d{{6}}$")
|
|
286
|
+
return sorted(index for index in indices if index_pattern.fullmatch(index))
|
|
287
|
+
|
|
288
|
+
def _refresh_ilm_index_name(self):
|
|
289
|
+
"""Set ``index_name`` to the latest existing ILM index, when one exists."""
|
|
290
|
+
if not self.ilm_config:
|
|
291
|
+
return
|
|
292
|
+
|
|
293
|
+
ilm_indices = self._get_ilm_index_names()
|
|
294
|
+
|
|
295
|
+
if ilm_indices:
|
|
296
|
+
self._index_list = ilm_indices
|
|
297
|
+
self.index_name = ilm_indices[-1]
|
|
298
|
+
|
|
267
299
|
def scan_with_retry(
|
|
268
300
|
self,
|
|
269
301
|
query,
|
|
@@ -523,6 +555,26 @@ class ESCollection(Generic[ModelType]):
|
|
|
523
555
|
else:
|
|
524
556
|
updated += res["updated"]
|
|
525
557
|
|
|
558
|
+
def bulk(self, operations: ElasticBulkPlan, refresh: str | None = None):
|
|
559
|
+
"""
|
|
560
|
+
Execute a bulk plan.
|
|
561
|
+
|
|
562
|
+
:return: True if the operation completed without errors
|
|
563
|
+
"""
|
|
564
|
+
responses = []
|
|
565
|
+
for operation_batch in operations.get_plan_batches():
|
|
566
|
+
response = self.with_retries(self.datastore.client.bulk, operations=operation_batch, refresh=refresh)
|
|
567
|
+
responses.append(response)
|
|
568
|
+
return not any(response["errors"] for response in responses)
|
|
569
|
+
|
|
570
|
+
def get_bulk_plan(self):
|
|
571
|
+
"""
|
|
572
|
+
Create a BulkPlan tailored for the current datastore
|
|
573
|
+
|
|
574
|
+
:return: The BulkPlan object
|
|
575
|
+
"""
|
|
576
|
+
return ElasticBulkPlan(self.index_list, self.model_class)
|
|
577
|
+
|
|
526
578
|
@tracer.start_as_current_span(f"{__name__}.commit")
|
|
527
579
|
def commit(self):
|
|
528
580
|
"""This function should be overloaded to perform a commit of the index data of all the different hosts
|
|
@@ -709,7 +761,7 @@ class ESCollection(Generic[ModelType]):
|
|
|
709
761
|
def reindex(self, allow_failures: bool = False, request_timeout: int = 60):
|
|
710
762
|
"""Reindex all the data of the collection into a freshly mapped index.
|
|
711
763
|
|
|
712
|
-
For every index in ``self.
|
|
764
|
+
For every physical index in ``self.index_list_full`` the data is copied into a temporary
|
|
713
765
|
``__reindex`` index that uses the current mappings/settings. Writes to the source
|
|
714
766
|
index are blocked while the copy runs so the reindex result and document counts can
|
|
715
767
|
be validated before the original index is deleted. The temporary index is only
|
|
@@ -729,7 +781,8 @@ class ESCollection(Generic[ModelType]):
|
|
|
729
781
|
operation can be recovered with :meth:`reindex_cleanup`.
|
|
730
782
|
"""
|
|
731
783
|
logger.warning("Beginning Reindex")
|
|
732
|
-
|
|
784
|
+
self._refresh_ilm_index_name()
|
|
785
|
+
for index in self.index_list_full:
|
|
733
786
|
new_name = f"{index}__reindex"
|
|
734
787
|
index_data = None
|
|
735
788
|
source_count = None
|
|
@@ -777,7 +830,7 @@ class ESCollection(Generic[ModelType]):
|
|
|
777
830
|
self.datastore.client.indices.create,
|
|
778
831
|
index=new_name,
|
|
779
832
|
mappings=self._get_index_mappings(),
|
|
780
|
-
settings=self.
|
|
833
|
+
settings=self._get_reindex_settings(index_data),
|
|
781
834
|
)
|
|
782
835
|
|
|
783
836
|
# Reindex data into target
|
|
@@ -826,12 +879,10 @@ class ESCollection(Generic[ModelType]):
|
|
|
826
879
|
)
|
|
827
880
|
|
|
828
881
|
alias_actions = []
|
|
829
|
-
aliases = index_data.get("aliases", {})
|
|
882
|
+
aliases = index_data.get("aliases", {})
|
|
830
883
|
for alias, alias_data in aliases.items():
|
|
831
884
|
alias_action = {"index": index, "alias": alias}
|
|
832
885
|
alias_action.update(alias_data)
|
|
833
|
-
if alias == self.name or alias_data.get("is_write_index", False):
|
|
834
|
-
alias_action["is_write_index"] = True
|
|
835
886
|
alias_actions.append({"add": alias_action})
|
|
836
887
|
alias_actions.append({"remove_index": {"index": new_name}})
|
|
837
888
|
self.with_retries(self.datastore.client.indices.update_aliases, actions=alias_actions)
|
|
@@ -859,6 +910,14 @@ class ESCollection(Generic[ModelType]):
|
|
|
859
910
|
|
|
860
911
|
return True
|
|
861
912
|
|
|
913
|
+
def _get_reindex_settings(self, index_data: dict) -> dict:
|
|
914
|
+
"""Build settings for a reindex target without dropping ILM lifecycle metadata."""
|
|
915
|
+
settings = self._get_index_settings()
|
|
916
|
+
lifecycle = index_data.get("settings", {}).get("index", {}).get("lifecycle")
|
|
917
|
+
if lifecycle:
|
|
918
|
+
settings["index"]["lifecycle"] = deepcopy(lifecycle)
|
|
919
|
+
return settings
|
|
920
|
+
|
|
862
921
|
def _validate_reindex_result(self, index, new_name, reindex_result, allow_failures):
|
|
863
922
|
"""Validate the result of an Elasticsearch reindex task.
|
|
864
923
|
|
|
@@ -935,17 +994,18 @@ class ESCollection(Generic[ModelType]):
|
|
|
935
994
|
def reindex_cleanup(self):
|
|
936
995
|
"""Recover from a failed or interrupted :meth:`reindex` run.
|
|
937
996
|
|
|
938
|
-
For every index that still has a leftover ``__reindex`` index, restore the source
|
|
939
|
-
index
|
|
940
|
-
|
|
941
|
-
|
|
997
|
+
For every physical index that still has a leftover ``__reindex`` index, restore the source
|
|
998
|
+
index's original aliases and delete the orphaned ``__reindex`` index. If the source
|
|
999
|
+
index is missing, raise an error instead of deleting ``__reindex`` because it may
|
|
1000
|
+
be the only remaining copy of the data.
|
|
942
1001
|
|
|
943
1002
|
Write blocks set during reindexing are cleared so the collection is usable again.
|
|
944
1003
|
|
|
945
1004
|
:return: ``True`` when cleanup completed on all indexes
|
|
946
1005
|
"""
|
|
947
1006
|
logger.warning("Beginning reindex cleanup")
|
|
948
|
-
|
|
1007
|
+
self._refresh_ilm_index_name()
|
|
1008
|
+
for index in self.index_list_full:
|
|
949
1009
|
new_name = f"{index}__reindex"
|
|
950
1010
|
|
|
951
1011
|
if not self.with_retries(self.datastore.client.indices.exists, index=new_name):
|
|
@@ -966,17 +1026,13 @@ class ESCollection(Generic[ModelType]):
|
|
|
966
1026
|
reindex_aliases = new_index_data.get("aliases", {})
|
|
967
1027
|
|
|
968
1028
|
alias_actions = []
|
|
969
|
-
for alias in sorted(set(source_aliases) | set(reindex_aliases)
|
|
1029
|
+
for alias in sorted(set(source_aliases) | set(reindex_aliases)):
|
|
970
1030
|
source_alias_data = source_aliases.get(alias, {})
|
|
971
1031
|
reindex_alias_data = reindex_aliases.get(alias, {})
|
|
972
1032
|
|
|
973
1033
|
add_alias_data = {"index": index, "alias": alias}
|
|
974
1034
|
add_alias_data.update(source_alias_data)
|
|
975
|
-
if (
|
|
976
|
-
alias == self.name
|
|
977
|
-
or source_alias_data.get("is_write_index", False)
|
|
978
|
-
or reindex_alias_data.get("is_write_index", False)
|
|
979
|
-
):
|
|
1035
|
+
if source_alias_data.get("is_write_index", False) or reindex_alias_data.get("is_write_index", False):
|
|
980
1036
|
add_alias_data["is_write_index"] = True
|
|
981
1037
|
|
|
982
1038
|
alias_actions.append({"add": add_alias_data})
|
|
@@ -2884,7 +2940,42 @@ class ESCollection(Generic[ModelType]):
|
|
|
2884
2940
|
if existing_ilm_indices:
|
|
2885
2941
|
# ILM already bootstrapped — ensure the alias exists
|
|
2886
2942
|
latest = sorted(existing_ilm_indices)[-1]
|
|
2887
|
-
|
|
2943
|
+
legacy_hot_index = f"{self.name}_hot"
|
|
2944
|
+
if self.with_retries(self.datastore.client.indices.exists_alias, name=self.name):
|
|
2945
|
+
alias_indices = self.with_retries(self.datastore.client.indices.get_alias, name=self.name)
|
|
2946
|
+
alias_actions = []
|
|
2947
|
+
for alias_index, alias_index_data in alias_indices.items():
|
|
2948
|
+
alias_data = alias_index_data.get("aliases", {}).get(self.name, {})
|
|
2949
|
+
if alias_index == legacy_hot_index:
|
|
2950
|
+
alias_actions.append({"remove": {"index": alias_index, "alias": self.name}})
|
|
2951
|
+
elif alias_index != latest and alias_data.get("is_write_index", False):
|
|
2952
|
+
alias_actions.append(
|
|
2953
|
+
{
|
|
2954
|
+
"add": {
|
|
2955
|
+
"index": alias_index,
|
|
2956
|
+
"alias": self.name,
|
|
2957
|
+
**alias_data,
|
|
2958
|
+
"is_write_index": False,
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
)
|
|
2962
|
+
|
|
2963
|
+
latest_alias_data = alias_indices.get(latest, {}).get("aliases", {}).get(self.name, {})
|
|
2964
|
+
if not latest_alias_data.get("is_write_index", False):
|
|
2965
|
+
alias_actions.append(
|
|
2966
|
+
{
|
|
2967
|
+
"add": {
|
|
2968
|
+
"index": latest,
|
|
2969
|
+
"alias": self.name,
|
|
2970
|
+
**latest_alias_data,
|
|
2971
|
+
"is_write_index": True,
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
)
|
|
2975
|
+
|
|
2976
|
+
if alias_actions:
|
|
2977
|
+
self.with_retries(self.datastore.client.indices.update_aliases, actions=alias_actions)
|
|
2978
|
+
else:
|
|
2888
2979
|
# Find the latest index to set as write index
|
|
2889
2980
|
self.with_retries(
|
|
2890
2981
|
self.datastore.client.indices.put_alias,
|
|
@@ -1358,6 +1358,9 @@ class Model:
|
|
|
1358
1358
|
if data is None:
|
|
1359
1359
|
data = {}
|
|
1360
1360
|
|
|
1361
|
+
if "__index" in data:
|
|
1362
|
+
data.pop("__index") # Ignore the __index field sometimes generated by as_primitives
|
|
1363
|
+
|
|
1361
1364
|
if not hasattr(data, "items"):
|
|
1362
1365
|
raise HowlerTypeError(f"'{self.__class__.__name__}' object must be constructed with dict like")
|
|
1363
1366
|
self._odm_py_obj = {}
|
|
@@ -149,7 +149,15 @@ class Datastore(BaseModel):
|
|
|
149
149
|
"""
|
|
150
150
|
|
|
151
151
|
hosts: list[Host] = Field(
|
|
152
|
-
default=[
|
|
152
|
+
default=[
|
|
153
|
+
Host(
|
|
154
|
+
name="elastic",
|
|
155
|
+
username="elastic",
|
|
156
|
+
password="devpass", # noqa: S106
|
|
157
|
+
scheme="http",
|
|
158
|
+
host="localhost:9200",
|
|
159
|
+
)
|
|
160
|
+
],
|
|
153
161
|
description="List of hosts used for the datastore",
|
|
154
162
|
)
|
|
155
163
|
type: Literal["elasticsearch"] = Field(
|
|
@@ -159,6 +167,8 @@ class Datastore(BaseModel):
|
|
|
159
167
|
default_factory=ILMConfig,
|
|
160
168
|
description="Index Lifecycle Management configuration",
|
|
161
169
|
)
|
|
170
|
+
max_request_size: Optional[int] = Field(description="Maximum request size in bytes", default=100_000_000) # 100MB
|
|
171
|
+
request_batch_size: Optional[int] = Field(description="Default number of operations for bulk requests", default=500)
|
|
162
172
|
|
|
163
173
|
|
|
164
174
|
class Logging(BaseModel):
|
|
@@ -867,8 +867,12 @@ def wipe_actions(ds: HowlerDatastore):
|
|
|
867
867
|
def create_dossiers(ds: HowlerDatastore, num_dossiers: int = 5):
|
|
868
868
|
"Create random dossiers"
|
|
869
869
|
users = ds.user.search("*:*")["items"]
|
|
870
|
-
for
|
|
870
|
+
for index in range(num_dossiers):
|
|
871
871
|
dossier = generate_useful_dossier(users)
|
|
872
|
+
if index == 0:
|
|
873
|
+
dossier.type = "personal"
|
|
874
|
+
elif index == 1:
|
|
875
|
+
dossier.type = "global"
|
|
872
876
|
ds.dossier.save(dossier.dossier_id, dossier)
|
|
873
877
|
|
|
874
878
|
ds.dossier.commit()
|
|
@@ -123,11 +123,11 @@ def save_from_hits(
|
|
|
123
123
|
analytics.append(analytic_update)
|
|
124
124
|
|
|
125
125
|
if analytics:
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
bulk_plan = storage.analytic.get_bulk_plan()
|
|
127
|
+
for analytic in analytics:
|
|
128
|
+
bulk_plan.add_index_operation(analytic.analytic_id, analytic)
|
|
128
129
|
|
|
129
|
-
|
|
130
|
-
storage.analytic.save(analytics[-1].analytic_id, analytics[-1], refresh=refresh)
|
|
130
|
+
storage.analytic.bulk(bulk_plan, refresh=refresh)
|
|
131
131
|
|
|
132
132
|
|
|
133
133
|
def _get_analytic_updates_from_hit_group(analytic_name: str, hit_group: list[Hit], user: User) -> Analytic | None:
|
|
@@ -442,6 +442,31 @@ def create_hit(
|
|
|
442
442
|
return datastore().hit.save(id, hit, refresh=refresh)
|
|
443
443
|
|
|
444
444
|
|
|
445
|
+
@tracer.start_as_current_span(f"{__name__}.create_hits")
|
|
446
|
+
def create_hits(
|
|
447
|
+
hits: list[Hit], user: Optional[str] = None, overwrite: bool = False, refresh: str | None = None
|
|
448
|
+
) -> bool:
|
|
449
|
+
"""Bulk create multiple hits in the database.
|
|
450
|
+
|
|
451
|
+
Similar to create_hit for batch.
|
|
452
|
+
Will raise and abort the entire batch if any hit already exists and overwrite=False.
|
|
453
|
+
"""
|
|
454
|
+
storage = datastore()
|
|
455
|
+
bulk_plan = storage.hit.get_bulk_plan()
|
|
456
|
+
|
|
457
|
+
for hit in hits:
|
|
458
|
+
if not overwrite and storage.hit.exists(hit.howler.id):
|
|
459
|
+
raise ResourceExists("Hit %s already exists in datastore" % hit.howler.id)
|
|
460
|
+
|
|
461
|
+
if user:
|
|
462
|
+
hit.howler.log = [Log({"timestamp": "NOW", "explanation": "Created hit", "user": user})]
|
|
463
|
+
|
|
464
|
+
CREATED_HITS.labels(hit.howler.analytic).inc()
|
|
465
|
+
bulk_plan.add_insert_operation(hit.howler.id, hit)
|
|
466
|
+
|
|
467
|
+
return storage.hit.bulk(bulk_plan, refresh=refresh)
|
|
468
|
+
|
|
469
|
+
|
|
445
470
|
@tracer.start_as_current_span(f"{__name__}.update_hit")
|
|
446
471
|
def update_hit(
|
|
447
472
|
hit_id: str,
|
|
@@ -476,6 +501,21 @@ def update_hit(
|
|
|
476
501
|
return _update_hit(hit_id, operations, user, version=version, refresh=refresh)
|
|
477
502
|
|
|
478
503
|
|
|
504
|
+
@tracer.start_as_current_span(f"{__name__}.overwrite_hits")
|
|
505
|
+
def overwrite_hits(hits: list[Hit], refresh: str | None = None) -> bool:
|
|
506
|
+
"""Bulk save multiple hits to the datastore.
|
|
507
|
+
|
|
508
|
+
Similar to save_hit for batch without versioning. Will overwrite existing hits with the same ID.
|
|
509
|
+
"""
|
|
510
|
+
storage = datastore()
|
|
511
|
+
bulk_plan = storage.hit.get_bulk_plan()
|
|
512
|
+
|
|
513
|
+
for hit in hits:
|
|
514
|
+
bulk_plan.add_index_operation(hit.howler.id, hit)
|
|
515
|
+
|
|
516
|
+
return storage.hit.bulk(bulk_plan, refresh=refresh)
|
|
517
|
+
|
|
518
|
+
|
|
479
519
|
@typing.no_type_check
|
|
480
520
|
@tracer.start_as_current_span(f"{__name__}.save_hit")
|
|
481
521
|
def save_hit(hit: Hit, version: Optional[str] = None, refresh: str | None = None) -> tuple[Hit, str]:
|