howler-api 4.0.0.dev1298__tar.gz → 4.0.0.dev1308__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.dev1298 → howler_api-4.0.0.dev1308}/PKG-INFO +1 -1
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/hit.py +9 -11
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/tool.py +14 -11
- howler_api-4.0.0.dev1308/howler/datastore/bulk.py +148 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/datastore/collection.py +21 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/base.py +3 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/config.py +11 -1
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/analytic_service.py +4 -4
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/hit_service.py +40 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/utils/constants.py +1 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/utils/indexes.py +3 -1
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/pyproject.toml +2 -2
- howler_api-4.0.0.dev1298/howler/datastore/bulk.py +0 -72
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/README.md +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/__init__.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/actions/__init__.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/actions/add_label.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/actions/add_to_bundle.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/actions/add_to_case.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/actions/change_field.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/actions/demote.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/actions/example_plugin.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/actions/prioritization.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/actions/promote.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/actions/remove_from_bundle.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/actions/remove_label.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/actions/transition.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/__init__.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/base.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/socket.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/__init__.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/action.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/analytic.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/auth.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/clue.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/configs.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/dossier.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/help.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/notebook.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/overview.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/search.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/template.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/user.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/utils/__init__.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/utils/etag.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/utils/params.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/view.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v2/__init__.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v2/case.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v2/fuzzy.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v2/ingest.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v2/search.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/app.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/common/README.md +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/common/__init__.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/common/classification.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/common/classification.yml +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/common/exceptions.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/common/loader.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/common/logging/__init__.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/common/logging/audit.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/common/logging/format.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/common/net.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/common/net_static.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/common/random_user.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/common/swagger.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/config.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/cronjobs/__init__.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/cronjobs/action_queue_worker.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/cronjobs/correlation.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/cronjobs/retention.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/cronjobs/view_cleanup.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/datastore/README.md +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/datastore/__init__.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/datastore/constants.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/datastore/exceptions.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/datastore/howler_store.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/datastore/migrations/fix_process.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/datastore/operations.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/datastore/schemas.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/datastore/store.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/datastore/support/__init__.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/datastore/support/build.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/datastore/support/schemas.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/datastore/types.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/error.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/external/README.md +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/external/__init__.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/external/generate_mitre.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/external/generate_sigma_rules.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/external/generate_tlds.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/external/reindex_data.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/external/wipe_databases.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/gunicorn_config.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/healthz.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/helper/__init__.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/helper/azure.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/helper/discover.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/helper/hit.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/helper/oauth.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/helper/search.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/helper/workflow.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/helper/ws.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/README.md +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/__init__.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/charter.txt +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/constants.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/helper.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/howler_enum.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/mixins.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/__init__.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/action.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/analytic.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/assemblyline.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/aws.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/azure.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/case.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/cbs.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/clue.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/dossier.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/__init__.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/agent.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/autonomous_system.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/client.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/cloud.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/code_signature.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/container.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/dns.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/egress.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/elf.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/email.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/error.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/event.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/faas.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/file.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/geo.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/group.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/hash.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/host.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/http.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/ingress.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/interface.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/network.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/observer.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/organization.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/os.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/pe.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/process.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/registry.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/related.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/rule.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/server.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/threat.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/tls.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/url.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/user.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/user_agent.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/vulnerability.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/event.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/gcp.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/hit.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/howler_data.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/lead.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/localized_label.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/overview.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/pivot.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/record.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/template.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/user.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/view.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/random_data.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/randomizer.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/patched.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/plugins/__init__.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/plugins/config.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/remote/__init__.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/remote/datatypes/README.md +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/remote/datatypes/__init__.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/remote/datatypes/counters.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/remote/datatypes/events.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/remote/datatypes/hash.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/remote/datatypes/lock.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/remote/datatypes/queues/__init__.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/remote/datatypes/queues/comms.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/remote/datatypes/queues/multi.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/remote/datatypes/queues/named.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/remote/datatypes/queues/priority.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/remote/datatypes/set.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/remote/datatypes/user_quota_tracker.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/security/__init__.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/security/socket.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/security/utils.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/__init__.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/action_service.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/auth_service.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/bundle_compat_service.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/case_service.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/comms_service.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/config_service.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/correlation_service.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/docs_service.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/dossier_service.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/event_service.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/fuzzy_service.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/jwt_service.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/lucene_service.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/notebook_service.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/overview_service.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/search_service.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/template_service.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/user_service.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/viewer_service.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/telemetry.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/utils/__init__.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/utils/annotations.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/utils/chunk.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/utils/compat.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/utils/dict_utils.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/utils/isotime.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/utils/list_utils.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/utils/lucene.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/utils/net_utils.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/utils/path.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/utils/socket_utils.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/utils/str_utils.py +0 -0
- {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/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,
|
|
@@ -523,6 +524,26 @@ class ESCollection(Generic[ModelType]):
|
|
|
523
524
|
else:
|
|
524
525
|
updated += res["updated"]
|
|
525
526
|
|
|
527
|
+
def bulk(self, operations: ElasticBulkPlan, refresh: str | None = None):
|
|
528
|
+
"""
|
|
529
|
+
Execute a bulk plan.
|
|
530
|
+
|
|
531
|
+
:return: True if the operation completed without errors
|
|
532
|
+
"""
|
|
533
|
+
responses = []
|
|
534
|
+
for operation_batch in operations.get_plan_batches():
|
|
535
|
+
response = self.with_retries(self.datastore.client.bulk, operations=operation_batch, refresh=refresh)
|
|
536
|
+
responses.append(response)
|
|
537
|
+
return not any(response["errors"] for response in responses)
|
|
538
|
+
|
|
539
|
+
def get_bulk_plan(self):
|
|
540
|
+
"""
|
|
541
|
+
Create a BulkPlan tailored for the current datastore
|
|
542
|
+
|
|
543
|
+
:return: The BulkPlan object
|
|
544
|
+
"""
|
|
545
|
+
return ElasticBulkPlan(self.index_list, self.model_class)
|
|
546
|
+
|
|
526
547
|
@tracer.start_as_current_span(f"{__name__}.commit")
|
|
527
548
|
def commit(self):
|
|
528
549
|
"""This function should be overloaded to perform a commit of the index data of all the different hosts
|
|
@@ -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):
|
|
@@ -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]:
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
1
3
|
from howler.common.loader import APP_NAME, DATASTORE_INDEX_PREFIX
|
|
2
4
|
from howler.datastore.exceptions import SearchException
|
|
3
5
|
|
|
@@ -53,4 +55,4 @@ def get_logical_index_name(raw_index: str) -> str:
|
|
|
53
55
|
raw_index = raw_index.removeprefix(f"{index_prefix}-")
|
|
54
56
|
break
|
|
55
57
|
|
|
56
|
-
return raw_index.removesuffix("_hot")
|
|
58
|
+
return re.sub(r"-\d+$", "", raw_index).removesuffix("_hot")
|
|
@@ -152,7 +152,7 @@ suppress-none-returning = true
|
|
|
152
152
|
[tool.poetry]
|
|
153
153
|
package-mode = true
|
|
154
154
|
name = "howler-api"
|
|
155
|
-
version = "4.0.0.
|
|
155
|
+
version = "4.0.0.dev1308"
|
|
156
156
|
description = "Howler - API server"
|
|
157
157
|
authors = [
|
|
158
158
|
"Canadian Centre for Cyber Security <howler@cyber.gc.ca>",
|
|
@@ -255,7 +255,7 @@ typing-extensions = "^4.12.2"
|
|
|
255
255
|
types-PyYAML = "6.0.12.20260518"
|
|
256
256
|
types-paramiko = "3.5.0.20250801"
|
|
257
257
|
types-pyOpenSSL = "23.3.0.20240106"
|
|
258
|
-
types-six = "1.17.0.
|
|
258
|
+
types-six = "1.17.0.20260721"
|
|
259
259
|
types-requests = "<2.33.0.20260713"
|
|
260
260
|
websocket-client = "1.9.0"
|
|
261
261
|
types-pytz = "^2024.1.0.20240417"
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
from copy import deepcopy
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class ElasticBulkPlan(object):
|
|
6
|
-
def __init__(self, indexes, model=None):
|
|
7
|
-
self.indexes = indexes
|
|
8
|
-
self.model = model
|
|
9
|
-
self.operations = []
|
|
10
|
-
|
|
11
|
-
@property
|
|
12
|
-
def empty(self):
|
|
13
|
-
return len(self.operations) == 0
|
|
14
|
-
|
|
15
|
-
def add_delete_operation(self, doc_id, index=None):
|
|
16
|
-
if index:
|
|
17
|
-
self.operations.append(json.dumps({"delete": {"_index": index, "_id": doc_id}}))
|
|
18
|
-
else:
|
|
19
|
-
for cur_index in self.indexes:
|
|
20
|
-
self.operations.append(json.dumps({"delete": {"_index": cur_index, "_id": doc_id}}))
|
|
21
|
-
|
|
22
|
-
def add_insert_operation(self, doc_id, doc, index=None):
|
|
23
|
-
if isinstance(doc, self.model):
|
|
24
|
-
saved_doc = doc.as_primitives(hidden_fields=True) # type: ignore
|
|
25
|
-
elif self.model:
|
|
26
|
-
saved_doc = self.model(doc).as_primitives(hidden_fields=True) # type: ignore
|
|
27
|
-
else:
|
|
28
|
-
if not isinstance(doc, dict):
|
|
29
|
-
saved_doc = {"__non_doc_raw__": doc}
|
|
30
|
-
else:
|
|
31
|
-
saved_doc = deepcopy(doc) # type: ignore
|
|
32
|
-
saved_doc["id"] = doc_id
|
|
33
|
-
|
|
34
|
-
self.operations.append(json.dumps({"create": {"_index": index or self.indexes[0], "_id": doc_id}}))
|
|
35
|
-
self.operations.append(json.dumps(saved_doc))
|
|
36
|
-
|
|
37
|
-
def add_upsert_operation(self, doc_id, doc, index=None):
|
|
38
|
-
if isinstance(doc, self.model):
|
|
39
|
-
saved_doc = doc.as_primitives(hidden_fields=True) # type: ignore
|
|
40
|
-
elif self.model:
|
|
41
|
-
saved_doc = self.model(doc).as_primitives(hidden_fields=True) # type: ignore
|
|
42
|
-
else:
|
|
43
|
-
if not isinstance(doc, dict):
|
|
44
|
-
saved_doc = {"__non_doc_raw__": doc} # type: ignore
|
|
45
|
-
else:
|
|
46
|
-
saved_doc = deepcopy(doc) # type: ignore
|
|
47
|
-
saved_doc["id"] = doc_id
|
|
48
|
-
|
|
49
|
-
self.operations.append(json.dumps({"update": {"_index": index or self.indexes[0], "_id": doc_id}}))
|
|
50
|
-
self.operations.append(json.dumps({"doc": saved_doc, "doc_as_upsert": True}))
|
|
51
|
-
|
|
52
|
-
def add_update_operation(self, doc_id, doc, index=None):
|
|
53
|
-
if isinstance(doc, self.model):
|
|
54
|
-
saved_doc = doc.as_primitives(hidden_fields=True) # type: ignore
|
|
55
|
-
elif self.model:
|
|
56
|
-
saved_doc = self.model(doc, mask=list(doc.keys())).as_primitives(hidden_fields=True) # type: ignore
|
|
57
|
-
else:
|
|
58
|
-
if not isinstance(doc, dict):
|
|
59
|
-
saved_doc = {"__non_doc_raw__": doc} # type: ignore
|
|
60
|
-
else:
|
|
61
|
-
saved_doc = deepcopy(doc) # type: ignore
|
|
62
|
-
|
|
63
|
-
if index:
|
|
64
|
-
self.operations.append(json.dumps({"update": {"_index": index, "_id": doc_id}}))
|
|
65
|
-
self.operations.append(json.dumps({"doc": saved_doc}))
|
|
66
|
-
else:
|
|
67
|
-
for cur_index in self.indexes:
|
|
68
|
-
self.operations.append(json.dumps({"update": {"_index": cur_index, "_id": doc_id}}))
|
|
69
|
-
self.operations.append(json.dumps({"doc": saved_doc}))
|
|
70
|
-
|
|
71
|
-
def get_plan_data(self):
|
|
72
|
-
return "\n".join(self.operations)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|