howler-api 4.0.0.dev1141__tar.gz → 4.0.0.dev1147__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.dev1141 → howler_api-4.0.0.dev1147}/PKG-INFO +3 -3
- howler_api-4.0.0.dev1147/howler/healthz.py +91 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/config.py +1 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/view.py +16 -1
- howler_api-4.0.0.dev1147/howler/remote/datatypes/__init__.py +257 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/remote/datatypes/counters.py +2 -2
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/remote/datatypes/hash.py +1 -1
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/remote/datatypes/user_quota_tracker.py +3 -3
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/pyproject.toml +4 -5
- howler_api-4.0.0.dev1141/howler/healthz.py +0 -47
- howler_api-4.0.0.dev1141/howler/remote/datatypes/__init__.py +0 -158
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/README.md +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/__init__.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/actions/__init__.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/actions/add_label.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/actions/add_to_bundle.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/actions/add_to_case.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/actions/change_field.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/actions/demote.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/actions/example_plugin.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/actions/prioritization.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/actions/promote.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/actions/remove_from_bundle.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/actions/remove_label.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/actions/transition.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/api/__init__.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/api/base.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/api/socket.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/api/v1/__init__.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/api/v1/action.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/api/v1/analytic.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/api/v1/auth.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/api/v1/clue.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/api/v1/configs.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/api/v1/dossier.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/api/v1/help.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/api/v1/hit.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/api/v1/notebook.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/api/v1/overview.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/api/v1/search.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/api/v1/template.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/api/v1/tool.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/api/v1/user.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/api/v1/utils/__init__.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/api/v1/utils/etag.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/api/v1/utils/params.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/api/v1/view.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/api/v2/__init__.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/api/v2/case.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/api/v2/fuzzy.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/api/v2/ingest.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/api/v2/search.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/app.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/common/README.md +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/common/__init__.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/common/classification.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/common/classification.yml +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/common/exceptions.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/common/loader.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/common/logging/__init__.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/common/logging/audit.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/common/logging/format.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/common/net.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/common/net_static.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/common/random_user.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/common/swagger.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/config.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/cronjobs/__init__.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/cronjobs/action_queue_worker.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/cronjobs/correlation.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/cronjobs/retention.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/cronjobs/view_cleanup.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/datastore/README.md +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/datastore/__init__.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/datastore/bulk.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/datastore/collection.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/datastore/constants.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/datastore/exceptions.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/datastore/howler_store.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/datastore/migrations/fix_process.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/datastore/operations.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/datastore/schemas.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/datastore/store.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/datastore/support/__init__.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/datastore/support/build.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/datastore/support/schemas.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/datastore/types.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/error.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/external/README.md +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/external/__init__.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/external/generate_mitre.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/external/generate_sigma_rules.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/external/generate_tlds.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/external/reindex_data.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/external/wipe_databases.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/gunicorn_config.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/helper/__init__.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/helper/azure.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/helper/discover.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/helper/hit.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/helper/oauth.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/helper/search.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/helper/workflow.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/helper/ws.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/README.md +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/__init__.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/base.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/charter.txt +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/constants.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/helper.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/howler_enum.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/mixins.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/__init__.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/action.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/analytic.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/assemblyline.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/aws.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/azure.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/case.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/cbs.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/clue.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/dossier.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/__init__.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/agent.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/autonomous_system.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/client.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/cloud.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/code_signature.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/container.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/dns.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/egress.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/elf.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/email.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/error.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/event.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/faas.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/file.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/geo.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/group.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/hash.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/host.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/http.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/ingress.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/interface.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/network.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/observer.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/organization.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/os.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/pe.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/process.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/registry.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/related.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/rule.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/server.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/threat.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/tls.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/url.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/user.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/user_agent.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/ecs/vulnerability.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/event.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/gcp.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/hit.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/howler_data.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/lead.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/localized_label.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/overview.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/pivot.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/record.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/template.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/models/user.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/random_data.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/odm/randomizer.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/patched.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/plugins/__init__.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/plugins/config.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/remote/__init__.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/remote/datatypes/README.md +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/remote/datatypes/events.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/remote/datatypes/lock.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/remote/datatypes/queues/__init__.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/remote/datatypes/queues/comms.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/remote/datatypes/queues/multi.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/remote/datatypes/queues/named.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/remote/datatypes/queues/priority.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/remote/datatypes/set.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/security/__init__.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/security/socket.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/security/utils.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/services/__init__.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/services/action_service.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/services/analytic_service.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/services/auth_service.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/services/bundle_compat_service.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/services/case_service.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/services/comms_service.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/services/config_service.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/services/correlation_service.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/services/docs_service.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/services/dossier_service.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/services/event_service.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/services/fuzzy_service.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/services/hit_service.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/services/jwt_service.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/services/lucene_service.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/services/notebook_service.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/services/overview_service.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/services/search_service.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/services/template_service.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/services/user_service.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/services/viewer_service.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/telemetry.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/utils/__init__.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/utils/annotations.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/utils/chunk.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/utils/compat.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/utils/constants.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/utils/dict_utils.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/utils/isotime.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/utils/list_utils.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/utils/lucene.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/utils/path.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/utils/socket_utils.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/utils/str_utils.py +0 -0
- {howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/utils/uid.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: howler-api
|
|
3
|
-
Version: 4.0.0.
|
|
3
|
+
Version: 4.0.0.dev1147
|
|
4
4
|
Summary: Howler - API server
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: howler,alerting,gc,canada,cse-cst,cse,cst,cyber,cccs
|
|
@@ -19,7 +19,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.13
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.14
|
|
21
21
|
Classifier: Topic :: Software Development :: Libraries
|
|
22
|
-
Requires-Dist: apscheduler (==3.11.
|
|
22
|
+
Requires-Dist: apscheduler (==3.11.3)
|
|
23
23
|
Requires-Dist: authlib (>=1.6.0,<2.0.0)
|
|
24
24
|
Requires-Dist: azure-monitor-opentelemetry (>=1.8.7,<2.0.0)
|
|
25
25
|
Requires-Dist: bcrypt (==4.3.0)
|
|
@@ -50,7 +50,7 @@ Requires-Dist: python-datemath (==3.0.3)
|
|
|
50
50
|
Requires-Dist: python-dotenv (>=1.1.0,<2.0.0)
|
|
51
51
|
Requires-Dist: pytz (>=2025.2,<2026.0)
|
|
52
52
|
Requires-Dist: pyyaml (==6.0.3)
|
|
53
|
-
Requires-Dist: redis (==
|
|
53
|
+
Requires-Dist: redis (==8.0.1)
|
|
54
54
|
Requires-Dist: requests (==2.34.2)
|
|
55
55
|
Requires-Dist: tzdata (>=2026.1,<2027.0)
|
|
56
56
|
Requires-Dist: validators (>=0.34,<0.36)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from flask import Blueprint, abort, make_response
|
|
2
|
+
from redis import RedisError
|
|
3
|
+
|
|
4
|
+
from howler import config as howler_config
|
|
5
|
+
from howler.common.loader import datastore
|
|
6
|
+
from howler.common.logging import get_logger
|
|
7
|
+
|
|
8
|
+
API_PREFIX = "/api/healthz"
|
|
9
|
+
healthz = Blueprint("healthz", __name__, url_prefix=API_PREFIX)
|
|
10
|
+
logger = get_logger(__file__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _is_datastore_healthy() -> bool:
|
|
14
|
+
"""Check whether the datastore is reachable."""
|
|
15
|
+
try:
|
|
16
|
+
if datastore().ds.ping():
|
|
17
|
+
return True
|
|
18
|
+
except Exception as error: # noqa: BLE001
|
|
19
|
+
logger.warning("Health check failed: datastore ping raised %s", error)
|
|
20
|
+
return False
|
|
21
|
+
|
|
22
|
+
logger.warning("Health check failed: datastore ping returned false")
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _is_redis_healthy() -> bool:
|
|
27
|
+
"""Check whether configured Redis clients are reachable."""
|
|
28
|
+
redis_config = howler_config.config.core.redis
|
|
29
|
+
|
|
30
|
+
if redis_config.nonpersistent.host:
|
|
31
|
+
try:
|
|
32
|
+
if not howler_config.redis.ping():
|
|
33
|
+
logger.warning("Health check failed: nonpersistent Redis ping returned false")
|
|
34
|
+
return False
|
|
35
|
+
except (ConnectionResetError, OSError, RedisError) as error:
|
|
36
|
+
logger.warning("Health check failed: nonpersistent Redis ping raised %s", error)
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
if redis_config.persistent.host:
|
|
40
|
+
try:
|
|
41
|
+
if not howler_config.redis_persistent.ping():
|
|
42
|
+
logger.warning("Health check failed: persistent Redis ping returned false")
|
|
43
|
+
return False
|
|
44
|
+
except (ConnectionResetError, OSError, RedisError) as error:
|
|
45
|
+
logger.warning("Health check failed: persistent Redis ping raised %s", error)
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
return True
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@healthz.route("/live")
|
|
52
|
+
def liveness(**_):
|
|
53
|
+
"""Check if the API is live
|
|
54
|
+
|
|
55
|
+
Variables:
|
|
56
|
+
None
|
|
57
|
+
|
|
58
|
+
Arguments:
|
|
59
|
+
None
|
|
60
|
+
|
|
61
|
+
Result Example:
|
|
62
|
+
OK or FAIL
|
|
63
|
+
"""
|
|
64
|
+
return make_response("OK")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@healthz.route("/ready")
|
|
68
|
+
def readyness(**_):
|
|
69
|
+
"""Check if the API is Ready
|
|
70
|
+
|
|
71
|
+
Checks that the datastore and configured Redis connections are alive.
|
|
72
|
+
|
|
73
|
+
Variables:
|
|
74
|
+
None
|
|
75
|
+
|
|
76
|
+
Arguments:
|
|
77
|
+
None
|
|
78
|
+
|
|
79
|
+
Result Example:
|
|
80
|
+
OK or FAIL
|
|
81
|
+
"""
|
|
82
|
+
if _is_datastore_healthy() and _is_redis_healthy():
|
|
83
|
+
return make_response("OK")
|
|
84
|
+
else:
|
|
85
|
+
abort(503)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@healthz.errorhandler(503)
|
|
89
|
+
def error(_):
|
|
90
|
+
"Handle errors exposed in healthz routes"
|
|
91
|
+
return "FAIL", 503
|
|
@@ -33,6 +33,7 @@ class RedisServer(BaseModel):
|
|
|
33
33
|
tls_ca_cert: Optional[str] = Field(
|
|
34
34
|
description="Path to CA Certificate (PEM) to validate Redis instance certificate when using TLS", default=None
|
|
35
35
|
)
|
|
36
|
+
tls_disable_check_hostname: bool = Field(default=False, description="Disable TLS hostname check")
|
|
36
37
|
is_cluster: bool = Field(default=False, description="Is this Redis instance a cluster?")
|
|
37
38
|
|
|
38
39
|
|
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
# mypy: ignore-errors
|
|
2
|
-
from typing import Literal, Union
|
|
2
|
+
from typing import Literal, Optional, Union
|
|
3
3
|
|
|
4
4
|
from howler import odm
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
@odm.model(index=True, store=True, description="The field and width of a column to display in a grid view.")
|
|
8
|
+
class GridColumn(odm.Model):
|
|
9
|
+
field: str = odm.Keyword(description="The field key for this column.")
|
|
10
|
+
width: Optional[int] = odm.Optional(odm.Integer(description="The width of this column in pixels."))
|
|
11
|
+
|
|
12
|
+
|
|
7
13
|
@odm.model(index=True, store=True, description="Additional View Settings")
|
|
8
14
|
class Settings(odm.Model):
|
|
9
15
|
advance_on_triage: bool = odm.Boolean(
|
|
10
16
|
description="Should the user advance to the next alert when triage is complete?", default=False
|
|
11
17
|
)
|
|
18
|
+
display: Optional[Union[Literal["list"], Literal["grid"]]] = odm.Optional(
|
|
19
|
+
odm.Enum(
|
|
20
|
+
values=["list", "grid"],
|
|
21
|
+
description="The layout to use when opening this view",
|
|
22
|
+
)
|
|
23
|
+
)
|
|
24
|
+
columns: Optional[list[GridColumn]] = odm.Optional(
|
|
25
|
+
odm.List(odm.Compound(GridColumn, description="The columns to display in this view."))
|
|
26
|
+
)
|
|
12
27
|
|
|
13
28
|
|
|
14
29
|
DEFAULT_INDEXES = ["hit"]
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import socket
|
|
7
|
+
import time
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import redis
|
|
13
|
+
from packaging.version import parse
|
|
14
|
+
from redis.backoff import ExponentialBackoff
|
|
15
|
+
from redis.retry import Retry
|
|
16
|
+
|
|
17
|
+
from howler.odm.models.config import config
|
|
18
|
+
from howler.utils.uid import get_random_id
|
|
19
|
+
|
|
20
|
+
# Add a version warning if redis python client is < 2.10.0. Older versions
|
|
21
|
+
# have a connection bug that can manifest with the dispatcher.
|
|
22
|
+
if parse(redis.__version__) <= parse("2.10.0"):
|
|
23
|
+
import warnings
|
|
24
|
+
|
|
25
|
+
warnings.warn(
|
|
26
|
+
"%s works best with redis > 2.10.0. You're running"
|
|
27
|
+
" redis %s. You should upgrade." % (__name__, redis.__version__)
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
APP_NAME = os.environ.get("APP_NAME", "howler")
|
|
31
|
+
|
|
32
|
+
log = logging.getLogger(f"{APP_NAME}.queue")
|
|
33
|
+
pool: dict[tuple[str, str, bool], redis.BlockingConnectionPool] = {}
|
|
34
|
+
|
|
35
|
+
# TCP keepalive tuning so a connection to a primary that has gone away (e.g.
|
|
36
|
+
# during an Azure Managed Redis failover) is detected in ~90s instead of the
|
|
37
|
+
# Linux default keepalive idle of ~2 hours. This is the mechanism that lets
|
|
38
|
+
# *blocking* reads (BLPOP/BZPOPMIN with timeout=0) notice a dead peer, since we
|
|
39
|
+
# deliberately do NOT set socket_timeout (see RESILIENCE_CONFIG below). The
|
|
40
|
+
# option constants are platform specific, so each is added only if available.
|
|
41
|
+
_KEEPALIVE_OPTIONS: dict[int, int] = {
|
|
42
|
+
getattr(socket, name): value
|
|
43
|
+
for name, value in (("TCP_KEEPIDLE", 60), ("TCP_KEEPINTVL", 10), ("TCP_KEEPCNT", 3))
|
|
44
|
+
if hasattr(socket, name)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Connection resilience defaults. These ensure that during a managed Redis
|
|
48
|
+
# failover (e.g. Azure Managed Redis) the client quickly detects dead/stale
|
|
49
|
+
# connections and reconnects to the new primary instead of hanging on the OS
|
|
50
|
+
# level TCP timeout (which can be several minutes).
|
|
51
|
+
# - socket_connect_timeout: cap how long establishing a new connection can
|
|
52
|
+
# block so a dead endpoint surfaces as a (retryable) error in seconds.
|
|
53
|
+
# - socket_keepalive / socket_keepalive_options: enable and tune TCP
|
|
54
|
+
# keepalives so half-open connections (including those blocked in a long
|
|
55
|
+
# BLPOP/BZPOPMIN) are detected and recycled.
|
|
56
|
+
# - health_check_interval: proactively PING idle pooled connections before
|
|
57
|
+
# use so stale connections to the old primary are recycled transparently.
|
|
58
|
+
#
|
|
59
|
+
# NOTE: socket_timeout is explicitly set to None. It is a per-read socket
|
|
60
|
+
# timeout that redis-py also applies to blocking commands, so any positive value
|
|
61
|
+
# makes BLPOP/BZPOPMIN/BRPOP with an infinite (timeout=0) wait raise spurious
|
|
62
|
+
# TimeoutErrors (see redis-py #2807). This MUST be set explicitly: redis-py 8.0
|
|
63
|
+
# changed the default socket_timeout from None to 5 seconds, so simply omitting
|
|
64
|
+
# it would silently break the infinite blocking pops the queue consumers in this
|
|
65
|
+
# package rely on. With socket_timeout disabled, keepalives + health checks
|
|
66
|
+
# handle failover detection instead.
|
|
67
|
+
RESILIENCE_CONFIG: dict[str, Any] = {
|
|
68
|
+
"socket_connect_timeout": 5,
|
|
69
|
+
"socket_timeout": None,
|
|
70
|
+
"socket_keepalive": True,
|
|
71
|
+
"health_check_interval": 15,
|
|
72
|
+
}
|
|
73
|
+
if _KEEPALIVE_OPTIONS:
|
|
74
|
+
RESILIENCE_CONFIG["socket_keepalive_options"] = _KEEPALIVE_OPTIONS
|
|
75
|
+
|
|
76
|
+
# Maximum wall-clock time (in seconds) that retry_call will keep retrying after
|
|
77
|
+
# the first observed Redis connection failure before giving up and re-raising the
|
|
78
|
+
# last error. The retry window starts after the first error because blocking
|
|
79
|
+
# Redis reads rely on TCP keepalive to notice a dead peer, and that detection can
|
|
80
|
+
# take longer than this retry budget.
|
|
81
|
+
RETRY_DEADLINE = 60
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def now_as_iso():
|
|
85
|
+
s = datetime.now(timezone.utc).replace(tzinfo=None).isoformat()
|
|
86
|
+
return f"{s}Z"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def reply_queue_name(prefix=None, suffix=None):
|
|
90
|
+
if prefix:
|
|
91
|
+
components = [prefix]
|
|
92
|
+
else:
|
|
93
|
+
components = []
|
|
94
|
+
|
|
95
|
+
components.append(get_random_id())
|
|
96
|
+
|
|
97
|
+
if suffix:
|
|
98
|
+
components.append(str(suffix))
|
|
99
|
+
|
|
100
|
+
return "-".join(components)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def retry_call(func, *args, **kw):
|
|
104
|
+
maximum = 2
|
|
105
|
+
exponent = -7
|
|
106
|
+
deadline = None
|
|
107
|
+
|
|
108
|
+
while True:
|
|
109
|
+
try:
|
|
110
|
+
ret_val = func(*args, **kw)
|
|
111
|
+
|
|
112
|
+
if exponent != -7:
|
|
113
|
+
log.info("Reconnected to Redis!")
|
|
114
|
+
|
|
115
|
+
return ret_val
|
|
116
|
+
except (redis.ConnectionError, redis.TimeoutError, OSError) as ce:
|
|
117
|
+
# redis.ConnectionError covers BusyLoadingError (node loading after a
|
|
118
|
+
# failover) and most connect-time socket errors. redis.TimeoutError
|
|
119
|
+
# covers Redis read/connect timeouts. OSError covers the builtin socket
|
|
120
|
+
# errors raised while a managed Redis instance is failing over,
|
|
121
|
+
# including its ConnectionResetError and TimeoutError subclasses.
|
|
122
|
+
#
|
|
123
|
+
# Use a monotonic clock for the retry budget so NTP/system clock
|
|
124
|
+
# adjustments can't make retries give up early or run far too long.
|
|
125
|
+
if deadline is None:
|
|
126
|
+
deadline = time.monotonic() + RETRY_DEADLINE
|
|
127
|
+
elif time.monotonic() >= deadline:
|
|
128
|
+
# Give up after RETRY_DEADLINE so a sustained outage fails the
|
|
129
|
+
# request cleanly instead of blocking the worker indefinitely.
|
|
130
|
+
log.exception("No connection to Redis after %ss, giving up.", RETRY_DEADLINE)
|
|
131
|
+
raise
|
|
132
|
+
|
|
133
|
+
log.warning("No connection to Redis, reconnecting... [%s]", ce)
|
|
134
|
+
time.sleep(2**exponent)
|
|
135
|
+
exponent = exponent + 1 if exponent < maximum else exponent
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def get_client(
|
|
139
|
+
host: str | redis.Redis | redis.StrictRedis | redis.RedisCluster | None,
|
|
140
|
+
port: int | None,
|
|
141
|
+
private: bool = False,
|
|
142
|
+
):
|
|
143
|
+
"""
|
|
144
|
+
Get Redis instance.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
host: Redis host, defaults to nonpersistent host
|
|
148
|
+
port: Redis port, defaults to nonpersistent port
|
|
149
|
+
private: If true then use standard connection, otherwise use a Pool
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Redis instance
|
|
153
|
+
"""
|
|
154
|
+
# In case a structure is passed a client as host
|
|
155
|
+
if isinstance(host, (redis.Redis, redis.StrictRedis, redis.RedisCluster)):
|
|
156
|
+
return host
|
|
157
|
+
|
|
158
|
+
if not host or not port:
|
|
159
|
+
host = host or config.core.redis.nonpersistent.host
|
|
160
|
+
port = int(port or config.core.redis.nonpersistent.port)
|
|
161
|
+
|
|
162
|
+
extra_conn_config: dict[str, Any] = {}
|
|
163
|
+
host_config = None
|
|
164
|
+
|
|
165
|
+
# Apply socket connect timeout / keepalive / health-check defaults so the
|
|
166
|
+
# client can detect and recover from managed Redis failovers instead of
|
|
167
|
+
# hanging.
|
|
168
|
+
extra_conn_config.update(RESILIENCE_CONFIG)
|
|
169
|
+
|
|
170
|
+
# Pin RESP2 on the wire. redis-py 8.0 switched the default to RESP3; pinning
|
|
171
|
+
# protocol=2 preserves the exact wire behaviour this codebase was written
|
|
172
|
+
# against so the dependency upgrade is behaviour-neutral. Can be revisited to
|
|
173
|
+
# adopt RESP3 once its response shapes have been validated end to end.
|
|
174
|
+
extra_conn_config["protocol"] = 2
|
|
175
|
+
|
|
176
|
+
# Configure connection-level retries using the modern Retry API (the
|
|
177
|
+
# replacement for the deprecated retry_on_timeout flag). A fresh Retry
|
|
178
|
+
# instance is built here per get_client call. Note that for pooled clients
|
|
179
|
+
# (private=False) the underlying connection pool is cached by host/port/ssl
|
|
180
|
+
# in get_pool, so this Retry is only applied when the pool is first created
|
|
181
|
+
# and is then effectively shared (per-pool) by every later client on that
|
|
182
|
+
# pool. The Retry config itself is immutable, so this sharing is benign.
|
|
183
|
+
extra_conn_config["retry"] = Retry(ExponentialBackoff(cap=10, base=0.5), retries=3)
|
|
184
|
+
extra_conn_config["retry_on_error"] = [redis.ConnectionError, redis.TimeoutError]
|
|
185
|
+
|
|
186
|
+
if host == config.core.redis.nonpersistent.host and port == config.core.redis.nonpersistent.port:
|
|
187
|
+
host_config = config.core.redis.nonpersistent
|
|
188
|
+
else:
|
|
189
|
+
host_config = config.core.redis.persistent
|
|
190
|
+
|
|
191
|
+
if host_config.password:
|
|
192
|
+
extra_conn_config["username"] = "default"
|
|
193
|
+
extra_conn_config["password"] = host_config.password
|
|
194
|
+
|
|
195
|
+
if host_config.tls_enabled:
|
|
196
|
+
extra_conn_config["ssl"] = True
|
|
197
|
+
extra_conn_config["ssl_cert_reqs"] = "required"
|
|
198
|
+
|
|
199
|
+
if host_config.tls_disable_check_hostname:
|
|
200
|
+
extra_conn_config["ssl_check_hostname"] = False
|
|
201
|
+
|
|
202
|
+
if host_config.tls_ca_cert:
|
|
203
|
+
if not Path(host_config.tls_ca_cert).exists():
|
|
204
|
+
raise FileNotFoundError(f"Redis TLS CA cert or path '{host_config.tls_ca_cert}' not found.")
|
|
205
|
+
|
|
206
|
+
if Path(host_config.tls_ca_cert).is_file():
|
|
207
|
+
extra_conn_config["ssl_ca_certs"] = host_config.tls_ca_cert
|
|
208
|
+
else:
|
|
209
|
+
extra_conn_config["ssl_ca_path"] = host_config.tls_ca_cert
|
|
210
|
+
|
|
211
|
+
if host_config.is_cluster is True:
|
|
212
|
+
return redis.RedisCluster(host=host, port=port, **extra_conn_config) # type: ignore
|
|
213
|
+
|
|
214
|
+
if private:
|
|
215
|
+
return redis.StrictRedis(host=host, port=port, **extra_conn_config) # type: ignore
|
|
216
|
+
else:
|
|
217
|
+
return redis.StrictRedis(connection_pool=get_pool(host, port, **extra_conn_config))
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def get_pool(host, port, **kwargs):
|
|
221
|
+
"""
|
|
222
|
+
Get Redis connection pool
|
|
223
|
+
Args:
|
|
224
|
+
host: Redis host
|
|
225
|
+
port: Redis port
|
|
226
|
+
**kwargs: Extra parameters to pass to pool connection class
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Redis BlockingConnectionPool
|
|
230
|
+
"""
|
|
231
|
+
key = (host, str(port), kwargs.get("ssl", False))
|
|
232
|
+
connection_pool = pool.get(key, None)
|
|
233
|
+
|
|
234
|
+
if not connection_pool:
|
|
235
|
+
if "ssl" in kwargs and kwargs["ssl"]:
|
|
236
|
+
# SSLConnection class doesn't accept 'ssl' parameter as it implicitly uses SSL
|
|
237
|
+
kwargs.pop("ssl")
|
|
238
|
+
connection_pool = redis.BlockingConnectionPool(
|
|
239
|
+
host=host,
|
|
240
|
+
port=port,
|
|
241
|
+
max_connections=200,
|
|
242
|
+
connection_class=redis.SSLConnection,
|
|
243
|
+
**kwargs,
|
|
244
|
+
)
|
|
245
|
+
else:
|
|
246
|
+
connection_pool = redis.BlockingConnectionPool(host=host, port=port, max_connections=200, **kwargs)
|
|
247
|
+
pool[key] = connection_pool
|
|
248
|
+
|
|
249
|
+
return connection_pool
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def decode(data):
|
|
253
|
+
try:
|
|
254
|
+
return json.loads(data)
|
|
255
|
+
except ValueError:
|
|
256
|
+
log.warning("Invalid data on queue: %s", str(data))
|
|
257
|
+
return None
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Optional
|
|
1
|
+
from typing import Any, Optional
|
|
2
2
|
|
|
3
3
|
from redis.exceptions import ConnectionError
|
|
4
4
|
|
|
@@ -8,7 +8,7 @@ from howler.remote.datatypes.hash import Hash
|
|
|
8
8
|
|
|
9
9
|
class Counters(object):
|
|
10
10
|
def __init__(self, prefix="counter", host=None, port=None, track_counters=False):
|
|
11
|
-
self.c = get_client(host, port, False)
|
|
11
|
+
self.c: Any = get_client(host, port, False)
|
|
12
12
|
self.prefix = prefix
|
|
13
13
|
if track_counters:
|
|
14
14
|
self.tracker: Optional[Hash] = Hash("c-tracker-%s" % prefix, host=host, port=port)
|
{howler_api-4.0.0.dev1141 → howler_api-4.0.0.dev1147}/howler/remote/datatypes/user_quota_tracker.py
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from typing import Any
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
from redis.exceptions import ResponseError
|
|
4
4
|
|
|
5
5
|
from howler.remote.datatypes import get_client, retry_call
|
|
6
6
|
|
|
@@ -35,7 +35,7 @@ class UserQuotaTracker(object):
|
|
|
35
35
|
def begin(self, user, max_quota):
|
|
36
36
|
try:
|
|
37
37
|
return retry_call(self.bs, args=[self._queue_name(user), max_quota, self.timeout]) == 1
|
|
38
|
-
except
|
|
38
|
+
except ResponseError as er:
|
|
39
39
|
# TODO: This is a failsafe for upgrade purposes could be removed in a future version
|
|
40
40
|
if "WRONGTYPE" in str(er):
|
|
41
41
|
retry_call(self.c.delete, self._queue_name(user))
|
|
@@ -47,7 +47,7 @@ class UserQuotaTracker(object):
|
|
|
47
47
|
"""When only one item is requested, blocking is is possible."""
|
|
48
48
|
try:
|
|
49
49
|
retry_call(self.c.zpopmin, self._queue_name(user))
|
|
50
|
-
except
|
|
50
|
+
except ResponseError as er:
|
|
51
51
|
# TODO: This is a failsafe for upgrade purposes could be removed in a future version
|
|
52
52
|
if "WRONGTYPE" in str(er):
|
|
53
53
|
retry_call(self.c.delete, self._queue_name(user))
|
|
@@ -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.dev1147"
|
|
156
156
|
description = "Howler - API server"
|
|
157
157
|
authors = [
|
|
158
158
|
"Canadian Centre for Cyber Security <howler@cyber.gc.ca>",
|
|
@@ -197,7 +197,7 @@ repository = "https://github.com/CybercentreCanada/howler-api"
|
|
|
197
197
|
[tool.poetry.dependencies]
|
|
198
198
|
python = "^3.10"
|
|
199
199
|
gunicorn = "23.0.0"
|
|
200
|
-
apscheduler = "3.11.
|
|
200
|
+
apscheduler = "3.11.3"
|
|
201
201
|
authlib = "^1.6.0"
|
|
202
202
|
chardet = "5.2.0"
|
|
203
203
|
elasticsearch = "8.19.3"
|
|
@@ -211,7 +211,7 @@ pyjwt = "2.13.0"
|
|
|
211
211
|
python-baseconv = "1.2.2"
|
|
212
212
|
python-datemath = "3.0.3"
|
|
213
213
|
pyyaml = "6.0.3"
|
|
214
|
-
redis = "
|
|
214
|
+
redis = "8.0.1"
|
|
215
215
|
requests = "2.34.2"
|
|
216
216
|
wsproto = "1.3.2"
|
|
217
217
|
chevron = "0.14.0"
|
|
@@ -240,7 +240,7 @@ pyright = { extras = ["nodejs"], version = "^1.1.408" }
|
|
|
240
240
|
mypy = "^1.6.1"
|
|
241
241
|
|
|
242
242
|
[tool.poetry.group.test.dependencies]
|
|
243
|
-
pytest = "9.
|
|
243
|
+
pytest = "9.1.1"
|
|
244
244
|
retrying = "^1.3.4"
|
|
245
245
|
pyftpdlib = "^1.5.9"
|
|
246
246
|
pyopenssl = ">=24.1,<27.0"
|
|
@@ -255,7 +255,6 @@ 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-redis = "4.6.0.20241004"
|
|
259
258
|
types-six = "1.17.0.20260518"
|
|
260
259
|
types-requests = "<2.33.0.20260519"
|
|
261
260
|
websocket-client = "1.9.0"
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
from flask import Blueprint, abort, make_response
|
|
2
|
-
|
|
3
|
-
from howler.common.loader import datastore
|
|
4
|
-
|
|
5
|
-
API_PREFIX = "/api/healthz"
|
|
6
|
-
healthz = Blueprint("healthz", __name__, url_prefix=API_PREFIX)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@healthz.route("/live")
|
|
10
|
-
def liveness(**_):
|
|
11
|
-
"""Check if the API is live
|
|
12
|
-
|
|
13
|
-
Variables:
|
|
14
|
-
None
|
|
15
|
-
|
|
16
|
-
Arguments:
|
|
17
|
-
None
|
|
18
|
-
|
|
19
|
-
Result Example:
|
|
20
|
-
OK or FAIL
|
|
21
|
-
"""
|
|
22
|
-
return make_response("OK")
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@healthz.route("/ready")
|
|
26
|
-
def readyness(**_):
|
|
27
|
-
"""Check if the API is Ready
|
|
28
|
-
|
|
29
|
-
Variables:
|
|
30
|
-
None
|
|
31
|
-
|
|
32
|
-
Arguments:
|
|
33
|
-
None
|
|
34
|
-
|
|
35
|
-
Result Example:
|
|
36
|
-
OK or FAIL
|
|
37
|
-
"""
|
|
38
|
-
if datastore().ds.ping():
|
|
39
|
-
return make_response("OK")
|
|
40
|
-
else:
|
|
41
|
-
abort(503)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
@healthz.errorhandler(503)
|
|
45
|
-
def error(_):
|
|
46
|
-
"Handle errors exposed in healthz routes"
|
|
47
|
-
return "FAIL", 503
|