howler-api 4.0.0.dev1198__tar.gz → 4.0.0.dev1246__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.dev1198 → howler_api-4.0.0.dev1246}/PKG-INFO +2 -2
- howler_api-4.0.0.dev1246/howler/actions/add_to_bundle.py +171 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/actions/demote.py +2 -2
- howler_api-4.0.0.dev1246/howler/actions/remove_from_bundle.py +144 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/actions/transition.py +7 -5
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/api/__init__.py +1 -2
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/api/socket.py +16 -56
- howler_api-4.0.0.dev1198/howler/services/docs_service.py → howler_api-4.0.0.dev1246/howler/api/v1/__init__.py +46 -38
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/api/v1/action.py +2 -1
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/api/v1/analytic.py +106 -17
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/api/v1/hit.py +128 -52
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/api/v1/tool.py +22 -47
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/api/v1/user.py +1 -1
- howler_api-4.0.0.dev1246/howler/api/v1/utils/etag.py +84 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/app.py +0 -17
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/common/classification.py +0 -1
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/common/loader.py +6 -5
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/common/swagger.py +80 -23
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/cronjobs/retention.py +67 -2
- howler_api-4.0.0.dev1246/howler/cronjobs/rules.py +279 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/datastore/collection.py +29 -47
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/datastore/howler_store.py +14 -29
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/datastore/types.py +6 -1
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/helper/discover.py +8 -12
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/helper/hit.py +4 -4
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/helper/search.py +3 -13
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/base.py +2 -26
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/helper.py +8 -117
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/config.py +27 -14
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/event.py +1 -1
- howler_api-4.0.0.dev1198/howler/odm/models/record.py → howler_api-4.0.0.dev1246/howler/odm/models/hit.py +28 -15
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/howler_data.py +35 -3
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/view.py +0 -8
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/random_data.py +50 -481
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/randomizer.py +2 -6
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/remote/datatypes/__init__.py +4 -11
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/remote/datatypes/queues/comms.py +1 -2
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/security/socket.py +2 -27
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/services/analytic_service.py +2 -6
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/services/config_service.py +3 -5
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/services/dossier_service.py +2 -11
- howler_api-4.0.0.dev1246/howler/services/event_service.py +96 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/services/hit_service.py +177 -94
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/services/user_service.py +1 -7
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/utils/socket_utils.py +26 -5
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/utils/str_utils.py +1 -1
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/pyproject.toml +5 -5
- howler_api-4.0.0.dev1198/howler/actions/add_to_bundle.py +0 -152
- howler_api-4.0.0.dev1198/howler/actions/add_to_case.py +0 -141
- howler_api-4.0.0.dev1198/howler/actions/remove_from_bundle.py +0 -166
- howler_api-4.0.0.dev1198/howler/api/v1/__init__.py +0 -44
- howler_api-4.0.0.dev1198/howler/api/v1/utils/etag.py +0 -106
- howler_api-4.0.0.dev1198/howler/api/v2/__init__.py +0 -44
- howler_api-4.0.0.dev1198/howler/api/v2/case.py +0 -471
- howler_api-4.0.0.dev1198/howler/api/v2/fuzzy.py +0 -110
- howler_api-4.0.0.dev1198/howler/api/v2/ingest.py +0 -369
- howler_api-4.0.0.dev1198/howler/api/v2/search.py +0 -333
- howler_api-4.0.0.dev1198/howler/cronjobs/correlation.py +0 -36
- howler_api-4.0.0.dev1198/howler/odm/constants.py +0 -37
- howler_api-4.0.0.dev1198/howler/odm/mixins.py +0 -97
- howler_api-4.0.0.dev1198/howler/odm/models/case.py +0 -269
- howler_api-4.0.0.dev1198/howler/odm/models/event.py +0 -124
- howler_api-4.0.0.dev1198/howler/odm/models/hit.py +0 -31
- howler_api-4.0.0.dev1198/howler/services/bundle_compat_service.py +0 -282
- howler_api-4.0.0.dev1198/howler/services/case_service.py +0 -1094
- howler_api-4.0.0.dev1198/howler/services/comms_service.py +0 -134
- howler_api-4.0.0.dev1198/howler/services/correlation_service.py +0 -219
- howler_api-4.0.0.dev1198/howler/services/event_service.py +0 -140
- howler_api-4.0.0.dev1198/howler/services/fuzzy_service.py +0 -392
- howler_api-4.0.0.dev1198/howler/services/search_service.py +0 -238
- howler_api-4.0.0.dev1198/howler/services/viewer_service.py +0 -43
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/README.md +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/__init__.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/actions/__init__.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/actions/add_label.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/actions/change_field.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/actions/example_plugin.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/actions/prioritization.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/actions/promote.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/actions/remove_label.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/api/base.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/api/v1/auth.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/api/v1/clue.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/api/v1/configs.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/api/v1/dossier.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/api/v1/help.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/api/v1/notebook.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/api/v1/overview.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/api/v1/search.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/api/v1/template.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/api/v1/utils/__init__.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/api/v1/utils/params.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/api/v1/view.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/common/README.md +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/common/__init__.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/common/classification.yml +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/common/exceptions.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/common/logging/__init__.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/common/logging/audit.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/common/logging/format.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/common/net.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/common/net_static.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/common/random_user.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/config.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/cronjobs/__init__.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/cronjobs/action_queue_worker.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/cronjobs/view_cleanup.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/datastore/README.md +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/datastore/__init__.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/datastore/bulk.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/datastore/constants.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/datastore/exceptions.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/datastore/migrations/fix_process.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/datastore/operations.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/datastore/schemas.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/datastore/store.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/datastore/support/__init__.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/datastore/support/build.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/datastore/support/schemas.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/error.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/external/README.md +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/external/__init__.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/external/generate_mitre.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/external/generate_sigma_rules.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/external/generate_tlds.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/external/reindex_data.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/external/wipe_databases.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/gunicorn_config.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/healthz.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/helper/__init__.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/helper/azure.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/helper/oauth.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/helper/workflow.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/helper/ws.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/README.md +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/__init__.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/charter.txt +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/howler_enum.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/__init__.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/action.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/analytic.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/assemblyline.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/aws.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/azure.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/cbs.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/clue.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/dossier.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/__init__.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/agent.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/autonomous_system.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/client.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/cloud.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/code_signature.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/container.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/dns.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/egress.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/elf.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/email.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/error.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/faas.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/file.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/geo.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/group.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/hash.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/host.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/http.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/ingress.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/interface.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/network.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/observer.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/organization.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/os.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/pe.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/process.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/registry.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/related.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/rule.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/server.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/threat.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/tls.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/url.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/user.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/user_agent.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/ecs/vulnerability.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/gcp.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/lead.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/localized_label.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/overview.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/pivot.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/template.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/odm/models/user.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/patched.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/plugins/__init__.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/plugins/config.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/remote/__init__.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/remote/datatypes/README.md +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/remote/datatypes/counters.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/remote/datatypes/events.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/remote/datatypes/hash.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/remote/datatypes/lock.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/remote/datatypes/queues/__init__.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/remote/datatypes/queues/multi.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/remote/datatypes/queues/named.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/remote/datatypes/queues/priority.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/remote/datatypes/set.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/remote/datatypes/user_quota_tracker.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/security/__init__.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/security/utils.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/services/__init__.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/services/action_service.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/services/auth_service.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/services/jwt_service.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/services/lucene_service.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/services/notebook_service.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/services/overview_service.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/services/template_service.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/telemetry.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/utils/__init__.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/utils/annotations.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/utils/chunk.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/utils/compat.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/utils/constants.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/utils/dict_utils.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/utils/isotime.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/utils/list_utils.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/utils/lucene.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/howler/utils/path.py +0 -0
- {howler_api-4.0.0.dev1198 → howler_api-4.0.0.dev1246}/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.dev1246
|
|
4
4
|
Summary: Howler - API server
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: howler,alerting,gc,canada,cse-cst,cse,cst,cyber,cccs
|
|
@@ -29,7 +29,7 @@ Requires-Dist: elasticsearch (==8.19.3)
|
|
|
29
29
|
Requires-Dist: flasgger (>=0.9.7.1,<0.10.0.0)
|
|
30
30
|
Requires-Dist: flask (==3.1.3)
|
|
31
31
|
Requires-Dist: flask-caching (==2.4.0)
|
|
32
|
-
Requires-Dist: gevent (
|
|
32
|
+
Requires-Dist: gevent (==23.9.1)
|
|
33
33
|
Requires-Dist: gunicorn (==23.0.0)
|
|
34
34
|
Requires-Dist: luqum (>=1.0.0,<2.0.0)
|
|
35
35
|
Requires-Dist: mergedeep (>=1.3.4,<2.0.0)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from howler.actions import check_hit_limit
|
|
4
|
+
from howler.common.loader import datastore
|
|
5
|
+
from howler.datastore.operations import OdmHelper
|
|
6
|
+
from howler.odm.models.action import VALID_TRIGGERS
|
|
7
|
+
from howler.odm.models.hit import Hit
|
|
8
|
+
from howler.odm.models.user import User
|
|
9
|
+
from howler.services import hit_service
|
|
10
|
+
from howler.utils.str_utils import sanitize_lucene_query
|
|
11
|
+
|
|
12
|
+
hit_helper = OdmHelper(Hit)
|
|
13
|
+
|
|
14
|
+
OPERATION_ID = "add_to_bundle"
|
|
15
|
+
MAX_HITS_BASIC = 10
|
|
16
|
+
MAX_HITS_ADVANCED = 1000
|
|
17
|
+
SKIP_CENTRAL_LIMIT = True # This operation transforms the query, handles limit check locally
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def execute(query: str, bundle_id: Optional[str] = None, user: Optional[User] = None, **kwargs):
|
|
21
|
+
"""Add a set of hits matching the query to the specified bundle.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
query (str): The query containing the matching hits
|
|
25
|
+
bundle_id (str): The `howler.id` of the bundle to add the hits to.
|
|
26
|
+
"""
|
|
27
|
+
report = []
|
|
28
|
+
|
|
29
|
+
if not bundle_id:
|
|
30
|
+
return [
|
|
31
|
+
{
|
|
32
|
+
"query": query,
|
|
33
|
+
"outcome": "error",
|
|
34
|
+
"title": "Invalid Bundle ID",
|
|
35
|
+
"message": "Bundle ID cannot be empty.",
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
bundle_hit = hit_service.get_hit(bundle_id, as_odm=True)
|
|
41
|
+
if not bundle_hit or not bundle_hit.howler.is_bundle:
|
|
42
|
+
report.append(
|
|
43
|
+
{
|
|
44
|
+
"query": query,
|
|
45
|
+
"outcome": "error",
|
|
46
|
+
"title": "Invalid Bundle",
|
|
47
|
+
"message": f"Either a hit with ID {bundle_id} does not exist, or it is not a bundle.",
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
return report
|
|
51
|
+
|
|
52
|
+
ds = datastore()
|
|
53
|
+
|
|
54
|
+
skipped_hits_bundles = ds.hit.search(
|
|
55
|
+
f"({query}) AND howler.is_bundle:true",
|
|
56
|
+
fl="howler.id",
|
|
57
|
+
)["items"]
|
|
58
|
+
|
|
59
|
+
if len(skipped_hits_bundles) > 0:
|
|
60
|
+
report.append(
|
|
61
|
+
{
|
|
62
|
+
"query": f"({query}) AND howler.is_bundle:true",
|
|
63
|
+
"outcome": "skipped",
|
|
64
|
+
"title": "Skipped Bundles",
|
|
65
|
+
"message": "Bundles cannot be added to a bundle.",
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
skipped_hits_already_added = ds.hit.search(
|
|
70
|
+
f"({query}) AND (howler.bundles:{sanitize_lucene_query(bundle_id)})",
|
|
71
|
+
fl="howler.id",
|
|
72
|
+
)["items"]
|
|
73
|
+
|
|
74
|
+
if len(skipped_hits_already_added) > 0:
|
|
75
|
+
report.append(
|
|
76
|
+
{
|
|
77
|
+
"query": f"({query}) AND (howler.bundles:{sanitize_lucene_query(bundle_id)})",
|
|
78
|
+
"outcome": "skipped",
|
|
79
|
+
"title": "Skipped Hits",
|
|
80
|
+
"message": "These hits have already been added to the specified bundle.",
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
safe_query = f"({query}) AND (-howler.bundles:({sanitize_lucene_query(bundle_id)}) AND howler.is_bundle:false)"
|
|
85
|
+
|
|
86
|
+
# Check hit limit against the effective query (not raw query)
|
|
87
|
+
if user:
|
|
88
|
+
limit_error = check_hit_limit(safe_query, user, MAX_HITS_BASIC, MAX_HITS_ADVANCED)
|
|
89
|
+
if limit_error:
|
|
90
|
+
return [limit_error]
|
|
91
|
+
|
|
92
|
+
matching_hits = ds.hit.search(safe_query, rows=MAX_HITS_ADVANCED, fl="howler.id")["items"]
|
|
93
|
+
if len(matching_hits) < 1:
|
|
94
|
+
report.append(
|
|
95
|
+
{
|
|
96
|
+
"query": safe_query,
|
|
97
|
+
"outcome": "skipped",
|
|
98
|
+
"title": "No Matching Hits",
|
|
99
|
+
"message": "There were no hits matching this query.",
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
return report
|
|
103
|
+
|
|
104
|
+
ds.hit.update_by_query(
|
|
105
|
+
safe_query,
|
|
106
|
+
[hit_helper.list_add("howler.bundles", sanitize_lucene_query(bundle_id), if_missing=True)],
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
operations = [
|
|
110
|
+
hit_helper.list_add(
|
|
111
|
+
"howler.hits",
|
|
112
|
+
hit["howler"]["id"],
|
|
113
|
+
if_missing=True,
|
|
114
|
+
)
|
|
115
|
+
for hit in matching_hits
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
operations.append(hit_helper.update("howler.bundle_size", len(operations)))
|
|
119
|
+
hit_service.update_hit(
|
|
120
|
+
bundle_id,
|
|
121
|
+
operations,
|
|
122
|
+
)
|
|
123
|
+
bundle_hit = hit_service.get_hit(bundle_id, as_odm=True)
|
|
124
|
+
report.append(
|
|
125
|
+
{
|
|
126
|
+
"query": safe_query.replace("-howler.bundles", "howler.bundles"),
|
|
127
|
+
"outcome": "success",
|
|
128
|
+
"title": "Executed Successfully",
|
|
129
|
+
"message": "The specified bundle has had all matching hits added.",
|
|
130
|
+
}
|
|
131
|
+
)
|
|
132
|
+
except Exception as e:
|
|
133
|
+
report.append(
|
|
134
|
+
{
|
|
135
|
+
"query": query,
|
|
136
|
+
"outcome": "error",
|
|
137
|
+
"title": "Failed to Execute",
|
|
138
|
+
"message": f"Unknown exception occurred: {str(e)}",
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return report
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def specification():
|
|
146
|
+
"""Specify various properties of the action, such as title, descriptions, permissions and input steps."""
|
|
147
|
+
return {
|
|
148
|
+
"id": OPERATION_ID,
|
|
149
|
+
"title": "Add to Bundle",
|
|
150
|
+
"priority": 6,
|
|
151
|
+
"i18nKey": f"operations.{OPERATION_ID}",
|
|
152
|
+
"description": {
|
|
153
|
+
"short": "Add a set of hits to a bundle",
|
|
154
|
+
"long": execute.__doc__,
|
|
155
|
+
},
|
|
156
|
+
"roles": ["automation_basic", "actionrunner_basic"],
|
|
157
|
+
"steps": [
|
|
158
|
+
{
|
|
159
|
+
"args": {"bundle_id": []},
|
|
160
|
+
"options": {},
|
|
161
|
+
"validation": {
|
|
162
|
+
"warn": {"query": "howler.bundles:($bundle_id) OR howler.is_bundle:true"},
|
|
163
|
+
"error": {
|
|
164
|
+
"query": "howler.id:$bundle_id AND howler.is_bundle:false",
|
|
165
|
+
"message": "The bundle id given must be a bundle.",
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
],
|
|
170
|
+
"triggers": VALID_TRIGGERS,
|
|
171
|
+
}
|
|
@@ -9,7 +9,7 @@ from howler.odm.models.howler_data import (
|
|
|
9
9
|
Assessment,
|
|
10
10
|
AssessmentEscalationMap,
|
|
11
11
|
Escalation,
|
|
12
|
-
|
|
12
|
+
HitStatus,
|
|
13
13
|
)
|
|
14
14
|
from howler.odm.models.user import User
|
|
15
15
|
from howler.utils.str_utils import sanitize_lucene_query
|
|
@@ -99,7 +99,7 @@ def execute(
|
|
|
99
99
|
"howler.assignment",
|
|
100
100
|
user.get("uname", "automation") if user else "automation",
|
|
101
101
|
),
|
|
102
|
-
odm_helper.update("howler.status",
|
|
102
|
+
odm_helper.update("howler.status", HitStatus.RESOLVED),
|
|
103
103
|
],
|
|
104
104
|
)
|
|
105
105
|
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from howler.actions import check_hit_limit
|
|
4
|
+
from howler.common.exceptions import HowlerException
|
|
5
|
+
from howler.common.loader import datastore
|
|
6
|
+
from howler.datastore.operations import OdmHelper
|
|
7
|
+
from howler.odm.models.action import VALID_TRIGGERS
|
|
8
|
+
from howler.odm.models.hit import Hit
|
|
9
|
+
from howler.odm.models.user import User
|
|
10
|
+
from howler.services import hit_service
|
|
11
|
+
from howler.utils.str_utils import sanitize_lucene_query
|
|
12
|
+
|
|
13
|
+
hit_helper = OdmHelper(Hit)
|
|
14
|
+
|
|
15
|
+
OPERATION_ID = "remove_from_bundle"
|
|
16
|
+
MAX_HITS_BASIC = 10
|
|
17
|
+
MAX_HITS_ADVANCED = 1000
|
|
18
|
+
SKIP_CENTRAL_LIMIT = True # This operation transforms the query, handles limit check locally
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def execute(query: str, bundle_id: Optional[str] = None, user: Optional[User] = None, **kwargs):
|
|
22
|
+
"""Remove a set of hits matching the query from the specified bundle.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
query (str): The query containing the matching hits
|
|
26
|
+
bundle_id (str): The `howler.id` of the bundle to remove the hits from.
|
|
27
|
+
"""
|
|
28
|
+
report = []
|
|
29
|
+
|
|
30
|
+
if not bundle_id:
|
|
31
|
+
return [
|
|
32
|
+
{
|
|
33
|
+
"query": query,
|
|
34
|
+
"outcome": "error",
|
|
35
|
+
"title": "Invalid Bundle ID",
|
|
36
|
+
"message": "Bundle ID cannot be empty.",
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
bundle_hit = hit_service.get_hit(bundle_id, as_odm=True)
|
|
42
|
+
if not bundle_hit or not bundle_hit.howler.is_bundle:
|
|
43
|
+
report.append(
|
|
44
|
+
{
|
|
45
|
+
"query": query,
|
|
46
|
+
"outcome": "error",
|
|
47
|
+
"title": "Invalid Bundle",
|
|
48
|
+
"message": f"Either a hit with ID {bundle_id} does not exist, or it is not a bundle.",
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
return report
|
|
52
|
+
|
|
53
|
+
ds = datastore()
|
|
54
|
+
|
|
55
|
+
skipped_hits = ds.hit.search(
|
|
56
|
+
f"({query}) AND -howler.bundles:{sanitize_lucene_query(bundle_id)}",
|
|
57
|
+
fl="howler.id",
|
|
58
|
+
)["items"]
|
|
59
|
+
|
|
60
|
+
if len(skipped_hits) > 0:
|
|
61
|
+
report.append(
|
|
62
|
+
{
|
|
63
|
+
"query": f"howler.id:({' OR '.join(h.howler.id for h in skipped_hits)})",
|
|
64
|
+
"outcome": "skipped",
|
|
65
|
+
"title": "Skipped Hit not in Bundle",
|
|
66
|
+
"message": "These hits already are not in the bundle.",
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
safe_query = f"{query} AND (howler.bundles:{sanitize_lucene_query(bundle_id)})"
|
|
71
|
+
|
|
72
|
+
# Check hit limit against the effective query (not raw query)
|
|
73
|
+
if user:
|
|
74
|
+
limit_error = check_hit_limit(safe_query, user, MAX_HITS_BASIC, MAX_HITS_ADVANCED)
|
|
75
|
+
if limit_error:
|
|
76
|
+
return [limit_error]
|
|
77
|
+
|
|
78
|
+
matching_hits = ds.hit.search(safe_query, rows=MAX_HITS_ADVANCED, fl="howler.id")["items"]
|
|
79
|
+
if len(matching_hits) < 1:
|
|
80
|
+
report.append(
|
|
81
|
+
{
|
|
82
|
+
"query": safe_query,
|
|
83
|
+
"outcome": "skipped",
|
|
84
|
+
"title": "No Matching Hits",
|
|
85
|
+
"message": "There were no hits matching this query.",
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
return report
|
|
89
|
+
|
|
90
|
+
ds.hit.update_by_query(
|
|
91
|
+
safe_query,
|
|
92
|
+
[hit_helper.list_remove("howler.bundles", bundle_id)],
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
hit_service.update_hit(
|
|
96
|
+
bundle_id,
|
|
97
|
+
[hit_helper.list_remove("howler.hits", h["howler"]["id"]) for h in matching_hits],
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if len(ds.hit.get(bundle_id).howler.hits) < 1:
|
|
101
|
+
hit_service.update_hit(bundle_id, [hit_helper.update("howler.is_bundle", False)])
|
|
102
|
+
|
|
103
|
+
report.append(
|
|
104
|
+
{
|
|
105
|
+
"query": query,
|
|
106
|
+
"outcome": "success",
|
|
107
|
+
"title": "Executed Successfully",
|
|
108
|
+
"message": f"Matching hits removed from bundle with id {bundle_id}",
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
except HowlerException as e:
|
|
112
|
+
report.append(
|
|
113
|
+
{
|
|
114
|
+
"query": query,
|
|
115
|
+
"outcome": "error",
|
|
116
|
+
"title": "Failed to Execute",
|
|
117
|
+
"message": f"Unknown exception occurred: {str(e)}",
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
return report
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def specification():
|
|
125
|
+
"""Specify various properties of the action, such as title, descriptions, permissions and input steps."""
|
|
126
|
+
return {
|
|
127
|
+
"id": OPERATION_ID,
|
|
128
|
+
"title": "Remove from Bundle",
|
|
129
|
+
"priority": 5,
|
|
130
|
+
"i18nKey": f"operations.{OPERATION_ID}",
|
|
131
|
+
"description": {
|
|
132
|
+
"short": "Remove a set of hits from a bundle",
|
|
133
|
+
"long": execute.__doc__,
|
|
134
|
+
},
|
|
135
|
+
"roles": ["automation_basic", "actionrunner_basic"],
|
|
136
|
+
"steps": [
|
|
137
|
+
{
|
|
138
|
+
"args": {"bundle_id": []},
|
|
139
|
+
"options": {},
|
|
140
|
+
"validation": {"error": {"query": "-howler.bundles:$bundle_id"}},
|
|
141
|
+
}
|
|
142
|
+
],
|
|
143
|
+
"triggers": VALID_TRIGGERS,
|
|
144
|
+
}
|
|
@@ -9,12 +9,12 @@ from howler.helper.workflow import Workflow, WorkflowException
|
|
|
9
9
|
from howler.odm.models.action import VALID_TRIGGERS
|
|
10
10
|
from howler.odm.models.howler_data import (
|
|
11
11
|
Assessment,
|
|
12
|
+
HitStatus,
|
|
12
13
|
HitStatusTransition,
|
|
13
|
-
Status,
|
|
14
14
|
Vote,
|
|
15
15
|
)
|
|
16
16
|
from howler.odm.models.user import User
|
|
17
|
-
from howler.services import
|
|
17
|
+
from howler.services import event_service, hit_service
|
|
18
18
|
from howler.utils.list_utils import flatten_list
|
|
19
19
|
|
|
20
20
|
OPERATION_ID = "transition"
|
|
@@ -147,7 +147,7 @@ def execute(
|
|
|
147
147
|
if total_processed % 10 == 0:
|
|
148
148
|
log.debug("Transition executed on %s hits", total_processed)
|
|
149
149
|
if request_id is not None:
|
|
150
|
-
|
|
150
|
+
event_service.emit(
|
|
151
151
|
"automation",
|
|
152
152
|
{
|
|
153
153
|
"request_id": request_id,
|
|
@@ -193,13 +193,15 @@ def specification():
|
|
|
193
193
|
"steps": [
|
|
194
194
|
{
|
|
195
195
|
"args": {"status": []},
|
|
196
|
-
"options": {"status":
|
|
196
|
+
"options": {"status": HitStatus.list()},
|
|
197
197
|
"validation": {"error": {"query": "-howler.status:$status"}},
|
|
198
198
|
},
|
|
199
199
|
{
|
|
200
200
|
"args": {"transition": []},
|
|
201
201
|
"options": {
|
|
202
|
-
"transition": {
|
|
202
|
+
"transition": {
|
|
203
|
+
f"status:{status}": hit_service.get_transitions(status) for status in HitStatus.list()
|
|
204
|
+
},
|
|
203
205
|
},
|
|
204
206
|
},
|
|
205
207
|
{
|
|
@@ -25,8 +25,7 @@ logger = get_logger(__file__)
|
|
|
25
25
|
|
|
26
26
|
def make_subapi_blueprint(name, api_version=1):
|
|
27
27
|
"""Create a flask Blueprint for a subapi in a standard way."""
|
|
28
|
-
|
|
29
|
-
return Blueprint(full_name, full_name, url_prefix="/".join([API_PREFIX, f"v{api_version}", name]))
|
|
28
|
+
return Blueprint(name, name, url_prefix="/".join([API_PREFIX, f"v{api_version}", name]))
|
|
30
29
|
|
|
31
30
|
|
|
32
31
|
def _make_api_response(
|
|
@@ -6,12 +6,12 @@ from typing import Any
|
|
|
6
6
|
from flask import Blueprint, request
|
|
7
7
|
from opentelemetry import trace
|
|
8
8
|
|
|
9
|
-
import howler.services.
|
|
10
|
-
import howler.services.viewer_service as viewer_service
|
|
9
|
+
import howler.services.event_service as event_service
|
|
11
10
|
from howler.api import ok, unauthorized
|
|
12
11
|
from howler.common.logging import get_logger
|
|
12
|
+
from howler.datastore.operations import OdmHelper
|
|
13
13
|
from howler.helper.ws import ConnectionClosed, Server
|
|
14
|
-
from howler.
|
|
14
|
+
from howler.odm.models.hit import Hit
|
|
15
15
|
from howler.security.socket import websocket_auth, ws_response
|
|
16
16
|
from howler.utils.socket_utils import check_action
|
|
17
17
|
|
|
@@ -24,16 +24,13 @@ socket_api._doc = "Endpoints concerning websocket connectivity between the clien
|
|
|
24
24
|
logger = get_logger(__file__)
|
|
25
25
|
tracer = trace.get_tracer(__name__)
|
|
26
26
|
|
|
27
|
+
hit_helper = OdmHelper(Hit)
|
|
28
|
+
|
|
27
29
|
|
|
28
30
|
@tracer.start_as_current_span(f"{__name__}.emit")
|
|
29
31
|
@socket_api.route("/emit/<event>", methods=["POST"])
|
|
30
32
|
def emit(event: str):
|
|
31
|
-
"""Emit an event to all listening websockets
|
|
32
|
-
|
|
33
|
-
.. deprecated::
|
|
34
|
-
This endpoint is deprecated. Events are now propagated via Redis pubsub
|
|
35
|
-
and no longer require a dedicated websocket pod.
|
|
36
|
-
"""
|
|
33
|
+
"""Emit an event to all listening websockets"""
|
|
37
34
|
if "Authorization" not in request.headers:
|
|
38
35
|
return unauthorized(err="Missing authorization header")
|
|
39
36
|
|
|
@@ -47,30 +44,15 @@ def emit(event: str):
|
|
|
47
44
|
|
|
48
45
|
return unauthorized(err="Invalid auth data")
|
|
49
46
|
|
|
50
|
-
|
|
47
|
+
event_service.emit(event, request.json)
|
|
51
48
|
|
|
52
49
|
return ok()
|
|
53
50
|
|
|
54
51
|
|
|
55
|
-
@tracer.start_as_current_span(f"{__name__}.get_viewers")
|
|
56
|
-
@socket_api.route("/viewers/<entity_id>", methods=["GET"])
|
|
57
|
-
@api_login(audit=False, required_priv=["R"])
|
|
58
|
-
def get_viewers(entity_id: str, **kwargs): # pragma: no cover
|
|
59
|
-
"""Get the list of users currently viewing the specified entity
|
|
60
|
-
|
|
61
|
-
Variables:
|
|
62
|
-
entity_id => The ID of the entity to get viewers for
|
|
63
|
-
|
|
64
|
-
Result Example:
|
|
65
|
-
["user1", "user2"]
|
|
66
|
-
"""
|
|
67
|
-
return ok(viewer_service.get_viewers(entity_id))
|
|
68
|
-
|
|
69
|
-
|
|
70
52
|
@tracer.start_as_current_span(f"{__name__}.connect")
|
|
71
53
|
@socket_api.route("/connect", websocket=True) # type: ignore
|
|
72
54
|
@websocket_auth(required_priv=["R"])
|
|
73
|
-
def connect(ws: Server, *args: Any, ws_id: str, **kwargs):
|
|
55
|
+
def connect(ws: Server, *args: Any, ws_id: str, **kwargs):
|
|
74
56
|
"""Connect to the server to monitor for updates via websocket
|
|
75
57
|
|
|
76
58
|
Variables:
|
|
@@ -82,7 +64,6 @@ def connect(ws: Server, *args: Any, ws_id: str, **kwargs): # noqa: C901
|
|
|
82
64
|
Result Example:
|
|
83
65
|
A continuous websocket connection
|
|
84
66
|
"""
|
|
85
|
-
logger.info("%s: WS connect handler started", ws_id)
|
|
86
67
|
outstanding_actions: list[tuple[str, str, bool]] = []
|
|
87
68
|
|
|
88
69
|
def send_hit(data: dict[str, Any]):
|
|
@@ -97,29 +78,14 @@ def connect(ws: Server, *args: Any, ws_id: str, **kwargs): # noqa: C901
|
|
|
97
78
|
logger.debug("Sending action: %s", data)
|
|
98
79
|
ws.send(ws_response("action", data))
|
|
99
80
|
|
|
100
|
-
def send_case(data: dict[str, Any]): # pragma: no cover
|
|
101
|
-
logger.debug("Sending case update: %s", data.get("case", {}).get("case_id", "unknown"))
|
|
102
|
-
ws.send(ws_response("cases", data))
|
|
103
|
-
|
|
104
|
-
def send_viewers_update(data: dict[str, Any]): # pragma: no cover
|
|
105
|
-
logger.debug("Sending viewers update: %s", data.get("id", "unknown"))
|
|
106
|
-
ws.send(ws_response("viewers_update", data))
|
|
107
|
-
|
|
108
81
|
try:
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
)
|
|
113
|
-
comms_service.on("hits", send_hit)
|
|
114
|
-
comms_service.on("broadcast", send_broadcast)
|
|
115
|
-
comms_service.on("action", send_action)
|
|
116
|
-
comms_service.on("cases", send_case)
|
|
117
|
-
comms_service.on("viewers_update", send_viewers_update)
|
|
82
|
+
event_service.on("hits", send_hit)
|
|
83
|
+
event_service.on("broadcast", send_broadcast)
|
|
84
|
+
event_service.on("action", send_action)
|
|
118
85
|
while ws.connected:
|
|
119
86
|
data = ws.receive(10)
|
|
120
87
|
if data:
|
|
121
88
|
obj = json.loads(data)
|
|
122
|
-
logger.debug("%s: Received message: keys=%s", ws_id, list(obj.keys()))
|
|
123
89
|
|
|
124
90
|
if "id" not in obj or "action" not in obj or "broadcast" not in obj:
|
|
125
91
|
ws.close(
|
|
@@ -131,28 +97,22 @@ def connect(ws: Server, *args: Any, ws_id: str, **kwargs): # noqa: C901
|
|
|
131
97
|
message="Sent data is invalid.",
|
|
132
98
|
),
|
|
133
99
|
)
|
|
134
|
-
logger.warning("%s: Closed due to invalid payload: %s", ws_id, obj)
|
|
135
100
|
return
|
|
136
101
|
|
|
137
102
|
outstanding_actions = check_action(
|
|
138
103
|
obj["id"], obj["action"], obj["broadcast"], outstanding_actions=outstanding_actions, **kwargs
|
|
139
104
|
)
|
|
140
|
-
logger.debug("%s: Outstanding actions count=%s", ws_id, len(outstanding_actions))
|
|
141
105
|
else:
|
|
142
|
-
logger.debug("
|
|
106
|
+
logger.debug(ws_id + " listening")
|
|
143
107
|
except Exception as e:
|
|
144
108
|
if isinstance(e, ConnectionClosed):
|
|
145
109
|
raise
|
|
146
110
|
else:
|
|
147
|
-
logger.exception("
|
|
111
|
+
logger.exception("Exception on connect.")
|
|
148
112
|
finally:
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
comms_service.off("action", send_action)
|
|
153
|
-
comms_service.off("cases", send_case)
|
|
154
|
-
comms_service.off("viewers_update", send_viewers_update)
|
|
113
|
+
event_service.off("hits", send_hit)
|
|
114
|
+
event_service.off("broadcast", send_broadcast)
|
|
115
|
+
event_service.off("action", send_action)
|
|
155
116
|
|
|
156
117
|
for id, action, broadcast in outstanding_actions:
|
|
157
118
|
outstanding_actions = check_action(id, action, broadcast, outstanding_actions=outstanding_actions, **kwargs)
|
|
158
|
-
logger.info("%s: WS connect handler finished", ws_id)
|
|
@@ -1,39 +1,47 @@
|
|
|
1
1
|
from textwrap import dedent
|
|
2
2
|
|
|
3
|
-
from flask import current_app, request
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
3
|
+
from flask import Blueprint, current_app, request
|
|
4
|
+
|
|
5
|
+
from howler.api import ok
|
|
6
|
+
from howler.security import api_login
|
|
7
|
+
|
|
8
|
+
API_PREFIX = "/api/v1"
|
|
9
|
+
apiv1 = Blueprint("apiv1", __name__, url_prefix=API_PREFIX)
|
|
10
|
+
apiv1._doc = "Api Documentation Version 1" # type: ignore[attr-defined] # type: ignore
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@apiv1.route("/")
|
|
14
|
+
@api_login(audit=False, required_priv=["R", "W"], required_type=["user", "admin"])
|
|
15
|
+
def get_api_documentation(**kwargs):
|
|
16
|
+
"""Full API doc.
|
|
17
|
+
|
|
18
|
+
Loop through all registered API paths and display their documentation.
|
|
19
|
+
Returns a list of API definition.
|
|
20
|
+
|
|
21
|
+
Variables:
|
|
22
|
+
None
|
|
23
|
+
|
|
24
|
+
Arguments:
|
|
25
|
+
None
|
|
26
|
+
|
|
27
|
+
Result Example:
|
|
28
|
+
[
|
|
29
|
+
{
|
|
30
|
+
'name': "Api Doc", # Name of the api
|
|
31
|
+
'path': "/api/path/<variable>/", # API path
|
|
32
|
+
'ui_only': false, # Is UI only API
|
|
33
|
+
'methods': ["GET", "POST"], # Allowed HTTP methods
|
|
34
|
+
'description': "API doc.", # API documentation
|
|
35
|
+
'id': "api_doc", # Unique ID for the API
|
|
36
|
+
'function': "apiv1.api_doc", # Function called in the code
|
|
37
|
+
'protected': False, # Does the API require login?
|
|
38
|
+
'required_type': ['user'], # Type of users allowed to use API
|
|
39
|
+
'complete' : True # Is the API stable?
|
|
40
|
+
},
|
|
41
|
+
]
|
|
36
42
|
"""
|
|
43
|
+
user_types = kwargs["user"]["type"]
|
|
44
|
+
|
|
37
45
|
api_blueprints = {}
|
|
38
46
|
api_list = []
|
|
39
47
|
for rule in current_app.url_map.iter_rules():
|
|
@@ -50,13 +58,13 @@ def build_route_docs(version: str, user_types: list[str]):
|
|
|
50
58
|
[x.capitalize() for x in rule.endpoint[rule.endpoint.rindex(".") + 1 :].split("_")]
|
|
51
59
|
)
|
|
52
60
|
blueprint = rule.endpoint[: rule.endpoint.rindex(".")]
|
|
53
|
-
if blueprint ==
|
|
61
|
+
if blueprint == "apiv1":
|
|
54
62
|
blueprint = "documentation"
|
|
55
63
|
|
|
56
64
|
if blueprint not in api_blueprints:
|
|
57
65
|
try:
|
|
58
66
|
doc = current_app.blueprints[rule.endpoint[: rule.endpoint.rindex(".")]]._doc # type: ignore[attr-defined]
|
|
59
|
-
except Exception:
|
|
67
|
+
except Exception:
|
|
60
68
|
doc = ""
|
|
61
69
|
|
|
62
70
|
api_blueprints[blueprint] = doc
|
|
@@ -66,7 +74,7 @@ def build_route_docs(version: str, user_types: list[str]):
|
|
|
66
74
|
else:
|
|
67
75
|
description = "[INCOMPLETE]\n\nTHIS API HAS NOT BEEN DOCUMENTED YET!"
|
|
68
76
|
|
|
69
|
-
api_id = rule.endpoint.replace(
|
|
77
|
+
api_id = rule.endpoint.replace("apiv1.", "").replace(".", "_")
|
|
70
78
|
|
|
71
79
|
api_list.append(
|
|
72
80
|
{
|
|
@@ -74,7 +82,7 @@ def build_route_docs(version: str, user_types: list[str]):
|
|
|
74
82
|
"required_type": sorted(required_type),
|
|
75
83
|
"name": func_title,
|
|
76
84
|
"id": api_id,
|
|
77
|
-
"function": f"api.
|
|
85
|
+
"function": f"api.v1.{rule.endpoint}",
|
|
78
86
|
"path": rule.rule,
|
|
79
87
|
"ui_only": rule.rule.startswith("%sui/" % request.path),
|
|
80
88
|
"methods": sorted(methods),
|
|
@@ -86,4 +94,4 @@ def build_route_docs(version: str, user_types: list[str]):
|
|
|
86
94
|
|
|
87
95
|
break
|
|
88
96
|
|
|
89
|
-
return {"apis": api_list, "blueprints": api_blueprints}
|
|
97
|
+
return ok({"apis": api_list, "blueprints": api_blueprints})
|