howler-api 4.0.0.dev1299__tar.gz → 4.0.0.dev1311__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.dev1299 → howler_api-4.0.0.dev1311}/PKG-INFO +1 -1
  2. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/hit.py +9 -11
  3. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/tool.py +14 -11
  4. howler_api-4.0.0.dev1311/howler/datastore/bulk.py +148 -0
  5. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/datastore/collection.py +113 -22
  6. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/external/reindex_data.py +1 -0
  7. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/base.py +3 -0
  8. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/config.py +11 -1
  9. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/random_data.py +5 -1
  10. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/analytic_service.py +4 -4
  11. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/hit_service.py +40 -0
  12. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/utils/constants.py +1 -0
  13. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/pyproject.toml +2 -2
  14. howler_api-4.0.0.dev1299/howler/datastore/bulk.py +0 -72
  15. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/README.md +0 -0
  16. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/__init__.py +0 -0
  17. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/actions/__init__.py +0 -0
  18. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/actions/add_label.py +0 -0
  19. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/actions/add_to_bundle.py +0 -0
  20. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/actions/add_to_case.py +0 -0
  21. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/actions/change_field.py +0 -0
  22. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/actions/demote.py +0 -0
  23. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/actions/example_plugin.py +0 -0
  24. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/actions/prioritization.py +0 -0
  25. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/actions/promote.py +0 -0
  26. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/actions/remove_from_bundle.py +0 -0
  27. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/actions/remove_label.py +0 -0
  28. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/actions/transition.py +0 -0
  29. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/__init__.py +0 -0
  30. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/base.py +0 -0
  31. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/socket.py +0 -0
  32. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/__init__.py +0 -0
  33. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/action.py +0 -0
  34. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/analytic.py +0 -0
  35. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/auth.py +0 -0
  36. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/clue.py +0 -0
  37. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/configs.py +0 -0
  38. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/dossier.py +0 -0
  39. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/help.py +0 -0
  40. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/notebook.py +0 -0
  41. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/overview.py +0 -0
  42. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/search.py +0 -0
  43. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/template.py +0 -0
  44. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/user.py +0 -0
  45. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/utils/__init__.py +0 -0
  46. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/utils/etag.py +0 -0
  47. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/utils/params.py +0 -0
  48. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v1/view.py +0 -0
  49. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v2/__init__.py +0 -0
  50. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v2/case.py +0 -0
  51. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v2/fuzzy.py +0 -0
  52. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v2/ingest.py +0 -0
  53. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/api/v2/search.py +0 -0
  54. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/app.py +0 -0
  55. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/common/README.md +0 -0
  56. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/common/__init__.py +0 -0
  57. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/common/classification.py +0 -0
  58. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/common/classification.yml +0 -0
  59. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/common/exceptions.py +0 -0
  60. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/common/loader.py +0 -0
  61. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/common/logging/__init__.py +0 -0
  62. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/common/logging/audit.py +0 -0
  63. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/common/logging/format.py +0 -0
  64. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/common/net.py +0 -0
  65. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/common/net_static.py +0 -0
  66. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/common/random_user.py +0 -0
  67. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/common/swagger.py +0 -0
  68. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/config.py +0 -0
  69. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/cronjobs/__init__.py +0 -0
  70. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/cronjobs/action_queue_worker.py +0 -0
  71. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/cronjobs/correlation.py +0 -0
  72. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/cronjobs/retention.py +0 -0
  73. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/cronjobs/view_cleanup.py +0 -0
  74. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/datastore/README.md +0 -0
  75. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/datastore/__init__.py +0 -0
  76. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/datastore/constants.py +0 -0
  77. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/datastore/exceptions.py +0 -0
  78. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/datastore/howler_store.py +0 -0
  79. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/datastore/migrations/fix_process.py +0 -0
  80. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/datastore/operations.py +0 -0
  81. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/datastore/schemas.py +0 -0
  82. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/datastore/store.py +0 -0
  83. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/datastore/support/__init__.py +0 -0
  84. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/datastore/support/build.py +0 -0
  85. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/datastore/support/schemas.py +0 -0
  86. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/datastore/types.py +0 -0
  87. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/error.py +0 -0
  88. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/external/README.md +0 -0
  89. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/external/__init__.py +0 -0
  90. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/external/generate_mitre.py +0 -0
  91. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/external/generate_sigma_rules.py +0 -0
  92. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/external/generate_tlds.py +0 -0
  93. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/external/wipe_databases.py +0 -0
  94. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/gunicorn_config.py +0 -0
  95. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/healthz.py +0 -0
  96. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/helper/__init__.py +0 -0
  97. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/helper/azure.py +0 -0
  98. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/helper/discover.py +0 -0
  99. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/helper/hit.py +0 -0
  100. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/helper/oauth.py +0 -0
  101. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/helper/search.py +0 -0
  102. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/helper/workflow.py +0 -0
  103. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/helper/ws.py +0 -0
  104. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/README.md +0 -0
  105. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/__init__.py +0 -0
  106. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/charter.txt +0 -0
  107. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/constants.py +0 -0
  108. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/helper.py +0 -0
  109. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/howler_enum.py +0 -0
  110. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/mixins.py +0 -0
  111. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/__init__.py +0 -0
  112. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/action.py +0 -0
  113. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/analytic.py +0 -0
  114. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/assemblyline.py +0 -0
  115. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/aws.py +0 -0
  116. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/azure.py +0 -0
  117. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/case.py +0 -0
  118. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/cbs.py +0 -0
  119. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/clue.py +0 -0
  120. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/dossier.py +0 -0
  121. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/__init__.py +0 -0
  122. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/agent.py +0 -0
  123. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/autonomous_system.py +0 -0
  124. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/client.py +0 -0
  125. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/cloud.py +0 -0
  126. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/code_signature.py +0 -0
  127. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/container.py +0 -0
  128. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/dns.py +0 -0
  129. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/egress.py +0 -0
  130. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/elf.py +0 -0
  131. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/email.py +0 -0
  132. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/error.py +0 -0
  133. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/event.py +0 -0
  134. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/faas.py +0 -0
  135. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/file.py +0 -0
  136. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/geo.py +0 -0
  137. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/group.py +0 -0
  138. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/hash.py +0 -0
  139. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/host.py +0 -0
  140. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/http.py +0 -0
  141. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/ingress.py +0 -0
  142. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/interface.py +0 -0
  143. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/network.py +0 -0
  144. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/observer.py +0 -0
  145. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/organization.py +0 -0
  146. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/os.py +0 -0
  147. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/pe.py +0 -0
  148. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/process.py +0 -0
  149. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/registry.py +0 -0
  150. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/related.py +0 -0
  151. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/rule.py +0 -0
  152. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/server.py +0 -0
  153. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/threat.py +0 -0
  154. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/tls.py +0 -0
  155. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/url.py +0 -0
  156. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/user.py +0 -0
  157. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/user_agent.py +0 -0
  158. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/ecs/vulnerability.py +0 -0
  159. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/event.py +0 -0
  160. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/gcp.py +0 -0
  161. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/hit.py +0 -0
  162. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/howler_data.py +0 -0
  163. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/lead.py +0 -0
  164. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/localized_label.py +0 -0
  165. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/overview.py +0 -0
  166. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/pivot.py +0 -0
  167. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/record.py +0 -0
  168. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/template.py +0 -0
  169. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/user.py +0 -0
  170. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/models/view.py +0 -0
  171. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/odm/randomizer.py +0 -0
  172. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/patched.py +0 -0
  173. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/plugins/__init__.py +0 -0
  174. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/plugins/config.py +0 -0
  175. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/remote/__init__.py +0 -0
  176. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/remote/datatypes/README.md +0 -0
  177. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/remote/datatypes/__init__.py +0 -0
  178. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/remote/datatypes/counters.py +0 -0
  179. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/remote/datatypes/events.py +0 -0
  180. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/remote/datatypes/hash.py +0 -0
  181. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/remote/datatypes/lock.py +0 -0
  182. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/remote/datatypes/queues/__init__.py +0 -0
  183. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/remote/datatypes/queues/comms.py +0 -0
  184. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/remote/datatypes/queues/multi.py +0 -0
  185. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/remote/datatypes/queues/named.py +0 -0
  186. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/remote/datatypes/queues/priority.py +0 -0
  187. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/remote/datatypes/set.py +0 -0
  188. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/remote/datatypes/user_quota_tracker.py +0 -0
  189. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/security/__init__.py +0 -0
  190. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/security/socket.py +0 -0
  191. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/security/utils.py +0 -0
  192. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/__init__.py +0 -0
  193. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/action_service.py +0 -0
  194. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/auth_service.py +0 -0
  195. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/bundle_compat_service.py +0 -0
  196. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/case_service.py +0 -0
  197. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/comms_service.py +0 -0
  198. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/config_service.py +0 -0
  199. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/correlation_service.py +0 -0
  200. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/docs_service.py +0 -0
  201. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/dossier_service.py +0 -0
  202. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/event_service.py +0 -0
  203. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/fuzzy_service.py +0 -0
  204. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/jwt_service.py +0 -0
  205. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/lucene_service.py +0 -0
  206. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/notebook_service.py +0 -0
  207. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/overview_service.py +0 -0
  208. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/search_service.py +0 -0
  209. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/template_service.py +0 -0
  210. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/user_service.py +0 -0
  211. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/services/viewer_service.py +0 -0
  212. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/telemetry.py +0 -0
  213. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/utils/__init__.py +0 -0
  214. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/utils/annotations.py +0 -0
  215. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/utils/chunk.py +0 -0
  216. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/utils/compat.py +0 -0
  217. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/utils/dict_utils.py +0 -0
  218. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/utils/indexes.py +0 -0
  219. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/utils/isotime.py +0 -0
  220. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/utils/list_utils.py +0 -0
  221. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/utils/lucene.py +0 -0
  222. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/utils/net_utils.py +0 -0
  223. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/utils/path.py +0 -0
  224. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/utils/socket_utils.py +0 -0
  225. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/howler/utils/str_utils.py +0 -0
  226. {howler_api-4.0.0.dev1299 → howler_api-4.0.0.dev1311}/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.dev1299
3
+ Version: 4.0.0.dev1311
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,
@@ -251,19 +252,50 @@ class ESCollection(Generic[ModelType]):
251
252
 
252
253
  @property
253
254
  def index_list_full(self):
255
+ """Return every physical index for this collection, newest ILM index first.
256
+
257
+ Maintenance operations that must visit historical rollover indexes should use this
258
+ property. The active index is included even when no ILM rollover index exists.
259
+ """
254
260
  if not self._index_list:
255
- self._index_list = list(self.with_retries(self.datastore.client.indices.get, index=f"{self.name}-*").keys())
261
+ self._index_list = self._get_ilm_index_names()
256
262
 
257
- return [self.index_name] + sorted(self._index_list, reverse=True)
263
+ return list(dict.fromkeys([self.index_name, *sorted(self._index_list, reverse=True)]))
258
264
 
259
265
  @property
260
266
  def index_list(self):
261
- """This property contains the list of valid indexes for the current collection.
267
+ """Return the active physical write index for this collection.
262
268
 
263
- :return: list of valid indexes for this collection
269
+ Historical ILM rollover indexes are intentionally excluded. Bulk plans use this
270
+ list for default write, update, and delete targets, and widening it would fan those
271
+ operations out across historical indexes.
264
272
  """
265
273
  return [self.index_name]
266
274
 
275
+ def _get_ilm_index_names(self) -> list[str]:
276
+ """Return physical ILM rollover indexes, excluding temporary maintenance indexes."""
277
+ try:
278
+ indices = self.datastore.client.indices.get(
279
+ index=f"{self.name}-0*",
280
+ ignore_unavailable=True,
281
+ )
282
+ except elasticsearch.exceptions.NotFoundError:
283
+ return []
284
+
285
+ index_pattern = re.compile(rf"^{re.escape(self.name)}-\d{{6}}$")
286
+ return sorted(index for index in indices if index_pattern.fullmatch(index))
287
+
288
+ def _refresh_ilm_index_name(self):
289
+ """Set ``index_name`` to the latest existing ILM index, when one exists."""
290
+ if not self.ilm_config:
291
+ return
292
+
293
+ ilm_indices = self._get_ilm_index_names()
294
+
295
+ if ilm_indices:
296
+ self._index_list = ilm_indices
297
+ self.index_name = ilm_indices[-1]
298
+
267
299
  def scan_with_retry(
268
300
  self,
269
301
  query,
@@ -523,6 +555,26 @@ class ESCollection(Generic[ModelType]):
523
555
  else:
524
556
  updated += res["updated"]
525
557
 
558
+ def bulk(self, operations: ElasticBulkPlan, refresh: str | None = None):
559
+ """
560
+ Execute a bulk plan.
561
+
562
+ :return: True if the operation completed without errors
563
+ """
564
+ responses = []
565
+ for operation_batch in operations.get_plan_batches():
566
+ response = self.with_retries(self.datastore.client.bulk, operations=operation_batch, refresh=refresh)
567
+ responses.append(response)
568
+ return not any(response["errors"] for response in responses)
569
+
570
+ def get_bulk_plan(self):
571
+ """
572
+ Create a BulkPlan tailored for the current datastore
573
+
574
+ :return: The BulkPlan object
575
+ """
576
+ return ElasticBulkPlan(self.index_list, self.model_class)
577
+
526
578
  @tracer.start_as_current_span(f"{__name__}.commit")
527
579
  def commit(self):
528
580
  """This function should be overloaded to perform a commit of the index data of all the different hosts
@@ -709,7 +761,7 @@ class ESCollection(Generic[ModelType]):
709
761
  def reindex(self, allow_failures: bool = False, request_timeout: int = 60):
710
762
  """Reindex all the data of the collection into a freshly mapped index.
711
763
 
712
- For every index in ``self.index_list`` the data is copied into a temporary
764
+ For every physical index in ``self.index_list_full`` the data is copied into a temporary
713
765
  ``__reindex`` index that uses the current mappings/settings. Writes to the source
714
766
  index are blocked while the copy runs so the reindex result and document counts can
715
767
  be validated before the original index is deleted. The temporary index is only
@@ -729,7 +781,8 @@ class ESCollection(Generic[ModelType]):
729
781
  operation can be recovered with :meth:`reindex_cleanup`.
730
782
  """
731
783
  logger.warning("Beginning Reindex")
732
- for index in self.index_list:
784
+ self._refresh_ilm_index_name()
785
+ for index in self.index_list_full:
733
786
  new_name = f"{index}__reindex"
734
787
  index_data = None
735
788
  source_count = None
@@ -777,7 +830,7 @@ class ESCollection(Generic[ModelType]):
777
830
  self.datastore.client.indices.create,
778
831
  index=new_name,
779
832
  mappings=self._get_index_mappings(),
780
- settings=self._get_index_settings(),
833
+ settings=self._get_reindex_settings(index_data),
781
834
  )
782
835
 
783
836
  # Reindex data into target
@@ -826,12 +879,10 @@ class ESCollection(Generic[ModelType]):
826
879
  )
827
880
 
828
881
  alias_actions = []
829
- aliases = index_data.get("aliases", {}) or {self.name: {"is_write_index": True}}
882
+ aliases = index_data.get("aliases", {})
830
883
  for alias, alias_data in aliases.items():
831
884
  alias_action = {"index": index, "alias": alias}
832
885
  alias_action.update(alias_data)
833
- if alias == self.name or alias_data.get("is_write_index", False):
834
- alias_action["is_write_index"] = True
835
886
  alias_actions.append({"add": alias_action})
836
887
  alias_actions.append({"remove_index": {"index": new_name}})
837
888
  self.with_retries(self.datastore.client.indices.update_aliases, actions=alias_actions)
@@ -859,6 +910,14 @@ class ESCollection(Generic[ModelType]):
859
910
 
860
911
  return True
861
912
 
913
+ def _get_reindex_settings(self, index_data: dict) -> dict:
914
+ """Build settings for a reindex target without dropping ILM lifecycle metadata."""
915
+ settings = self._get_index_settings()
916
+ lifecycle = index_data.get("settings", {}).get("index", {}).get("lifecycle")
917
+ if lifecycle:
918
+ settings["index"]["lifecycle"] = deepcopy(lifecycle)
919
+ return settings
920
+
862
921
  def _validate_reindex_result(self, index, new_name, reindex_result, allow_failures):
863
922
  """Validate the result of an Elasticsearch reindex task.
864
923
 
@@ -935,17 +994,18 @@ class ESCollection(Generic[ModelType]):
935
994
  def reindex_cleanup(self):
936
995
  """Recover from a failed or interrupted :meth:`reindex` run.
937
996
 
938
- For every index that still has a leftover ``__reindex`` index, restore the source
939
- index as the writable target and delete the orphaned ``__reindex`` index. If the
940
- source index is missing, raise an error instead of deleting ``__reindex`` because
941
- it may be the only remaining copy of the data.
997
+ For every physical index that still has a leftover ``__reindex`` index, restore the source
998
+ index's original aliases and delete the orphaned ``__reindex`` index. If the source
999
+ index is missing, raise an error instead of deleting ``__reindex`` because it may
1000
+ be the only remaining copy of the data.
942
1001
 
943
1002
  Write blocks set during reindexing are cleared so the collection is usable again.
944
1003
 
945
1004
  :return: ``True`` when cleanup completed on all indexes
946
1005
  """
947
1006
  logger.warning("Beginning reindex cleanup")
948
- for index in self.index_list:
1007
+ self._refresh_ilm_index_name()
1008
+ for index in self.index_list_full:
949
1009
  new_name = f"{index}__reindex"
950
1010
 
951
1011
  if not self.with_retries(self.datastore.client.indices.exists, index=new_name):
@@ -966,17 +1026,13 @@ class ESCollection(Generic[ModelType]):
966
1026
  reindex_aliases = new_index_data.get("aliases", {})
967
1027
 
968
1028
  alias_actions = []
969
- for alias in sorted(set(source_aliases) | set(reindex_aliases) | {self.name}):
1029
+ for alias in sorted(set(source_aliases) | set(reindex_aliases)):
970
1030
  source_alias_data = source_aliases.get(alias, {})
971
1031
  reindex_alias_data = reindex_aliases.get(alias, {})
972
1032
 
973
1033
  add_alias_data = {"index": index, "alias": alias}
974
1034
  add_alias_data.update(source_alias_data)
975
- if (
976
- alias == self.name
977
- or source_alias_data.get("is_write_index", False)
978
- or reindex_alias_data.get("is_write_index", False)
979
- ):
1035
+ if source_alias_data.get("is_write_index", False) or reindex_alias_data.get("is_write_index", False):
980
1036
  add_alias_data["is_write_index"] = True
981
1037
 
982
1038
  alias_actions.append({"add": add_alias_data})
@@ -2884,7 +2940,42 @@ class ESCollection(Generic[ModelType]):
2884
2940
  if existing_ilm_indices:
2885
2941
  # ILM already bootstrapped — ensure the alias exists
2886
2942
  latest = sorted(existing_ilm_indices)[-1]
2887
- if not self.with_retries(self.datastore.client.indices.exists_alias, name=self.name):
2943
+ legacy_hot_index = f"{self.name}_hot"
2944
+ if self.with_retries(self.datastore.client.indices.exists_alias, name=self.name):
2945
+ alias_indices = self.with_retries(self.datastore.client.indices.get_alias, name=self.name)
2946
+ alias_actions = []
2947
+ for alias_index, alias_index_data in alias_indices.items():
2948
+ alias_data = alias_index_data.get("aliases", {}).get(self.name, {})
2949
+ if alias_index == legacy_hot_index:
2950
+ alias_actions.append({"remove": {"index": alias_index, "alias": self.name}})
2951
+ elif alias_index != latest and alias_data.get("is_write_index", False):
2952
+ alias_actions.append(
2953
+ {
2954
+ "add": {
2955
+ "index": alias_index,
2956
+ "alias": self.name,
2957
+ **alias_data,
2958
+ "is_write_index": False,
2959
+ }
2960
+ }
2961
+ )
2962
+
2963
+ latest_alias_data = alias_indices.get(latest, {}).get("aliases", {}).get(self.name, {})
2964
+ if not latest_alias_data.get("is_write_index", False):
2965
+ alias_actions.append(
2966
+ {
2967
+ "add": {
2968
+ "index": latest,
2969
+ "alias": self.name,
2970
+ **latest_alias_data,
2971
+ "is_write_index": True,
2972
+ }
2973
+ }
2974
+ )
2975
+
2976
+ if alias_actions:
2977
+ self.with_retries(self.datastore.client.indices.update_aliases, actions=alias_actions)
2978
+ else:
2888
2979
  # Find the latest index to set as write index
2889
2980
  self.with_retries(
2890
2981
  self.datastore.client.indices.put_alias,
@@ -99,6 +99,7 @@ if __name__ == "__main__":
99
99
 
100
100
  for index_name in selected:
101
101
  collection: ESCollection = getattr(ds, index_name)
102
+ collection._refresh_ilm_index_name()
102
103
 
103
104
  if args.verbose:
104
105
  print(f"Index schema for '{index_name}':")
@@ -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):
@@ -867,8 +867,12 @@ def wipe_actions(ds: HowlerDatastore):
867
867
  def create_dossiers(ds: HowlerDatastore, num_dossiers: int = 5):
868
868
  "Create random dossiers"
869
869
  users = ds.user.search("*:*")["items"]
870
- for _ in range(num_dossiers):
870
+ for index in range(num_dossiers):
871
871
  dossier = generate_useful_dossier(users)
872
+ if index == 0:
873
+ dossier.type = "personal"
874
+ elif index == 1:
875
+ dossier.type = "global"
872
876
  ds.dossier.save(dossier.dossier_id, dossier)
873
877
 
874
878
  ds.dossier.commit()
@@ -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