howler-api 4.0.0.dev1298__tar.gz → 4.0.0.dev1308__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/PKG-INFO +1 -1
  2. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/hit.py +9 -11
  3. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/tool.py +14 -11
  4. howler_api-4.0.0.dev1308/howler/datastore/bulk.py +148 -0
  5. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/datastore/collection.py +21 -0
  6. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/base.py +3 -0
  7. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/config.py +11 -1
  8. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/analytic_service.py +4 -4
  9. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/hit_service.py +40 -0
  10. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/utils/constants.py +1 -0
  11. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/utils/indexes.py +3 -1
  12. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/pyproject.toml +2 -2
  13. howler_api-4.0.0.dev1298/howler/datastore/bulk.py +0 -72
  14. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/README.md +0 -0
  15. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/__init__.py +0 -0
  16. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/actions/__init__.py +0 -0
  17. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/actions/add_label.py +0 -0
  18. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/actions/add_to_bundle.py +0 -0
  19. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/actions/add_to_case.py +0 -0
  20. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/actions/change_field.py +0 -0
  21. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/actions/demote.py +0 -0
  22. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/actions/example_plugin.py +0 -0
  23. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/actions/prioritization.py +0 -0
  24. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/actions/promote.py +0 -0
  25. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/actions/remove_from_bundle.py +0 -0
  26. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/actions/remove_label.py +0 -0
  27. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/actions/transition.py +0 -0
  28. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/__init__.py +0 -0
  29. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/base.py +0 -0
  30. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/socket.py +0 -0
  31. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/__init__.py +0 -0
  32. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/action.py +0 -0
  33. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/analytic.py +0 -0
  34. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/auth.py +0 -0
  35. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/clue.py +0 -0
  36. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/configs.py +0 -0
  37. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/dossier.py +0 -0
  38. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/help.py +0 -0
  39. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/notebook.py +0 -0
  40. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/overview.py +0 -0
  41. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/search.py +0 -0
  42. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/template.py +0 -0
  43. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/user.py +0 -0
  44. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/utils/__init__.py +0 -0
  45. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/utils/etag.py +0 -0
  46. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/utils/params.py +0 -0
  47. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v1/view.py +0 -0
  48. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v2/__init__.py +0 -0
  49. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v2/case.py +0 -0
  50. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v2/fuzzy.py +0 -0
  51. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v2/ingest.py +0 -0
  52. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/api/v2/search.py +0 -0
  53. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/app.py +0 -0
  54. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/common/README.md +0 -0
  55. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/common/__init__.py +0 -0
  56. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/common/classification.py +0 -0
  57. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/common/classification.yml +0 -0
  58. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/common/exceptions.py +0 -0
  59. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/common/loader.py +0 -0
  60. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/common/logging/__init__.py +0 -0
  61. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/common/logging/audit.py +0 -0
  62. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/common/logging/format.py +0 -0
  63. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/common/net.py +0 -0
  64. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/common/net_static.py +0 -0
  65. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/common/random_user.py +0 -0
  66. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/common/swagger.py +0 -0
  67. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/config.py +0 -0
  68. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/cronjobs/__init__.py +0 -0
  69. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/cronjobs/action_queue_worker.py +0 -0
  70. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/cronjobs/correlation.py +0 -0
  71. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/cronjobs/retention.py +0 -0
  72. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/cronjobs/view_cleanup.py +0 -0
  73. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/datastore/README.md +0 -0
  74. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/datastore/__init__.py +0 -0
  75. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/datastore/constants.py +0 -0
  76. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/datastore/exceptions.py +0 -0
  77. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/datastore/howler_store.py +0 -0
  78. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/datastore/migrations/fix_process.py +0 -0
  79. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/datastore/operations.py +0 -0
  80. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/datastore/schemas.py +0 -0
  81. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/datastore/store.py +0 -0
  82. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/datastore/support/__init__.py +0 -0
  83. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/datastore/support/build.py +0 -0
  84. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/datastore/support/schemas.py +0 -0
  85. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/datastore/types.py +0 -0
  86. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/error.py +0 -0
  87. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/external/README.md +0 -0
  88. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/external/__init__.py +0 -0
  89. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/external/generate_mitre.py +0 -0
  90. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/external/generate_sigma_rules.py +0 -0
  91. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/external/generate_tlds.py +0 -0
  92. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/external/reindex_data.py +0 -0
  93. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/external/wipe_databases.py +0 -0
  94. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/gunicorn_config.py +0 -0
  95. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/healthz.py +0 -0
  96. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/helper/__init__.py +0 -0
  97. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/helper/azure.py +0 -0
  98. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/helper/discover.py +0 -0
  99. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/helper/hit.py +0 -0
  100. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/helper/oauth.py +0 -0
  101. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/helper/search.py +0 -0
  102. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/helper/workflow.py +0 -0
  103. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/helper/ws.py +0 -0
  104. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/README.md +0 -0
  105. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/__init__.py +0 -0
  106. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/charter.txt +0 -0
  107. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/constants.py +0 -0
  108. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/helper.py +0 -0
  109. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/howler_enum.py +0 -0
  110. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/mixins.py +0 -0
  111. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/__init__.py +0 -0
  112. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/action.py +0 -0
  113. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/analytic.py +0 -0
  114. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/assemblyline.py +0 -0
  115. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/aws.py +0 -0
  116. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/azure.py +0 -0
  117. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/case.py +0 -0
  118. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/cbs.py +0 -0
  119. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/clue.py +0 -0
  120. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/dossier.py +0 -0
  121. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/__init__.py +0 -0
  122. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/agent.py +0 -0
  123. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/autonomous_system.py +0 -0
  124. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/client.py +0 -0
  125. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/cloud.py +0 -0
  126. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/code_signature.py +0 -0
  127. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/container.py +0 -0
  128. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/dns.py +0 -0
  129. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/egress.py +0 -0
  130. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/elf.py +0 -0
  131. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/email.py +0 -0
  132. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/error.py +0 -0
  133. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/event.py +0 -0
  134. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/faas.py +0 -0
  135. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/file.py +0 -0
  136. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/geo.py +0 -0
  137. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/group.py +0 -0
  138. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/hash.py +0 -0
  139. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/host.py +0 -0
  140. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/http.py +0 -0
  141. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/ingress.py +0 -0
  142. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/interface.py +0 -0
  143. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/network.py +0 -0
  144. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/observer.py +0 -0
  145. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/organization.py +0 -0
  146. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/os.py +0 -0
  147. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/pe.py +0 -0
  148. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/process.py +0 -0
  149. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/registry.py +0 -0
  150. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/related.py +0 -0
  151. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/rule.py +0 -0
  152. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/server.py +0 -0
  153. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/threat.py +0 -0
  154. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/tls.py +0 -0
  155. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/url.py +0 -0
  156. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/user.py +0 -0
  157. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/user_agent.py +0 -0
  158. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/ecs/vulnerability.py +0 -0
  159. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/event.py +0 -0
  160. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/gcp.py +0 -0
  161. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/hit.py +0 -0
  162. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/howler_data.py +0 -0
  163. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/lead.py +0 -0
  164. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/localized_label.py +0 -0
  165. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/overview.py +0 -0
  166. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/pivot.py +0 -0
  167. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/record.py +0 -0
  168. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/template.py +0 -0
  169. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/user.py +0 -0
  170. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/models/view.py +0 -0
  171. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/random_data.py +0 -0
  172. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/odm/randomizer.py +0 -0
  173. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/patched.py +0 -0
  174. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/plugins/__init__.py +0 -0
  175. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/plugins/config.py +0 -0
  176. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/remote/__init__.py +0 -0
  177. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/remote/datatypes/README.md +0 -0
  178. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/remote/datatypes/__init__.py +0 -0
  179. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/remote/datatypes/counters.py +0 -0
  180. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/remote/datatypes/events.py +0 -0
  181. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/remote/datatypes/hash.py +0 -0
  182. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/remote/datatypes/lock.py +0 -0
  183. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/remote/datatypes/queues/__init__.py +0 -0
  184. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/remote/datatypes/queues/comms.py +0 -0
  185. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/remote/datatypes/queues/multi.py +0 -0
  186. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/remote/datatypes/queues/named.py +0 -0
  187. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/remote/datatypes/queues/priority.py +0 -0
  188. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/remote/datatypes/set.py +0 -0
  189. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/remote/datatypes/user_quota_tracker.py +0 -0
  190. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/security/__init__.py +0 -0
  191. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/security/socket.py +0 -0
  192. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/security/utils.py +0 -0
  193. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/__init__.py +0 -0
  194. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/action_service.py +0 -0
  195. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/auth_service.py +0 -0
  196. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/bundle_compat_service.py +0 -0
  197. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/case_service.py +0 -0
  198. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/comms_service.py +0 -0
  199. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/config_service.py +0 -0
  200. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/correlation_service.py +0 -0
  201. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/docs_service.py +0 -0
  202. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/dossier_service.py +0 -0
  203. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/event_service.py +0 -0
  204. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/fuzzy_service.py +0 -0
  205. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/jwt_service.py +0 -0
  206. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/lucene_service.py +0 -0
  207. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/notebook_service.py +0 -0
  208. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/overview_service.py +0 -0
  209. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/search_service.py +0 -0
  210. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/template_service.py +0 -0
  211. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/user_service.py +0 -0
  212. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/services/viewer_service.py +0 -0
  213. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/telemetry.py +0 -0
  214. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/utils/__init__.py +0 -0
  215. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/utils/annotations.py +0 -0
  216. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/utils/chunk.py +0 -0
  217. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/utils/compat.py +0 -0
  218. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/utils/dict_utils.py +0 -0
  219. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/utils/isotime.py +0 -0
  220. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/utils/list_utils.py +0 -0
  221. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/utils/lucene.py +0 -0
  222. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/utils/net_utils.py +0 -0
  223. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/utils/path.py +0 -0
  224. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/utils/socket_utils.py +0 -0
  225. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/howler/utils/str_utils.py +0 -0
  226. {howler_api-4.0.0.dev1298 → howler_api-4.0.0.dev1308}/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.dev1298
3
+ Version: 4.0.0.dev1308
4
4
  Summary: Howler - API server
5
5
  License: MIT
6
6
  Keywords: howler,alerting,gc,canada,cse-cst,cse,cst,cyber,cccs
@@ -31,6 +31,7 @@ from howler.odm.models.howler_data import Comment, HitOperationType, HitStatusTr
31
31
  from howler.odm.models.user import User
32
32
  from howler.security import api_login
33
33
  from howler.services import action_service, analytic_service, comms_service, hit_service
34
+ from howler.utils.constants import DEBUG_FORCE_REFRESH
34
35
  from howler.utils.str_utils import sanitize_lucene_query
35
36
 
36
37
  MAX_COMMENT_LEN = 5000
@@ -60,7 +61,8 @@ def create_hits(user: User, **kwargs):
60
61
  None
61
62
 
62
63
  Optional Arguments:
63
- refresh => ('true') Whether to refresh (commit) the datastore before returning.
64
+ refresh => ('true' | 'false' | 'wait_for') Whether to refresh the datastore before returning.
65
+ 'wait_for' will wait for the change to be visible in search.
64
66
 
65
67
  Data Block:
66
68
  {
@@ -121,17 +123,12 @@ def create_hits(user: User, **kwargs):
121
123
 
122
124
  if len(response_body["invalid"]) == 0:
123
125
  if len(odms) > 0:
124
- for odm in odms: # Save all but the last hit without refreshing
126
+ for odm in odms:
125
127
  # Ensure all ids are consistent
126
128
  if odm.event is not None:
127
129
  odm.event.id = odm.howler.id
128
- # TODO keeping the commit approach until we can batch create hits
129
- # passing refresh to each one is potentially even more inefficient
130
- hit_service.create_hit(odm.howler.id, odm, user=user.uname, refresh="false")
131
-
132
- if refresh == "true":
133
- datastore().hit.commit()
134
130
 
131
+ hit_service.create_hits(odms, user=user.uname, refresh="true" if DEBUG_FORCE_REFRESH else refresh)
135
132
  analytic_service.save_from_hits(odms, user, refresh=refresh)
136
133
  action_service.enqueue_action_execution([odm.howler.id for odm in odms], trigger="create", user=user)
137
134
 
@@ -577,7 +574,7 @@ def add_label(id, label_set, user, **kwargs):
577
574
  id,
578
575
  [hit_helper.list_add(f"howler.labels.{label_set}", label) for label in labels],
579
576
  user["uname"],
580
- refresh=refresh,
577
+ refresh="true" if DEBUG_FORCE_REFRESH else refresh,
581
578
  )
582
579
 
583
580
  action_service.enqueue_action_execution([id], trigger="add_label", user=user)
@@ -634,7 +631,7 @@ def remove_labels(id, label_set, user, **kwargs):
634
631
  id,
635
632
  [hit_helper.list_remove(f"howler.labels.{label_set}", label) for label in labels],
636
633
  user["uname"],
637
- refresh=refresh,
634
+ refresh="true" if DEBUG_FORCE_REFRESH else refresh,
638
635
  )
639
636
 
640
637
  action_service.enqueue_action_execution([id], trigger="remove_label", user=user)
@@ -1042,7 +1039,8 @@ def create_bundle(user: User, **kwargs):
1042
1039
  None
1043
1040
 
1044
1041
  Optional Arguments:
1045
- refresh => ('true') Whether to refresh (commit) the datastore before returning.
1042
+ refresh => ('true' | 'false' | 'wait_for') Whether to refresh the datastore before returning.
1043
+ 'wait_for' will wait for the change to be visible in search.
1046
1044
 
1047
1045
  Data Block:
1048
1046
  {
@@ -3,6 +3,7 @@ from typing import Any, Optional
3
3
  from flask import request
4
4
 
5
5
  from howler.api import bad_request, created, make_subapi_blueprint
6
+ from howler.api.v1.utils.params import parse_parameters, parse_refresh
6
7
  from howler.common.exceptions import HowlerException
7
8
  from howler.common.loader import datastore
8
9
  from howler.common.logging import get_logger
@@ -13,6 +14,7 @@ from howler.odm.models.hit import Hit
13
14
  from howler.odm.models.user import User
14
15
  from howler.security import api_login
15
16
  from howler.services import action_service, analytic_service, hit_service
17
+ from howler.utils.constants import DEBUG_FORCE_REFRESH
16
18
  from howler.utils.dict_utils import flatten
17
19
  from howler.utils.isotime import now_as_iso
18
20
  from howler.utils.str_utils import get_parent_key
@@ -30,6 +32,7 @@ hit_helper = OdmHelper(Hit)
30
32
  @generate_swagger_docs()
31
33
  @tool_api.route("/<tool_name>/hits", methods=["POST", "PUT"])
32
34
  @api_login(required_priv=["W"])
35
+ @parse_parameters(refresh=parse_refresh)
33
36
  def create_one_or_many_hits(tool_name: str, user: User, **kwargs): # noqa: C901
34
37
  """Create one or many hits for a tool using field mapping.
35
38
 
@@ -39,6 +42,10 @@ def create_one_or_many_hits(tool_name: str, user: User, **kwargs): # noqa: C901
39
42
  Arguments:
40
43
  None
41
44
 
45
+ Optional Arguments:
46
+ refresh => ('true' | 'false' | 'wait_for') Whether to refresh the datastore before returning.
47
+ 'wait_for' will wait for the change to be visible in search.
48
+
42
49
  Data Block:
43
50
  {
44
51
  "map": { # For each field in the hit, list of field data will be copied to
@@ -64,6 +71,7 @@ def create_one_or_many_hits(tool_name: str, user: User, **kwargs): # noqa: C901
64
71
  Use POST /api/v1/hit/ directly with pre-mapped hit data instead.
65
72
  """
66
73
  data = request.json
74
+ refresh = kwargs.get("refresh", None)
67
75
  if not isinstance(data, dict):
68
76
  return bad_request(err="Invalid data format")
69
77
 
@@ -204,10 +212,9 @@ def create_one_or_many_hits(tool_name: str, user: User, **kwargs): # noqa: C901
204
212
  if bundle_odm is not None:
205
213
  child_odms = [odm for odm in odms if odm.howler.id != bundle_id]
206
214
 
207
- for odm in child_odms:
208
- hit_service.create_hit(odm.howler.id, odm, user=user.uname)
215
+ hit_service.create_hits(child_odms, user=user.uname, refresh="true" if DEBUG_FORCE_REFRESH else refresh)
216
+ analytic_service.save_from_hits(odms, user, refresh=refresh)
209
217
 
210
- analytic_service.save_from_hits(odms, user)
211
218
  child_ids = [odm.howler.id for odm in child_odms]
212
219
  bundle_data = bundle_odm.as_primitives()
213
220
  result = bundle_compat_service.create_bundle(bundle_data, child_ids, user=user, refresh="wait_for")
@@ -220,15 +227,11 @@ def create_one_or_many_hits(tool_name: str, user: User, **kwargs): # noqa: C901
220
227
  entry["_case_id"] = result.get("_case_id")
221
228
  else:
222
229
  # The bundle hit may have been dropped by de-duplication; ingest remaining hits directly.
223
- for odm in odms:
224
- hit_service.create_hit(odm.howler.id, odm, user=user.uname)
225
- analytic_service.save_from_hits(odms, user)
230
+ hit_service.create_hits(odms, user=user.uname, refresh="true" if DEBUG_FORCE_REFRESH else refresh)
231
+ analytic_service.save_from_hits(odms, user, refresh=refresh)
226
232
  else:
227
- for odm in odms:
228
- hit_service.create_hit(odm.howler.id, odm, user=user.uname)
229
- analytic_service.save_from_hits(odms, user)
230
-
231
- datastore().hit.commit()
233
+ hit_service.create_hits(odms, user=user.uname, refresh="true" if DEBUG_FORCE_REFRESH else refresh)
234
+ analytic_service.save_from_hits(odms, user, refresh=refresh)
232
235
 
233
236
  action_service.enqueue_action_execution([entry["id"] for entry in out], trigger="create", user=user)
234
237
 
@@ -0,0 +1,148 @@
1
+ import json
2
+ import warnings
3
+ from copy import deepcopy
4
+ from typing import List, Optional
5
+
6
+ from howler import odm
7
+ from howler.config import config
8
+
9
+ _OPERATION_GROUP = tuple[str] | tuple[str, str]
10
+
11
+ ELASTIC_MAX_REQUEST_SIZE = config.datastore.max_request_size
12
+ DEFAULT_BATCH_SIZE = config.datastore.request_batch_size
13
+
14
+
15
+ class ElasticBulkPlan(object):
16
+ def __init__(self, indexes: List[str], model: Optional[type[odm.Model]] = None):
17
+ self.indexes = indexes
18
+ self.model = model
19
+ self.operations: list[_OPERATION_GROUP] = []
20
+
21
+ @property
22
+ def empty(self):
23
+ return len(self.operations) == 0
24
+
25
+ def add_delete_operation(self, doc_id, index=None):
26
+ if index:
27
+ self.operations.append((json.dumps({"delete": {"_index": index, "_id": doc_id}}),))
28
+ else:
29
+ for cur_index in self.indexes:
30
+ self.operations.append((json.dumps({"delete": {"_index": cur_index, "_id": doc_id}}),))
31
+
32
+ def add_insert_operation(self, doc_id: str, doc, index=None):
33
+ if self.model and isinstance(doc, self.model):
34
+ saved_doc = doc.as_primitives(hidden_fields=True)
35
+ elif self.model:
36
+ saved_doc = self.model(doc).as_primitives(hidden_fields=True)
37
+ else:
38
+ if not isinstance(doc, dict):
39
+ saved_doc = {"__non_doc_raw__": doc}
40
+ else:
41
+ saved_doc = deepcopy(doc)
42
+ saved_doc["id"] = doc_id
43
+
44
+ self.operations.append(
45
+ (
46
+ json.dumps({"create": {"_index": index or self.indexes[0], "_id": doc_id}}),
47
+ json.dumps(saved_doc),
48
+ )
49
+ )
50
+
51
+ def add_index_operation(self, doc_id, doc, index=None):
52
+ if self.model and isinstance(doc, self.model):
53
+ saved_doc = doc.as_primitives(hidden_fields=True)
54
+ elif self.model:
55
+ saved_doc = self.model(doc).as_primitives(hidden_fields=True)
56
+ else:
57
+ if not isinstance(doc, dict):
58
+ saved_doc = {"__non_doc_raw__": doc}
59
+ else:
60
+ saved_doc = deepcopy(doc)
61
+ saved_doc["id"] = doc_id
62
+
63
+ self.operations.append(
64
+ (
65
+ json.dumps({"index": {"_index": index or self.indexes[0], "_id": doc_id}}),
66
+ json.dumps(saved_doc),
67
+ )
68
+ )
69
+
70
+ def add_upsert_operation(self, doc_id, doc, index=None):
71
+ if self.model and isinstance(doc, self.model):
72
+ saved_doc = doc.as_primitives(hidden_fields=True)
73
+ elif self.model:
74
+ saved_doc = self.model(doc).as_primitives(hidden_fields=True)
75
+ else:
76
+ if not isinstance(doc, dict):
77
+ saved_doc = {"__non_doc_raw__": doc}
78
+ else:
79
+ saved_doc = deepcopy(doc)
80
+ saved_doc["id"] = doc_id
81
+
82
+ self.operations.append(
83
+ (
84
+ json.dumps({"update": {"_index": index or self.indexes[0], "_id": doc_id}}),
85
+ json.dumps({"doc": saved_doc, "doc_as_upsert": True}),
86
+ )
87
+ )
88
+
89
+ def add_update_operation(self, doc_id, doc, index=None):
90
+ if self.model and isinstance(doc, self.model):
91
+ saved_doc = doc.as_primitives(hidden_fields=True)
92
+ elif self.model:
93
+ saved_doc = self.model(doc, mask=list(doc.keys())).as_primitives(hidden_fields=True)
94
+ else:
95
+ if not isinstance(doc, dict):
96
+ saved_doc = {"__non_doc_raw__": doc}
97
+ else:
98
+ saved_doc = deepcopy(doc)
99
+
100
+ if index:
101
+ self.operations.append(
102
+ (
103
+ json.dumps({"update": {"_index": index, "_id": doc_id}}),
104
+ json.dumps({"doc": saved_doc}),
105
+ )
106
+ )
107
+ else:
108
+ for cur_index in self.indexes:
109
+ self.operations.append(
110
+ (
111
+ json.dumps({"update": {"_index": cur_index, "_id": doc_id}}),
112
+ json.dumps({"doc": saved_doc}),
113
+ )
114
+ )
115
+
116
+ def get_plan_data(self):
117
+ """Construct the bulk request from the current operations"""
118
+ return self._get_plan_for_operations()
119
+
120
+ def get_plan_batches(self, batch_size: int | None = DEFAULT_BATCH_SIZE):
121
+ """Yield plan data in batches"""
122
+ if batch_size is None:
123
+ batch_size = len(self.operations)
124
+ for ptr in range(0, len(self.operations), batch_size):
125
+ yield self._get_plan_for_operations(self.operations[ptr : ptr + batch_size])
126
+
127
+ def _flatten_operations(self, batch: List[_OPERATION_GROUP] | None = None) -> List[str]:
128
+ """Flatten the (operation, data) tuples for a batch or the full list of operations if no batch provided"""
129
+ if not batch:
130
+ batch = self.operations
131
+
132
+ flattened: list[str] = []
133
+ for op in batch:
134
+ flattened.extend(op)
135
+
136
+ return flattened
137
+
138
+ def _get_plan_for_operations(self, batch: List[_OPERATION_GROUP] | None = None) -> str:
139
+ """Get the bulk plan string for a batch or the full list of operations if no batch provided"""
140
+ plan = "\n".join(self._flatten_operations(batch)) + "\n"
141
+
142
+ if ELASTIC_MAX_REQUEST_SIZE and len(plan.encode("utf-8")) > ELASTIC_MAX_REQUEST_SIZE:
143
+ warnings.warn(
144
+ f"Bulk plan exceeds maximum request size of {ELASTIC_MAX_REQUEST_SIZE} bytes. "
145
+ f"Current size: {len(plan.encode('utf-8'))} bytes."
146
+ )
147
+
148
+ return plan
@@ -22,6 +22,7 @@ from howler import odm
22
22
  from howler.common.exceptions import HowlerRuntimeError, HowlerValueError, NonRecoverableError
23
23
  from howler.common.loader import DATASTORE_INDEX_PREFIX
24
24
  from howler.common.logging.format import HWL_DATE_FORMAT, HWL_LOG_FORMAT
25
+ from howler.datastore.bulk import ElasticBulkPlan
25
26
  from howler.datastore.constants import BACK_MAPPING, TYPE_MAPPING
26
27
  from howler.datastore.exceptions import (
27
28
  DataStoreException,
@@ -523,6 +524,26 @@ class ESCollection(Generic[ModelType]):
523
524
  else:
524
525
  updated += res["updated"]
525
526
 
527
+ def bulk(self, operations: ElasticBulkPlan, refresh: str | None = None):
528
+ """
529
+ Execute a bulk plan.
530
+
531
+ :return: True if the operation completed without errors
532
+ """
533
+ responses = []
534
+ for operation_batch in operations.get_plan_batches():
535
+ response = self.with_retries(self.datastore.client.bulk, operations=operation_batch, refresh=refresh)
536
+ responses.append(response)
537
+ return not any(response["errors"] for response in responses)
538
+
539
+ def get_bulk_plan(self):
540
+ """
541
+ Create a BulkPlan tailored for the current datastore
542
+
543
+ :return: The BulkPlan object
544
+ """
545
+ return ElasticBulkPlan(self.index_list, self.model_class)
546
+
526
547
  @tracer.start_as_current_span(f"{__name__}.commit")
527
548
  def commit(self):
528
549
  """This function should be overloaded to perform a commit of the index data of all the different hosts
@@ -1358,6 +1358,9 @@ class Model:
1358
1358
  if data is None:
1359
1359
  data = {}
1360
1360
 
1361
+ if "__index" in data:
1362
+ data.pop("__index") # Ignore the __index field sometimes generated by as_primitives
1363
+
1361
1364
  if not hasattr(data, "items"):
1362
1365
  raise HowlerTypeError(f"'{self.__class__.__name__}' object must be constructed with dict like")
1363
1366
  self._odm_py_obj = {}
@@ -149,7 +149,15 @@ class Datastore(BaseModel):
149
149
  """
150
150
 
151
151
  hosts: list[Host] = Field(
152
- default=[Host(name="elastic", username="elastic", password="devpass", scheme="http", host="localhost:9200")], # noqa: S106
152
+ default=[
153
+ Host(
154
+ name="elastic",
155
+ username="elastic",
156
+ password="devpass", # noqa: S106
157
+ scheme="http",
158
+ host="localhost:9200",
159
+ )
160
+ ],
153
161
  description="List of hosts used for the datastore",
154
162
  )
155
163
  type: Literal["elasticsearch"] = Field(
@@ -159,6 +167,8 @@ class Datastore(BaseModel):
159
167
  default_factory=ILMConfig,
160
168
  description="Index Lifecycle Management configuration",
161
169
  )
170
+ max_request_size: Optional[int] = Field(description="Maximum request size in bytes", default=100_000_000) # 100MB
171
+ request_batch_size: Optional[int] = Field(description="Default number of operations for bulk requests", default=500)
162
172
 
163
173
 
164
174
  class Logging(BaseModel):
@@ -123,11 +123,11 @@ def save_from_hits(
123
123
  analytics.append(analytic_update)
124
124
 
125
125
  if analytics:
126
- for analytic in analytics[:-1]:
127
- storage.analytic.save(analytic.analytic_id, analytic)
126
+ bulk_plan = storage.analytic.get_bulk_plan()
127
+ for analytic in analytics:
128
+ bulk_plan.add_index_operation(analytic.analytic_id, analytic)
128
129
 
129
- # save the last one passing on the refresh parameter
130
- storage.analytic.save(analytics[-1].analytic_id, analytics[-1], refresh=refresh)
130
+ storage.analytic.bulk(bulk_plan, refresh=refresh)
131
131
 
132
132
 
133
133
  def _get_analytic_updates_from_hit_group(analytic_name: str, hit_group: list[Hit], user: User) -> Analytic | None:
@@ -442,6 +442,31 @@ def create_hit(
442
442
  return datastore().hit.save(id, hit, refresh=refresh)
443
443
 
444
444
 
445
+ @tracer.start_as_current_span(f"{__name__}.create_hits")
446
+ def create_hits(
447
+ hits: list[Hit], user: Optional[str] = None, overwrite: bool = False, refresh: str | None = None
448
+ ) -> bool:
449
+ """Bulk create multiple hits in the database.
450
+
451
+ Similar to create_hit for batch.
452
+ Will raise and abort the entire batch if any hit already exists and overwrite=False.
453
+ """
454
+ storage = datastore()
455
+ bulk_plan = storage.hit.get_bulk_plan()
456
+
457
+ for hit in hits:
458
+ if not overwrite and storage.hit.exists(hit.howler.id):
459
+ raise ResourceExists("Hit %s already exists in datastore" % hit.howler.id)
460
+
461
+ if user:
462
+ hit.howler.log = [Log({"timestamp": "NOW", "explanation": "Created hit", "user": user})]
463
+
464
+ CREATED_HITS.labels(hit.howler.analytic).inc()
465
+ bulk_plan.add_insert_operation(hit.howler.id, hit)
466
+
467
+ return storage.hit.bulk(bulk_plan, refresh=refresh)
468
+
469
+
445
470
  @tracer.start_as_current_span(f"{__name__}.update_hit")
446
471
  def update_hit(
447
472
  hit_id: str,
@@ -476,6 +501,21 @@ def update_hit(
476
501
  return _update_hit(hit_id, operations, user, version=version, refresh=refresh)
477
502
 
478
503
 
504
+ @tracer.start_as_current_span(f"{__name__}.overwrite_hits")
505
+ def overwrite_hits(hits: list[Hit], refresh: str | None = None) -> bool:
506
+ """Bulk save multiple hits to the datastore.
507
+
508
+ Similar to save_hit for batch without versioning. Will overwrite existing hits with the same ID.
509
+ """
510
+ storage = datastore()
511
+ bulk_plan = storage.hit.get_bulk_plan()
512
+
513
+ for hit in hits:
514
+ bulk_plan.add_index_operation(hit.howler.id, hit)
515
+
516
+ return storage.hit.bulk(bulk_plan, refresh=refresh)
517
+
518
+
479
519
  @typing.no_type_check
480
520
  @tracer.start_as_current_span(f"{__name__}.save_hit")
481
521
  def save_hit(hit: Hit, version: Optional[str] = None, refresh: str | None = None) -> tuple[Hit, str]:
@@ -2,3 +2,4 @@ import os
2
2
  import sys
3
3
 
4
4
  TESTING = os.getenv("TESTING", "false").lower() in ["true", "1", "yes"] or "pytest" in sys.modules
5
+ DEBUG_FORCE_REFRESH = os.getenv("DEBUG_FORCE_REFRESH", "false").lower() in ["true", "1", "yes"] or TESTING
@@ -1,3 +1,5 @@
1
+ import re
2
+
1
3
  from howler.common.loader import APP_NAME, DATASTORE_INDEX_PREFIX
2
4
  from howler.datastore.exceptions import SearchException
3
5
 
@@ -53,4 +55,4 @@ def get_logical_index_name(raw_index: str) -> str:
53
55
  raw_index = raw_index.removeprefix(f"{index_prefix}-")
54
56
  break
55
57
 
56
- return raw_index.removesuffix("_hot")
58
+ return re.sub(r"-\d+$", "", raw_index).removesuffix("_hot")
@@ -152,7 +152,7 @@ suppress-none-returning = true
152
152
  [tool.poetry]
153
153
  package-mode = true
154
154
  name = "howler-api"
155
- version = "4.0.0.dev1298"
155
+ version = "4.0.0.dev1308"
156
156
  description = "Howler - API server"
157
157
  authors = [
158
158
  "Canadian Centre for Cyber Security <howler@cyber.gc.ca>",
@@ -255,7 +255,7 @@ typing-extensions = "^4.12.2"
255
255
  types-PyYAML = "6.0.12.20260518"
256
256
  types-paramiko = "3.5.0.20250801"
257
257
  types-pyOpenSSL = "23.3.0.20240106"
258
- types-six = "1.17.0.20260518"
258
+ types-six = "1.17.0.20260721"
259
259
  types-requests = "<2.33.0.20260713"
260
260
  websocket-client = "1.9.0"
261
261
  types-pytz = "^2024.1.0.20240417"
@@ -1,72 +0,0 @@
1
- import json
2
- from copy import deepcopy
3
-
4
-
5
- class ElasticBulkPlan(object):
6
- def __init__(self, indexes, model=None):
7
- self.indexes = indexes
8
- self.model = model
9
- self.operations = []
10
-
11
- @property
12
- def empty(self):
13
- return len(self.operations) == 0
14
-
15
- def add_delete_operation(self, doc_id, index=None):
16
- if index:
17
- self.operations.append(json.dumps({"delete": {"_index": index, "_id": doc_id}}))
18
- else:
19
- for cur_index in self.indexes:
20
- self.operations.append(json.dumps({"delete": {"_index": cur_index, "_id": doc_id}}))
21
-
22
- def add_insert_operation(self, doc_id, doc, index=None):
23
- if isinstance(doc, self.model):
24
- saved_doc = doc.as_primitives(hidden_fields=True) # type: ignore
25
- elif self.model:
26
- saved_doc = self.model(doc).as_primitives(hidden_fields=True) # type: ignore
27
- else:
28
- if not isinstance(doc, dict):
29
- saved_doc = {"__non_doc_raw__": doc}
30
- else:
31
- saved_doc = deepcopy(doc) # type: ignore
32
- saved_doc["id"] = doc_id
33
-
34
- self.operations.append(json.dumps({"create": {"_index": index or self.indexes[0], "_id": doc_id}}))
35
- self.operations.append(json.dumps(saved_doc))
36
-
37
- def add_upsert_operation(self, doc_id, doc, index=None):
38
- if isinstance(doc, self.model):
39
- saved_doc = doc.as_primitives(hidden_fields=True) # type: ignore
40
- elif self.model:
41
- saved_doc = self.model(doc).as_primitives(hidden_fields=True) # type: ignore
42
- else:
43
- if not isinstance(doc, dict):
44
- saved_doc = {"__non_doc_raw__": doc} # type: ignore
45
- else:
46
- saved_doc = deepcopy(doc) # type: ignore
47
- saved_doc["id"] = doc_id
48
-
49
- self.operations.append(json.dumps({"update": {"_index": index or self.indexes[0], "_id": doc_id}}))
50
- self.operations.append(json.dumps({"doc": saved_doc, "doc_as_upsert": True}))
51
-
52
- def add_update_operation(self, doc_id, doc, index=None):
53
- if isinstance(doc, self.model):
54
- saved_doc = doc.as_primitives(hidden_fields=True) # type: ignore
55
- elif self.model:
56
- saved_doc = self.model(doc, mask=list(doc.keys())).as_primitives(hidden_fields=True) # type: ignore
57
- else:
58
- if not isinstance(doc, dict):
59
- saved_doc = {"__non_doc_raw__": doc} # type: ignore
60
- else:
61
- saved_doc = deepcopy(doc) # type: ignore
62
-
63
- if index:
64
- self.operations.append(json.dumps({"update": {"_index": index, "_id": doc_id}}))
65
- self.operations.append(json.dumps({"doc": saved_doc}))
66
- else:
67
- for cur_index in self.indexes:
68
- self.operations.append(json.dumps({"update": {"_index": cur_index, "_id": doc_id}}))
69
- self.operations.append(json.dumps({"doc": saved_doc}))
70
-
71
- def get_plan_data(self):
72
- return "\n".join(self.operations)