howler-api 4.1.0.dev1368__tar.gz → 4.1.0.dev1387__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/PKG-INFO +1 -1
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/actions/add_to_case.py +10 -13
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/api/v2/ingest.py +9 -7
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/app.py +21 -4
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/config.py +1 -1
- howler_api-4.1.0.dev1387/howler/datastore/bulk.py +272 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/datastore/collection.py +11 -18
- howler_api-4.1.0.dev1387/howler/datastore/utils.py +59 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/remote/datatypes/__init__.py +1 -1
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/remote/datatypes/queues/named.py +5 -4
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/services/case_service.py +71 -55
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/services/correlation_service.py +165 -43
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/services/event_service.py +28 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/services/hit_service.py +4 -1
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/pyproject.toml +1 -1
- howler_api-4.1.0.dev1368/howler/datastore/bulk.py +0 -148
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/README.md +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/__init__.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/actions/__init__.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/actions/add_label.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/actions/add_to_bundle.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/actions/change_field.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/actions/demote.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/actions/example_plugin.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/actions/prioritization.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/actions/promote.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/actions/remove_from_bundle.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/actions/remove_label.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/actions/transition.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/api/__init__.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/api/base.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/api/socket.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/api/v1/__init__.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/api/v1/action.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/api/v1/analytic.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/api/v1/auth.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/api/v1/clue.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/api/v1/configs.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/api/v1/dossier.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/api/v1/help.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/api/v1/hit.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/api/v1/notebook.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/api/v1/overview.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/api/v1/search.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/api/v1/template.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/api/v1/tool.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/api/v1/user.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/api/v1/utils/__init__.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/api/v1/utils/etag.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/api/v1/utils/params.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/api/v1/view.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/api/v2/__init__.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/api/v2/case.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/api/v2/fuzzy.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/api/v2/search.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/common/README.md +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/common/__init__.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/common/classification.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/common/classification.yml +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/common/exceptions.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/common/loader.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/common/logging/__init__.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/common/logging/audit.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/common/logging/format.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/common/net.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/common/net_static.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/common/random_user.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/common/swagger.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/cronjobs/__init__.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/cronjobs/action_queue_worker.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/cronjobs/correlation.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/cronjobs/retention.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/cronjobs/view_cleanup.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/datastore/README.md +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/datastore/__init__.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/datastore/constants.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/datastore/exceptions.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/datastore/howler_store.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/datastore/migrations/fix_process.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/datastore/operations.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/datastore/schemas.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/datastore/store.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/datastore/support/__init__.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/datastore/support/build.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/datastore/support/schemas.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/datastore/types.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/error.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/external/README.md +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/external/__init__.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/external/generate_mitre.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/external/generate_sigma_rules.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/external/generate_tlds.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/external/reindex_data.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/external/wipe_databases.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/gunicorn_config.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/healthz.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/helper/__init__.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/helper/azure.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/helper/discover.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/helper/hit.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/helper/oauth.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/helper/search.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/helper/workflow.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/helper/ws.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/README.md +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/__init__.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/base.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/charter.txt +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/constants.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/helper.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/howler_enum.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/mixins.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/__init__.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/action.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/analytic.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/assemblyline.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/aws.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/azure.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/case.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/cbs.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/clue.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/config.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/dossier.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/__init__.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/agent.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/autonomous_system.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/client.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/cloud.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/code_signature.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/container.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/dns.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/egress.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/elf.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/email.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/error.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/event.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/faas.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/file.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/geo.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/group.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/hash.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/host.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/http.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/ingress.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/interface.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/network.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/observer.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/organization.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/os.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/pe.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/process.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/registry.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/related.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/rule.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/server.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/threat.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/tls.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/url.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/user.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/user_agent.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/ecs/vulnerability.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/event.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/gcp.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/hit.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/howler_data.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/lead.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/localized_label.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/overview.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/pivot.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/record.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/template.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/user.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/models/view.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/random_data.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/odm/randomizer.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/patched.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/plugins/__init__.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/plugins/config.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/remote/__init__.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/remote/datatypes/README.md +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/remote/datatypes/counters.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/remote/datatypes/events.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/remote/datatypes/hash.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/remote/datatypes/lock.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/remote/datatypes/queues/__init__.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/remote/datatypes/queues/comms.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/remote/datatypes/queues/multi.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/remote/datatypes/queues/priority.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/remote/datatypes/set.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/remote/datatypes/user_quota_tracker.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/security/__init__.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/security/socket.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/security/utils.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/services/__init__.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/services/action_service.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/services/analytic_service.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/services/auth_service.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/services/bundle_compat_service.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/services/comms_service.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/services/config_service.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/services/docs_service.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/services/dossier_service.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/services/fuzzy_service.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/services/jwt_service.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/services/lucene_service.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/services/notebook_service.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/services/overview_service.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/services/search_service.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/services/template_service.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/services/user_service.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/services/viewer_service.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/telemetry.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/utils/__init__.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/utils/annotations.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/utils/chunk.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/utils/compat.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/utils/constants.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/utils/dict_utils.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/utils/indexes.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/utils/isotime.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/utils/list_utils.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/utils/lucene.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/utils/net_utils.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/utils/path.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/utils/socket_utils.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/utils/str_utils.py +0 -0
- {howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/utils/uid.py +0 -0
|
@@ -13,8 +13,7 @@ OPERATION_ID = "add_to_case"
|
|
|
13
13
|
def execute( # noqa: C901
|
|
14
14
|
query: str,
|
|
15
15
|
case_id: Optional[str] = None,
|
|
16
|
-
|
|
17
|
-
title_template: str = "{{howler.analytic}} ({{howler.id}})",
|
|
16
|
+
destination: str = "related/{{howler.analytic}} ({{howler.id}})",
|
|
18
17
|
**kwargs,
|
|
19
18
|
):
|
|
20
19
|
"""Add matching alerts to a given case.
|
|
@@ -22,10 +21,9 @@ def execute( # noqa: C901
|
|
|
22
21
|
Args:
|
|
23
22
|
query (str): The query on which to apply this automation.
|
|
24
23
|
case_id (str): The ID of the case to add the alerts to.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
Defaults to "{{howler.analytic}} ({{howler.id}})".
|
|
24
|
+
destination (str): A Mustache-compatible template string for the case path at which each
|
|
25
|
+
alert will be placed, in the form "path/to/parent/name". The hit's fields are
|
|
26
|
+
available as template variables. Defaults to "related/{{howler.analytic}} ({{howler.id}})".
|
|
29
27
|
"""
|
|
30
28
|
if not case_id:
|
|
31
29
|
return [
|
|
@@ -67,15 +65,15 @@ def execute( # noqa: C901
|
|
|
67
65
|
added = []
|
|
68
66
|
|
|
69
67
|
for hit in hits:
|
|
70
|
-
|
|
71
|
-
item_path = f"{path.rstrip('/')}/{title}" if path else title
|
|
68
|
+
rendered_destination = chevron.render(destination, hit.as_primitives())
|
|
72
69
|
try:
|
|
73
|
-
|
|
70
|
+
item_path, name = rendered_destination.rsplit("/", maxsplit=1)
|
|
74
71
|
except ValueError:
|
|
75
|
-
|
|
72
|
+
item_path = None
|
|
73
|
+
name = rendered_destination
|
|
76
74
|
|
|
77
75
|
try:
|
|
78
|
-
parent = case_service.get_parent_from_path(case,
|
|
76
|
+
parent = case_service.get_parent_from_path(case, item_path, create_if_missing=True)
|
|
79
77
|
|
|
80
78
|
case_service.append_case_item(
|
|
81
79
|
case,
|
|
@@ -131,8 +129,7 @@ def specification():
|
|
|
131
129
|
{
|
|
132
130
|
"args": {
|
|
133
131
|
"case_id": [],
|
|
134
|
-
"
|
|
135
|
-
"title_template": [],
|
|
132
|
+
"destination": [],
|
|
136
133
|
},
|
|
137
134
|
"options": {},
|
|
138
135
|
}
|
|
@@ -39,7 +39,7 @@ hit_helper = OdmHelper(Hit)
|
|
|
39
39
|
@ingest_api.route("/<index>", methods=["POST"])
|
|
40
40
|
@api_login(required_priv=["W"])
|
|
41
41
|
@parse_parameters(refresh=parse_refresh)
|
|
42
|
-
def create(index: str, user: User, **kwargs):
|
|
42
|
+
def create(index: str, user: User, *, refresh: Literal["true", "false", "wait_for"] | None = None, **kwargs):
|
|
43
43
|
"""Create new records in a given index.
|
|
44
44
|
|
|
45
45
|
Variables:
|
|
@@ -75,11 +75,9 @@ def create(index: str, user: User, **kwargs):
|
|
|
75
75
|
if not isinstance(records, list):
|
|
76
76
|
return bad_request(err="JSON Payload must be a list of records.")
|
|
77
77
|
ignore_extra_values = request.args.get("ignore_extra_values", False, type=lambda v: v.lower() == "true")
|
|
78
|
-
refresh = cast(Literal["true", "false", "wait_for"] | None, kwargs.get("refresh"))
|
|
79
78
|
|
|
80
|
-
|
|
79
|
+
odms: list = []
|
|
81
80
|
warnings = []
|
|
82
|
-
refresh_kwargs: dict[str, Literal["true", "false", "wait_for"] | None] = {"refresh": refresh}
|
|
83
81
|
for i, record in enumerate(records):
|
|
84
82
|
try:
|
|
85
83
|
odm: Hit | Event
|
|
@@ -87,18 +85,22 @@ def create(index: str, user: User, **kwargs):
|
|
|
87
85
|
odm, _warnings = event_service.convert_event(
|
|
88
86
|
record, unique=True, ignore_extra_values=ignore_extra_values
|
|
89
87
|
)
|
|
90
|
-
event_service.create_event(odm.howler.id, odm, user.uname, skip_exists=True, **refresh_kwargs)
|
|
91
88
|
else:
|
|
92
89
|
odm, _warnings = hit_service.convert_hit(record, unique=True, ignore_extra_values=ignore_extra_values)
|
|
93
|
-
hit_service.create_hit(odm.howler.id, odm, user.uname, skip_exists=True, refresh=refresh)
|
|
94
90
|
|
|
95
|
-
|
|
91
|
+
odms.append(odm)
|
|
96
92
|
warnings.extend(_warnings)
|
|
97
93
|
except HowlerException as e:
|
|
98
94
|
logger.exception("Ingestion failed.")
|
|
99
95
|
return bad_request(err=f"Ingestion failure on record at index {i}: {e}")
|
|
100
96
|
|
|
97
|
+
if index == "event":
|
|
98
|
+
event_service.create_events(odms, user.uname, overwrite=False, refresh=refresh)
|
|
99
|
+
else:
|
|
100
|
+
hit_service.create_hits(odms, user.uname, overwrite=False, refresh=refresh)
|
|
101
|
+
|
|
101
102
|
# Enqueue newly created hit IDs for the correlation worker.
|
|
103
|
+
ids = [odm.howler.id for odm in odms]
|
|
102
104
|
if ids:
|
|
103
105
|
correlation_service.enqueue_for_correlation(ids)
|
|
104
106
|
|
|
@@ -71,6 +71,22 @@ from howler.telemetry import setup_telemetry
|
|
|
71
71
|
logger = get_logger(__file__)
|
|
72
72
|
|
|
73
73
|
|
|
74
|
+
def _should_start_background_services() -> bool:
|
|
75
|
+
"""Return whether this process should start long-lived background services.
|
|
76
|
+
|
|
77
|
+
Flask's development reloader imports this module in a parent process and
|
|
78
|
+
again in its serving child. The functional test wrapper sets
|
|
79
|
+
``HWL_START_BACKGROUND_SERVICES`` only for its no-reload Flask server,
|
|
80
|
+
preventing pytest imports from starting competing workers.
|
|
81
|
+
"""
|
|
82
|
+
return (
|
|
83
|
+
not DEBUG
|
|
84
|
+
or os.environ.get("WERKZEUG_RUN_MAIN") == "true"
|
|
85
|
+
or os.environ.get("HWL_START_BACKGROUND_SERVICES", "").lower() == "true"
|
|
86
|
+
or Path(sys.argv[0]).name.lower().startswith("gunicorn")
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
74
90
|
app = Flask(
|
|
75
91
|
"howler-api",
|
|
76
92
|
static_url_path="/api/static",
|
|
@@ -177,14 +193,15 @@ if HWL_USE_WEBSOCKET_API or DEBUG:
|
|
|
177
193
|
logger.debug("Enabled Websocket API")
|
|
178
194
|
app.register_blueprint(socket_api)
|
|
179
195
|
|
|
180
|
-
|
|
181
|
-
|
|
196
|
+
if _should_start_background_services():
|
|
197
|
+
# Start the Redis pubsub watcher so this pod receives events from all pods
|
|
198
|
+
import howler.services.comms_service as comms_service
|
|
182
199
|
|
|
183
|
-
|
|
200
|
+
comms_service.start_watcher()
|
|
184
201
|
else:
|
|
185
202
|
logger.info("Disabled Websocket API")
|
|
186
203
|
|
|
187
|
-
if HWL_USE_JOB_SYSTEM
|
|
204
|
+
if HWL_USE_JOB_SYSTEM and _should_start_background_services():
|
|
188
205
|
setup_jobs()
|
|
189
206
|
|
|
190
207
|
|
|
@@ -21,7 +21,7 @@ MAX_CLASSIFICATION: str = CLASSIFICATION.UNRESTRICTED
|
|
|
21
21
|
HWL_UNSECURED_UI = os.environ.get("HWL_UNSECURED_UI", "false").lower() == "true"
|
|
22
22
|
HWL_USE_REST_API = os.environ.get("HWL_USE_REST_API", "true").lower() == "true"
|
|
23
23
|
HWL_USE_WEBSOCKET_API = os.environ.get("HWL_USE_WEBSOCKET_API", "false").lower() == "true"
|
|
24
|
-
HWL_USE_JOB_SYSTEM = os.environ.get("HWL_USE_JOB_SYSTEM", "false").lower() == "true"
|
|
24
|
+
HWL_USE_JOB_SYSTEM = os.environ.get("HWL_USE_JOB_SYSTEM", "false").lower() == "true" or DEBUG
|
|
25
25
|
HWL_ENABLE_COVERAGE = os.environ.get("HWL_ENABLE_COVERAGE", "false").lower() == "true"
|
|
26
26
|
HWL_PLUGIN_DIRECTORY = os.environ.get("HWL_PLUGIN_DIRECTORY", "/etc/howler/plugins")
|
|
27
27
|
CORRELATION_QUEUE_NAME = os.environ.get("HWL_CORRELATION_QUEUE_NAME", "howler.ingestion_queue")
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""Build newline-delimited JSON payloads for Elasticsearch bulk requests.
|
|
2
|
+
|
|
3
|
+
``ElasticBulkPlan`` accumulates Elasticsearch bulk operations and renders them
|
|
4
|
+
as NDJSON suitable for the ``_bulk`` endpoint. Documents can be serialized
|
|
5
|
+
through an ODM model, and update operations can be limited to selected fields
|
|
6
|
+
to avoid overwriting unrelated stored data.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import warnings
|
|
11
|
+
from copy import deepcopy
|
|
12
|
+
from typing import List, Optional
|
|
13
|
+
|
|
14
|
+
from howler import odm
|
|
15
|
+
from howler.config import config
|
|
16
|
+
from howler.datastore.utils import expand_field_patterns, prune_to_paths
|
|
17
|
+
|
|
18
|
+
_OPERATION_GROUP = tuple[str] | tuple[str, str]
|
|
19
|
+
|
|
20
|
+
ELASTIC_MAX_REQUEST_SIZE = config.datastore.max_request_size
|
|
21
|
+
DEFAULT_BATCH_SIZE = config.datastore.request_batch_size
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ElasticBulkPlan(object):
|
|
25
|
+
"""Accumulate and render Elasticsearch bulk API operations.
|
|
26
|
+
|
|
27
|
+
Unless an operation supplies an explicit ``index``, insert, index, and
|
|
28
|
+
upsert operations target the first configured index. Delete and update
|
|
29
|
+
operations target every configured index, which supports collections that
|
|
30
|
+
span multiple physical indices.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
indexes: Elasticsearch indices available to the plan. The first index
|
|
34
|
+
is the default target for single-index operations.
|
|
35
|
+
model: Optional ODM model used to serialize document values before
|
|
36
|
+
they are added to the request.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, indexes: List[str], model: Optional[type[odm.Model]] = None):
|
|
40
|
+
self.indexes = indexes
|
|
41
|
+
self.model = model
|
|
42
|
+
self.operations: list[_OPERATION_GROUP] = []
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def empty(self):
|
|
46
|
+
"""Whether the plan has no queued operations.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
``True`` when no bulk operation has been added; otherwise,
|
|
50
|
+
``False``.
|
|
51
|
+
"""
|
|
52
|
+
return len(self.operations) == 0
|
|
53
|
+
|
|
54
|
+
def add_delete_operation(self, doc_id, index=None):
|
|
55
|
+
"""Queue a document delete operation.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
doc_id: Identifier of the document to delete.
|
|
59
|
+
index: Explicit index to target. When omitted, queues one delete
|
|
60
|
+
operation for each configured index.
|
|
61
|
+
"""
|
|
62
|
+
if index:
|
|
63
|
+
self.operations.append((json.dumps({"delete": {"_index": index, "_id": doc_id}}),))
|
|
64
|
+
else:
|
|
65
|
+
for cur_index in self.indexes:
|
|
66
|
+
self.operations.append((json.dumps({"delete": {"_index": cur_index, "_id": doc_id}}),))
|
|
67
|
+
|
|
68
|
+
def add_insert_operation(self, doc_id: str, doc, index=None):
|
|
69
|
+
"""Queue a create-only document insert operation.
|
|
70
|
+
|
|
71
|
+
The Elasticsearch ``create`` action fails if a document with ``doc_id``
|
|
72
|
+
already exists. The document's ``id`` field is set to ``doc_id`` in the
|
|
73
|
+
serialized request body.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
doc_id: Identifier to assign to the new document.
|
|
77
|
+
doc: Document data or an instance of the configured ODM model.
|
|
78
|
+
index: Explicit index to target. When omitted, uses the first
|
|
79
|
+
configured index.
|
|
80
|
+
"""
|
|
81
|
+
if self.model and isinstance(doc, self.model):
|
|
82
|
+
saved_doc = doc.as_primitives(hidden_fields=True)
|
|
83
|
+
elif self.model:
|
|
84
|
+
saved_doc = self.model(doc).as_primitives(hidden_fields=True)
|
|
85
|
+
else:
|
|
86
|
+
if not isinstance(doc, dict):
|
|
87
|
+
saved_doc = {"__non_doc_raw__": doc}
|
|
88
|
+
else:
|
|
89
|
+
saved_doc = deepcopy(doc)
|
|
90
|
+
saved_doc["id"] = doc_id
|
|
91
|
+
|
|
92
|
+
self.operations.append(
|
|
93
|
+
(
|
|
94
|
+
json.dumps({"create": {"_index": index or self.indexes[0], "_id": doc_id}}),
|
|
95
|
+
json.dumps(saved_doc),
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def add_index_operation(self, doc_id, doc, index=None):
|
|
100
|
+
"""Queue a document index operation.
|
|
101
|
+
|
|
102
|
+
The Elasticsearch ``index`` action creates the document or replaces an
|
|
103
|
+
existing document with the same identifier. The document's id field is
|
|
104
|
+
set to doc_id in the serialized request body.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
doc_id: Identifier of the document to create or replace.
|
|
108
|
+
doc: Document data or an instance of the configured ODM model.
|
|
109
|
+
index: Explicit index to target. When omitted, uses the first
|
|
110
|
+
configured index.
|
|
111
|
+
"""
|
|
112
|
+
if self.model and isinstance(doc, self.model):
|
|
113
|
+
saved_doc = doc.as_primitives(hidden_fields=True)
|
|
114
|
+
elif self.model:
|
|
115
|
+
saved_doc = self.model(doc).as_primitives(hidden_fields=True)
|
|
116
|
+
else:
|
|
117
|
+
if not isinstance(doc, dict):
|
|
118
|
+
saved_doc = {"__non_doc_raw__": doc}
|
|
119
|
+
else:
|
|
120
|
+
saved_doc = deepcopy(doc)
|
|
121
|
+
saved_doc["id"] = doc_id
|
|
122
|
+
|
|
123
|
+
self.operations.append(
|
|
124
|
+
(
|
|
125
|
+
json.dumps({"index": {"_index": index or self.indexes[0], "_id": doc_id}}),
|
|
126
|
+
json.dumps(saved_doc),
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def add_upsert_operation(self, doc_id, doc, index=None):
|
|
131
|
+
"""Queue an update operation that creates the document when absent.
|
|
132
|
+
|
|
133
|
+
The operation uses Elasticsearch's ``doc_as_upsert`` option. Its
|
|
134
|
+
serialized document includes ``id`` set to ``doc_id``.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
doc_id: Identifier of the document to update or create.
|
|
138
|
+
doc: Document data or an instance of the configured ODM model.
|
|
139
|
+
index: Explicit index to target. When omitted, uses the first
|
|
140
|
+
configured index.
|
|
141
|
+
"""
|
|
142
|
+
if self.model and isinstance(doc, self.model):
|
|
143
|
+
saved_doc = doc.as_primitives(hidden_fields=True)
|
|
144
|
+
elif self.model:
|
|
145
|
+
saved_doc = self.model(doc).as_primitives(hidden_fields=True)
|
|
146
|
+
else:
|
|
147
|
+
if not isinstance(doc, dict):
|
|
148
|
+
saved_doc = {"__non_doc_raw__": doc}
|
|
149
|
+
else:
|
|
150
|
+
saved_doc = deepcopy(doc)
|
|
151
|
+
saved_doc["id"] = doc_id
|
|
152
|
+
|
|
153
|
+
self.operations.append(
|
|
154
|
+
(
|
|
155
|
+
json.dumps({"update": {"_index": index or self.indexes[0], "_id": doc_id}}),
|
|
156
|
+
json.dumps({"doc": saved_doc, "doc_as_upsert": True}),
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def add_update_operation(self, doc_id, doc, index=None, fields: Optional[List[str]] = None):
|
|
161
|
+
"""Queue a partial-merge Elasticsearch ``update`` operation.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
doc_id: Identifier of the document to update.
|
|
165
|
+
doc: Document data or an instance of the configured ODM model.
|
|
166
|
+
index: Explicit index to target. When omitted, queues one update
|
|
167
|
+
operation for each configured index.
|
|
168
|
+
|
|
169
|
+
fields: When provided, restricts the update body to just these dotted field
|
|
170
|
+
paths (any valid key from :meth:`ESCollection.fields`/`Model.flat_fields`),
|
|
171
|
+
dropping everything else so unrelated concurrent edits to the stored
|
|
172
|
+
document aren't clobbered. Entries may contain ``*`` wildcards, expanded
|
|
173
|
+
against the model's known field paths (e.g. ``"items.*"`` selects every
|
|
174
|
+
subfield of the ``items`` list without touching ``title``, ``summary``,
|
|
175
|
+
etc.). A path with no wildcard is kept wholesale, including any nested
|
|
176
|
+
subfields, e.g. ``"items"`` alone also keeps every subfield of ``items``.
|
|
177
|
+
"""
|
|
178
|
+
if self.model and isinstance(doc, self.model):
|
|
179
|
+
saved_doc = doc.as_primitives(hidden_fields=True)
|
|
180
|
+
elif self.model:
|
|
181
|
+
saved_doc = self.model(doc, mask=list(doc.keys())).as_primitives(hidden_fields=True)
|
|
182
|
+
else:
|
|
183
|
+
if not isinstance(doc, dict):
|
|
184
|
+
saved_doc = {"__non_doc_raw__": doc}
|
|
185
|
+
else:
|
|
186
|
+
saved_doc = deepcopy(doc)
|
|
187
|
+
|
|
188
|
+
if fields is not None:
|
|
189
|
+
saved_doc = prune_to_paths(saved_doc, allowed=expand_field_patterns(self.model, fields))
|
|
190
|
+
|
|
191
|
+
if index:
|
|
192
|
+
self.operations.append(
|
|
193
|
+
(
|
|
194
|
+
json.dumps({"update": {"_index": index, "_id": doc_id}}),
|
|
195
|
+
json.dumps({"doc": saved_doc}),
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
else:
|
|
199
|
+
for cur_index in self.indexes:
|
|
200
|
+
self.operations.append(
|
|
201
|
+
(
|
|
202
|
+
json.dumps({"update": {"_index": cur_index, "_id": doc_id}}),
|
|
203
|
+
json.dumps({"doc": saved_doc}),
|
|
204
|
+
)
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def get_plan_data(self):
|
|
208
|
+
"""Render all queued operations as an Elasticsearch bulk request.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Newline-delimited JSON with a final trailing newline, ready to send
|
|
212
|
+
to Elasticsearch's ``_bulk`` endpoint.
|
|
213
|
+
"""
|
|
214
|
+
return self._get_plan_for_operations()
|
|
215
|
+
|
|
216
|
+
def get_plan_batches(self, batch_size: int | None = DEFAULT_BATCH_SIZE):
|
|
217
|
+
"""Yield queued operations as separate Elasticsearch bulk payloads.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
batch_size: Maximum number of queued operations in each payload.
|
|
221
|
+
Pass ``None`` to produce one payload containing all operations.
|
|
222
|
+
|
|
223
|
+
Yields:
|
|
224
|
+
Newline-delimited JSON payloads, each with a trailing newline.
|
|
225
|
+
"""
|
|
226
|
+
if batch_size is None:
|
|
227
|
+
batch_size = len(self.operations)
|
|
228
|
+
for ptr in range(0, len(self.operations), batch_size):
|
|
229
|
+
yield self._get_plan_for_operations(self.operations[ptr : ptr + batch_size])
|
|
230
|
+
|
|
231
|
+
def _flatten_operations(self, batch: List[_OPERATION_GROUP] | None = None) -> List[str]:
|
|
232
|
+
"""Flatten queued action/body groups into their NDJSON lines.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
batch: Operation groups to flatten. Uses every queued operation
|
|
236
|
+
when omitted or empty.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
JSON action and document lines in bulk API order.
|
|
240
|
+
"""
|
|
241
|
+
if not batch:
|
|
242
|
+
batch = self.operations
|
|
243
|
+
|
|
244
|
+
flattened: list[str] = []
|
|
245
|
+
for op in batch:
|
|
246
|
+
flattened.extend(op)
|
|
247
|
+
|
|
248
|
+
return flattened
|
|
249
|
+
|
|
250
|
+
def _get_plan_for_operations(self, batch: List[_OPERATION_GROUP] | None = None) -> str:
|
|
251
|
+
"""Render operation groups as a newline-delimited JSON payload.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
batch: Operation groups to render. Uses every queued operation when
|
|
255
|
+
omitted or empty.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
The rendered payload with a final trailing newline.
|
|
259
|
+
|
|
260
|
+
Warns:
|
|
261
|
+
UserWarning: If the encoded payload exceeds the configured maximum
|
|
262
|
+
Elasticsearch request size.
|
|
263
|
+
"""
|
|
264
|
+
plan = "\n".join(self._flatten_operations(batch)) + "\n"
|
|
265
|
+
|
|
266
|
+
if ELASTIC_MAX_REQUEST_SIZE and len(plan.encode("utf-8")) > ELASTIC_MAX_REQUEST_SIZE:
|
|
267
|
+
warnings.warn(
|
|
268
|
+
f"Bulk plan exceeds maximum request size of {ELASTIC_MAX_REQUEST_SIZE} bytes. "
|
|
269
|
+
f"Current size: {len(plan.encode('utf-8'))} bytes."
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
return plan
|
|
@@ -40,6 +40,7 @@ from howler.datastore.support.schemas import (
|
|
|
40
40
|
default_mapping,
|
|
41
41
|
)
|
|
42
42
|
from howler.datastore.types import AggSearchResult, SearchResult
|
|
43
|
+
from howler.datastore.utils import expand_field_patterns
|
|
43
44
|
from howler.odm.base import (
|
|
44
45
|
BANNED_FIELDS,
|
|
45
46
|
IP,
|
|
@@ -565,7 +566,14 @@ class ESCollection(Generic[ModelType]):
|
|
|
565
566
|
for operation_batch in operations.get_plan_batches():
|
|
566
567
|
response = self.with_retries(self.datastore.client.bulk, operations=operation_batch, refresh=refresh)
|
|
567
568
|
responses.append(response)
|
|
568
|
-
|
|
569
|
+
|
|
570
|
+
has_errors = False
|
|
571
|
+
for response in responses:
|
|
572
|
+
if response["errors"]:
|
|
573
|
+
has_errors = True
|
|
574
|
+
logger.error("Errors on bulk plan: %s", response["errors"])
|
|
575
|
+
|
|
576
|
+
return not has_errors
|
|
569
577
|
|
|
570
578
|
def get_bulk_plan(self):
|
|
571
579
|
"""
|
|
@@ -1628,23 +1636,8 @@ class ESCollection(Generic[ModelType]):
|
|
|
1628
1636
|
if not self.model_class or "*" not in fl:
|
|
1629
1637
|
return fl
|
|
1630
1638
|
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
for pattern in fl.split(","):
|
|
1634
|
-
pattern = pattern.strip()
|
|
1635
|
-
if not pattern:
|
|
1636
|
-
# Skip empty entries (e.g. from trailing commas).
|
|
1637
|
-
continue
|
|
1638
|
-
if "*" not in pattern or pattern == "*":
|
|
1639
|
-
# Exact names and the bare '*' (meaning "all fields") are kept as-is.
|
|
1640
|
-
expanded.append(pattern)
|
|
1641
|
-
else:
|
|
1642
|
-
# Convert the glob-style wildcard to a full regex pattern.
|
|
1643
|
-
# Replace '*' with '.*' and escape all other regex special characters.
|
|
1644
|
-
regex = re.compile("^" + re.escape(pattern).replace(r"\*", ".*") + "$")
|
|
1645
|
-
matched = [f for f in all_fields if regex.match(f)]
|
|
1646
|
-
expanded.extend(matched if matched else [pattern])
|
|
1647
|
-
return ",".join(expanded)
|
|
1639
|
+
patterns = [p.strip() for p in fl.split(",") if p.strip()]
|
|
1640
|
+
return ",".join(sorted(expand_field_patterns(self.model_class, patterns, preserve_all=True)))
|
|
1648
1641
|
|
|
1649
1642
|
def _format_output(self, result, fields=None, as_obj=True):
|
|
1650
1643
|
# Getting search document data
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Shared helpers for building field-scoped Elasticsearch operations from ODM models."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Iterable, Optional
|
|
5
|
+
|
|
6
|
+
from howler import odm
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def expand_field_patterns(
|
|
10
|
+
model: Optional[type[odm.Model]], patterns: Iterable[str], preserve_all: bool = False
|
|
11
|
+
) -> set[str]:
|
|
12
|
+
"""Expand `*`-wildcard field patterns against a model's known dotted field paths.
|
|
13
|
+
|
|
14
|
+
Entries without a `*` are kept as-is (even if the model doesn't recognize them, so
|
|
15
|
+
synthetic keys like `__non_doc_raw__` still work). Without a model, patterns can't be
|
|
16
|
+
expanded and are kept literally.
|
|
17
|
+
"""
|
|
18
|
+
if model is None:
|
|
19
|
+
return set(patterns)
|
|
20
|
+
|
|
21
|
+
known_paths = list(model.flat_fields().keys())
|
|
22
|
+
expanded: set[str] = set()
|
|
23
|
+
for pattern in patterns:
|
|
24
|
+
if "*" not in pattern:
|
|
25
|
+
expanded.add(pattern)
|
|
26
|
+
continue
|
|
27
|
+
|
|
28
|
+
if pattern == "*" and preserve_all:
|
|
29
|
+
expanded.add(pattern)
|
|
30
|
+
continue
|
|
31
|
+
|
|
32
|
+
regex = re.compile("^" + re.escape(pattern).replace(r"\*", ".*") + "$")
|
|
33
|
+
matches = [path for path in known_paths if regex.match(path)]
|
|
34
|
+
expanded.update(matches)
|
|
35
|
+
return expanded
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def prune_to_paths(value, allowed: set[str], prefix: str = ""):
|
|
39
|
+
"""Recursively keep only the parts of `value` selected by dotted paths in `allowed`.
|
|
40
|
+
|
|
41
|
+
A key is kept wholesale (subtree included as-is) if its own path is in `allowed`;
|
|
42
|
+
otherwise it's kept and recursed into if some allowed path starts with it, so
|
|
43
|
+
e.g. `allowed={"items.type"}` prunes each entry of a list field named `items` down
|
|
44
|
+
to just its `type` subfield.
|
|
45
|
+
"""
|
|
46
|
+
if isinstance(value, dict):
|
|
47
|
+
result = {}
|
|
48
|
+
for key, sub_value in value.items():
|
|
49
|
+
cur_path = f"{prefix}.{key}" if prefix else key
|
|
50
|
+
if cur_path in allowed:
|
|
51
|
+
result[key] = sub_value
|
|
52
|
+
elif any(path.startswith(f"{cur_path}.") for path in allowed):
|
|
53
|
+
result[key] = prune_to_paths(sub_value, allowed, cur_path)
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
if isinstance(value, list):
|
|
57
|
+
return [prune_to_paths(entry, allowed, prefix) for entry in value]
|
|
58
|
+
|
|
59
|
+
return value
|
{howler_api-4.1.0.dev1368 → howler_api-4.1.0.dev1387}/howler/remote/datatypes/queues/named.py
RENAMED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import time
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Generic, Optional, TypeVar
|
|
4
|
+
|
|
5
|
+
import redis
|
|
4
6
|
|
|
5
7
|
from howler.common.exceptions import HowlerTypeError
|
|
6
8
|
from howler.remote.datatypes import get_client, retry_call
|
|
@@ -10,7 +12,7 @@ T = TypeVar("T")
|
|
|
10
12
|
|
|
11
13
|
class NamedQueue(Generic[T]):
|
|
12
14
|
def __init__(self, name: str, host=None, port=None, private: bool = False, ttl: int = 0):
|
|
13
|
-
self.c:
|
|
15
|
+
self.c: redis.Redis | redis.StrictRedis | redis.RedisCluster = get_client(host, port, private)
|
|
14
16
|
self.name: str = name
|
|
15
17
|
self.ttl: int = ttl
|
|
16
18
|
self.last_expire_time: float = 0
|
|
@@ -66,8 +68,7 @@ class NamedQueue(Generic[T]):
|
|
|
66
68
|
return json.loads(response)
|
|
67
69
|
|
|
68
70
|
def push(self, *messages: T):
|
|
69
|
-
for message in messages
|
|
70
|
-
retry_call(self.c.rpush, self.name, json.dumps(message))
|
|
71
|
+
retry_call(self.c.rpush, self.name, *[json.dumps(message) for message in messages])
|
|
71
72
|
self._conditional_expire()
|
|
72
73
|
|
|
73
74
|
def unpop(self, *messages: T):
|