howler-api 4.0.0.dev978__tar.gz → 4.0.0.dev993__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 (203) hide show
  1. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/PKG-INFO +1 -1
  2. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/actions/demote.py +2 -1
  3. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/actions/promote.py +6 -1
  4. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/v1/search.py +0 -29
  5. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/v1/tool.py +42 -15
  6. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/datastore/collection.py +250 -19
  7. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/datastore/howler_store.py +2 -1
  8. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/datastore/store.py +12 -3
  9. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/helper/hit.py +10 -1
  10. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/helper.py +1 -0
  11. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/config.py +50 -0
  12. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/howler_data.py +6 -0
  13. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/random_data.py +1 -0
  14. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/services/hit_service.py +1 -1
  15. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/pyproject.toml +1 -1
  16. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/README.md +0 -0
  17. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/__init__.py +0 -0
  18. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/actions/__init__.py +0 -0
  19. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/actions/add_label.py +0 -0
  20. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/actions/add_to_bundle.py +0 -0
  21. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/actions/change_field.py +0 -0
  22. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/actions/example_plugin.py +0 -0
  23. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/actions/prioritization.py +0 -0
  24. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/actions/remove_from_bundle.py +0 -0
  25. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/actions/remove_label.py +0 -0
  26. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/actions/transition.py +0 -0
  27. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/__init__.py +0 -0
  28. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/base.py +0 -0
  29. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/socket.py +0 -0
  30. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/v1/__init__.py +0 -0
  31. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/v1/action.py +0 -0
  32. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/v1/analytic.py +0 -0
  33. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/v1/auth.py +0 -0
  34. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/v1/clue.py +0 -0
  35. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/v1/configs.py +0 -0
  36. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/v1/dossier.py +0 -0
  37. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/v1/help.py +0 -0
  38. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/v1/hit.py +0 -0
  39. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/v1/notebook.py +0 -0
  40. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/v1/overview.py +0 -0
  41. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/v1/template.py +0 -0
  42. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/v1/user.py +0 -0
  43. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/v1/utils/__init__.py +0 -0
  44. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/v1/utils/etag.py +0 -0
  45. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/api/v1/view.py +0 -0
  46. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/app.py +0 -0
  47. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/common/README.md +0 -0
  48. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/common/__init__.py +0 -0
  49. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/common/classification.py +0 -0
  50. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/common/classification.yml +0 -0
  51. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/common/exceptions.py +0 -0
  52. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/common/loader.py +0 -0
  53. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/common/logging/__init__.py +0 -0
  54. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/common/logging/audit.py +0 -0
  55. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/common/logging/format.py +0 -0
  56. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/common/net.py +0 -0
  57. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/common/net_static.py +0 -0
  58. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/common/random_user.py +0 -0
  59. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/common/swagger.py +0 -0
  60. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/config.py +0 -0
  61. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/cronjobs/__init__.py +0 -0
  62. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/cronjobs/action_queue_worker.py +0 -0
  63. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/cronjobs/retention.py +0 -0
  64. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/cronjobs/rules.py +0 -0
  65. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/cronjobs/view_cleanup.py +0 -0
  66. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/datastore/README.md +0 -0
  67. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/datastore/__init__.py +0 -0
  68. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/datastore/bulk.py +0 -0
  69. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/datastore/constants.py +0 -0
  70. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/datastore/exceptions.py +0 -0
  71. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/datastore/migrations/fix_process.py +0 -0
  72. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/datastore/operations.py +0 -0
  73. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/datastore/schemas.py +0 -0
  74. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/datastore/support/__init__.py +0 -0
  75. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/datastore/support/build.py +0 -0
  76. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/datastore/support/schemas.py +0 -0
  77. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/datastore/types.py +0 -0
  78. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/error.py +0 -0
  79. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/external/README.md +0 -0
  80. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/external/__init__.py +0 -0
  81. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/external/generate_mitre.py +0 -0
  82. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/external/generate_sigma_rules.py +0 -0
  83. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/external/generate_tlds.py +0 -0
  84. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/external/reindex_data.py +0 -0
  85. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/external/wipe_databases.py +0 -0
  86. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/gunicorn_config.py +0 -0
  87. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/healthz.py +0 -0
  88. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/helper/__init__.py +0 -0
  89. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/helper/azure.py +0 -0
  90. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/helper/discover.py +0 -0
  91. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/helper/oauth.py +0 -0
  92. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/helper/search.py +0 -0
  93. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/helper/workflow.py +0 -0
  94. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/helper/ws.py +0 -0
  95. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/README.md +0 -0
  96. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/__init__.py +0 -0
  97. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/base.py +0 -0
  98. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/charter.txt +0 -0
  99. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/howler_enum.py +0 -0
  100. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/__init__.py +0 -0
  101. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/action.py +0 -0
  102. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/analytic.py +0 -0
  103. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/assemblyline.py +0 -0
  104. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/aws.py +0 -0
  105. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/azure.py +0 -0
  106. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/cbs.py +0 -0
  107. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/clue.py +0 -0
  108. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/dossier.py +0 -0
  109. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/__init__.py +0 -0
  110. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/agent.py +0 -0
  111. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/autonomous_system.py +0 -0
  112. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/client.py +0 -0
  113. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/cloud.py +0 -0
  114. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/code_signature.py +0 -0
  115. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/container.py +0 -0
  116. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/dns.py +0 -0
  117. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/egress.py +0 -0
  118. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/elf.py +0 -0
  119. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/email.py +0 -0
  120. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/error.py +0 -0
  121. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/event.py +0 -0
  122. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/faas.py +0 -0
  123. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/file.py +0 -0
  124. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/geo.py +0 -0
  125. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/group.py +0 -0
  126. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/hash.py +0 -0
  127. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/host.py +0 -0
  128. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/http.py +0 -0
  129. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/ingress.py +0 -0
  130. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/interface.py +0 -0
  131. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/network.py +0 -0
  132. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/observer.py +0 -0
  133. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/organization.py +0 -0
  134. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/os.py +0 -0
  135. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/pe.py +0 -0
  136. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/process.py +0 -0
  137. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/registry.py +0 -0
  138. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/related.py +0 -0
  139. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/rule.py +0 -0
  140. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/server.py +0 -0
  141. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/threat.py +0 -0
  142. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/tls.py +0 -0
  143. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/url.py +0 -0
  144. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/user.py +0 -0
  145. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/user_agent.py +0 -0
  146. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/ecs/vulnerability.py +0 -0
  147. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/gcp.py +0 -0
  148. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/hit.py +0 -0
  149. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/lead.py +0 -0
  150. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/localized_label.py +0 -0
  151. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/overview.py +0 -0
  152. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/pivot.py +0 -0
  153. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/template.py +0 -0
  154. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/user.py +0 -0
  155. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/models/view.py +0 -0
  156. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/odm/randomizer.py +0 -0
  157. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/patched.py +0 -0
  158. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/plugins/__init__.py +0 -0
  159. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/plugins/config.py +0 -0
  160. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/remote/__init__.py +0 -0
  161. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/remote/datatypes/README.md +0 -0
  162. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/remote/datatypes/__init__.py +0 -0
  163. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/remote/datatypes/counters.py +0 -0
  164. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/remote/datatypes/events.py +0 -0
  165. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/remote/datatypes/hash.py +0 -0
  166. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/remote/datatypes/lock.py +0 -0
  167. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/remote/datatypes/queues/__init__.py +0 -0
  168. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/remote/datatypes/queues/comms.py +0 -0
  169. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/remote/datatypes/queues/multi.py +0 -0
  170. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/remote/datatypes/queues/named.py +0 -0
  171. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/remote/datatypes/queues/priority.py +0 -0
  172. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/remote/datatypes/set.py +0 -0
  173. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/remote/datatypes/user_quota_tracker.py +0 -0
  174. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/security/__init__.py +0 -0
  175. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/security/socket.py +0 -0
  176. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/security/utils.py +0 -0
  177. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/services/__init__.py +0 -0
  178. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/services/action_service.py +0 -0
  179. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/services/analytic_service.py +0 -0
  180. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/services/auth_service.py +0 -0
  181. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/services/config_service.py +0 -0
  182. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/services/dossier_service.py +0 -0
  183. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/services/event_service.py +0 -0
  184. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/services/jwt_service.py +0 -0
  185. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/services/lucene_service.py +0 -0
  186. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/services/notebook_service.py +0 -0
  187. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/services/overview_service.py +0 -0
  188. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/services/template_service.py +0 -0
  189. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/services/user_service.py +0 -0
  190. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/telemetry.py +0 -0
  191. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/utils/__init__.py +0 -0
  192. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/utils/annotations.py +0 -0
  193. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/utils/chunk.py +0 -0
  194. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/utils/compat.py +0 -0
  195. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/utils/constants.py +0 -0
  196. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/utils/dict_utils.py +0 -0
  197. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/utils/isotime.py +0 -0
  198. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/utils/list_utils.py +0 -0
  199. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/utils/lucene.py +0 -0
  200. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/utils/path.py +0 -0
  201. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/utils/socket_utils.py +0 -0
  202. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/howler/utils/str_utils.py +0 -0
  203. {howler_api-4.0.0.dev978 → howler_api-4.0.0.dev993}/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.dev978
3
+ Version: 4.0.0.dev993
4
4
  Summary: Howler - API server
5
5
  License: MIT
6
6
  Keywords: howler,alerting,gc,canada,cse-cst,cse,cst,cyber,cccs
@@ -76,6 +76,7 @@ def execute(
76
76
  *hit_helper.demote_hit(escalation=escalation),
77
77
  odm_helper.update("howler.assessment", None),
78
78
  odm_helper.update("howler.rationale", None),
79
+ odm_helper.update("howler.assignment", None),
79
80
  ],
80
81
  )
81
82
  else:
@@ -93,7 +94,7 @@ def execute(
93
94
  ds.hit.update_by_query(
94
95
  query,
95
96
  [
96
- *hit_helper.assess_hit(assessment, rationale),
97
+ *hit_helper.assess_hit(assessment, rationale, user=(user if user else "automation")),
97
98
  odm_helper.update(
98
99
  "howler.assignment",
99
100
  user.get("uname", "automation") if user else "automation",
@@ -10,6 +10,7 @@ from howler.odm.models.howler_data import (
10
10
  AssessmentEscalationMap,
11
11
  Escalation,
12
12
  )
13
+ from howler.odm.models.user import User
13
14
  from howler.utils.str_utils import sanitize_lucene_query
14
15
 
15
16
  OPERATION_ID = "promote"
@@ -26,6 +27,7 @@ def execute(
26
27
  escalation: Escalation = Escalation.ALERT,
27
28
  assessment: Optional[str] = None,
28
29
  rationale: Optional[str] = None,
30
+ user: Optional[User] = None,
29
31
  **kwargs,
30
32
  ):
31
33
  """Promote a hit.
@@ -73,6 +75,7 @@ def execute(
73
75
  *hit_helper.promote_hit(escalation=escalation),
74
76
  odm_helper.update("howler.assessment", None),
75
77
  odm_helper.update("howler.rationale", None),
78
+ odm_helper.update("howler.assignment", None),
76
79
  ],
77
80
  )
78
81
  else:
@@ -87,7 +90,9 @@ def execute(
87
90
  )
88
91
  return report
89
92
 
90
- ds.hit.update_by_query(query, hit_helper.assess_hit(assessment, rationale))
93
+ ds.hit.update_by_query(
94
+ query, hit_helper.assess_hit(assessment, rationale, user=(user if user else "automation"))
95
+ )
91
96
 
92
97
  report.append(
93
98
  {
@@ -78,7 +78,6 @@ def search(index, **kwargs):
78
78
  sort => How to sort the results (not available in deep paging)
79
79
  fl => List of fields to return
80
80
  timeout => Maximum execution time (ms)
81
- use_archive => Allow access to the datastore achive (Default: False)
82
81
  track_total_hits => Track the total number of query matches, instead of stopping at 10000 (Default: False)
83
82
  metadata => A list of additional features to be added to the result alongside the raw results
84
83
 
@@ -118,18 +117,9 @@ def search(index, **kwargs):
118
117
  "track_total_hits",
119
118
  ]
120
119
  multi_fields = ["filters", "metadata"]
121
- boolean_fields = ["use_archive"]
122
120
 
123
121
  params, req_data = generate_params(request, fields, multi_fields)
124
122
 
125
- params.update(
126
- {
127
- k: str(req_data.get(k, "false")).lower() in ["true", ""]
128
- for k in boolean_fields
129
- if req_data.get(k, None) is not None
130
- }
131
- )
132
-
133
123
  if has_access_control(index):
134
124
  params.update({"access_control": user["access_control"]})
135
125
 
@@ -350,18 +340,9 @@ def sigma_search(index, **kwargs):
350
340
  "track_total_hits",
351
341
  ]
352
342
  multi_fields = ["filters"]
353
- boolean_fields = ["use_archive"]
354
343
 
355
344
  params, req_data = generate_params(request, fields, multi_fields)
356
345
 
357
- params.update(
358
- {
359
- k: str(req_data.get(k, "false")).lower() in ["true", ""]
360
- for k in boolean_fields
361
- if req_data.get(k, None) is not None
362
- }
363
- )
364
-
365
346
  if has_access_control(index):
366
347
  params.update({"access_control": user["access_control"]})
367
348
 
@@ -520,7 +501,6 @@ def count(index, **kwargs):
520
501
  Optional Arguments:
521
502
  filters => List of additional filter queries limit the data
522
503
  timeout => Maximum execution time (ms)
523
- use_archive => Allow access to the datastore achive (Default: False)
524
504
 
525
505
  Data Block:
526
506
  # Note that the data block is for POST requests only!
@@ -543,15 +523,6 @@ def count(index, **kwargs):
543
523
 
544
524
  params, req_data = generate_params(request, [], [])
545
525
 
546
- boolean_fields = ["use_archive"]
547
- params.update(
548
- {
549
- k: str(req_data.get(k, "false")).lower() in ["true", ""]
550
- for k in boolean_fields
551
- if req_data.get(k, None) is not None
552
- }
553
- )
554
-
555
526
  if has_access_control(index):
556
527
  params.update({"access_control": user["access_control"]})
557
528
 
@@ -59,6 +59,9 @@ def create_one_or_many_hits(tool_name: str, user: User, **kwargs): # noqa: C901
59
59
  {'id': None, 'error': "Error message"},
60
60
  ]
61
61
  }
62
+
63
+ .. deprecated::
64
+ Use POST /api/v1/hit/ directly with pre-mapped hit data instead.
62
65
  """
63
66
  data = request.json
64
67
  if not isinstance(data, dict):
@@ -74,7 +77,10 @@ def create_one_or_many_hits(tool_name: str, user: User, **kwargs): # noqa: C901
74
77
 
75
78
  if not isinstance(hits, list):
76
79
  return bad_request(err="Invalid: 'hits' field is missing or invalid.")
77
- warnings = []
80
+ warnings = [
81
+ "This endpoint is deprecated and will be removed in a future version. "
82
+ "Use POST /api/v1/hit/ directly with pre-mapped hit data instead."
83
+ ]
78
84
  # Validate field_map targets
79
85
  hit_fields = Hit.flat_fields()
80
86
  for targets in field_map.values():
@@ -157,27 +163,48 @@ def create_one_or_many_hits(tool_name: str, user: User, **kwargs): # noqa: C901
157
163
  logger.warning(e)
158
164
 
159
165
  out.append({"id": None, "error": str(e)})
166
+
167
+ # Deduplicate by hash: skip hits whose hash already exists in the datastore
168
+ if odms:
169
+ hashes = [odm.howler.hash for odm in odms]
170
+ existing_hashes: dict[str, int] = datastore().hit.facet(
171
+ "howler.hash",
172
+ query=f"howler.hash:({' OR '.join(hashes)})",
173
+ rows=len(hashes),
174
+ )
175
+
176
+ deduplicated_odms = []
177
+ for odm in odms:
178
+ if odm.howler.hash in existing_hashes:
179
+ logger.warning("Hit with hash %s already exists in the DB, skipping", odm.howler.hash)
180
+ warnings.append(f"Hit with hash {odm.howler.hash} already exists in the DB and was skipped.")
181
+ out[:] = [entry for entry in out if entry["id"] != odm.howler.id]
182
+ else:
183
+ deduplicated_odms.append(odm)
184
+
185
+ odms = deduplicated_odms
186
+
160
187
  # If there are any errors...
161
188
  if any([obj["error"] for obj in out]):
162
189
  return bad_request(out, warnings=warnings, err="No valid hits were provided")
163
- else:
164
- for odm in odms:
165
- if bundle_hit is not None:
166
- bundle_hit.howler.hits.append(odm.howler.id)
167
- bundle_hit.howler.bundle_size += 1
168
- odm.howler.bundles.append(bundle_hit.howler.id)
169
190
 
170
- hit_service.create_hit(odm.howler.id, odm, user=user.uname)
191
+ for odm in odms:
192
+ if bundle_hit is not None:
193
+ bundle_hit.howler.hits.append(odm.howler.id)
194
+ bundle_hit.howler.bundle_size += 1
195
+ odm.howler.bundles.append(bundle_hit.howler.id)
196
+
197
+ hit_service.create_hit(odm.howler.id, odm, user=user.uname)
171
198
 
172
- analytic_service.save_from_hit(odm, user)
199
+ analytic_service.save_from_hit(odm, user)
173
200
 
174
- if bundle_hit:
175
- hit_service.create_hit(bundle_hit.howler.id, bundle_hit, user=user.uname)
201
+ if bundle_hit:
202
+ hit_service.create_hit(bundle_hit.howler.id, bundle_hit, user=user.uname)
176
203
 
177
- analytic_service.save_from_hit(bundle_hit, user)
204
+ analytic_service.save_from_hit(bundle_hit, user)
178
205
 
179
- datastore().hit.commit()
206
+ datastore().hit.commit()
180
207
 
181
- action_service.enqueue_action_execution([entry["id"] for entry in out], trigger="create", user=user)
208
+ action_service.enqueue_action_execution([entry["id"] for entry in out], trigger="create", user=user)
182
209
 
183
- return created(out, warnings=warnings)
210
+ return created(out, warnings=warnings)
@@ -217,7 +217,7 @@ class ESCollection(Generic[ModelType]):
217
217
  ENSURE_COLLECTION_WARNED: bool = False
218
218
  CUSTOM_AGG_PREFIX: str = "_custom_agg__"
219
219
 
220
- def __init__(self, datastore: ESStore, name, model_class=None, validate=True, max_attempts=10):
220
+ def __init__(self, datastore: ESStore, name, model_class=None, validate=True, max_attempts=10, ilm_config=None):
221
221
  self.replicas = int(
222
222
  environ.get(
223
223
  f"ELASTIC_{name.upper()}_REPLICAS",
@@ -229,6 +229,7 @@ class ESCollection(Generic[ModelType]):
229
229
 
230
230
  self.datastore = datastore
231
231
  self.name = f"{APP_NAME}-{name}"
232
+ self.ilm_config = ilm_config
232
233
  self.index_name = f"{self.name}_hot"
233
234
  self.model_class = model_class
234
235
  self.validate = validate
@@ -1627,7 +1628,7 @@ class ESCollection(Generic[ModelType]):
1627
1628
 
1628
1629
  return prune(source_data, fields, self.stored_fields, mapping_class=Mapping)
1629
1630
 
1630
- def _search(self, args=None, deep_paging_id=None, use_archive=False, track_total_hits=None):
1631
+ def _search(self, args=None, deep_paging_id=None, track_total_hits=None):
1631
1632
  if args is None:
1632
1633
  args = []
1633
1634
 
@@ -1804,7 +1805,6 @@ class ESCollection(Generic[ModelType]):
1804
1805
  filters: list[str] | str | None = None,
1805
1806
  access_control: typing.Any = None,
1806
1807
  deep_paging_id: str | None = None,
1807
- use_archive: bool = False,
1808
1808
  track_total_hits: bool = False,
1809
1809
  script_fields: list[str] = [],
1810
1810
  *,
@@ -1824,7 +1824,6 @@ class ESCollection(Generic[ModelType]):
1824
1824
  filters: list[str] | str | None = None,
1825
1825
  access_control: typing.Any = None,
1826
1826
  deep_paging_id: str | None = None,
1827
- use_archive: bool = False,
1828
1827
  track_total_hits: bool = False,
1829
1828
  script_fields: list[str] = [],
1830
1829
  *,
@@ -1844,7 +1843,6 @@ class ESCollection(Generic[ModelType]):
1844
1843
  filters: list[str] | str | None = None,
1845
1844
  access_control: typing.Any = None,
1846
1845
  deep_paging_id: str | None = None,
1847
- use_archive: bool = False,
1848
1846
  track_total_hits: bool = False,
1849
1847
  script_fields: list[str] = [],
1850
1848
  *,
@@ -1864,7 +1862,6 @@ class ESCollection(Generic[ModelType]):
1864
1862
  filters: list[str] | str | None = None,
1865
1863
  access_control: typing.Any = None,
1866
1864
  deep_paging_id: str | None = None,
1867
- use_archive: bool = False,
1868
1865
  track_total_hits: bool = False,
1869
1866
  script_fields: list[str] = [],
1870
1867
  *,
@@ -1883,7 +1880,6 @@ class ESCollection(Generic[ModelType]):
1883
1880
  filters=None,
1884
1881
  access_control=None,
1885
1882
  deep_paging_id=None,
1886
- use_archive=False,
1887
1883
  track_total_hits=None,
1888
1884
  script_fields=[],
1889
1885
  *,
@@ -1914,7 +1910,6 @@ class ESCollection(Generic[ModelType]):
1914
1910
 
1915
1911
  :param script_fields: List of name/script tuple of fields to be evaluated at runtime
1916
1912
  :param track_total_hits: Return to total matching document count
1917
- :param use_archive: Query also the archive
1918
1913
  :param deep_paging_id: ID of the next page during deep paging searches
1919
1914
  :param as_obj: Return objects instead of dictionaries
1920
1915
  :param query: lucene query to search for
@@ -1976,7 +1971,6 @@ class ESCollection(Generic[ModelType]):
1976
1971
  result = self._search(
1977
1972
  args,
1978
1973
  deep_paging_id=deep_paging_id,
1979
- use_archive=use_archive,
1980
1974
  track_total_hits=track_total_hits,
1981
1975
  )
1982
1976
 
@@ -2034,7 +2028,6 @@ class ESCollection(Generic[ModelType]):
2034
2028
  access_control=None,
2035
2029
  item_buffer_size=200,
2036
2030
  as_obj=True,
2037
- use_archive=False,
2038
2031
  ):
2039
2032
  """This function should perform a search through the datastore and stream
2040
2033
  all related results as a dictionary of key value pair where each keys
@@ -2047,7 +2040,6 @@ class ESCollection(Generic[ModelType]):
2047
2040
  >>> fl[x]: value
2048
2041
  >>> }
2049
2042
 
2050
- :param use_archive: Query also the archive
2051
2043
  :param as_obj: Return objects instead of dictionaries
2052
2044
  :param query: lucene query to search for
2053
2045
  :param fl: list of fields to return from the search
@@ -2274,7 +2266,6 @@ class ESCollection(Generic[ModelType]):
2274
2266
  mincount=None,
2275
2267
  filters=None,
2276
2268
  access_control=None,
2277
- use_archive=False,
2278
2269
  ):
2279
2270
  type_modifier = self._validate_steps_count(start, end, gap)
2280
2271
  start = type_modifier(start)
@@ -2313,7 +2304,7 @@ class ESCollection(Generic[ModelType]):
2313
2304
  if filters:
2314
2305
  args.append(("filters", filters))
2315
2306
 
2316
- result = self._search(args, use_archive=use_archive)
2307
+ result = self._search(args)
2317
2308
 
2318
2309
  # Convert the histogram into a dictionary
2319
2310
  return {
@@ -2333,7 +2324,6 @@ class ESCollection(Generic[ModelType]):
2333
2324
  mincount=None,
2334
2325
  filters=None,
2335
2326
  access_control=None,
2336
- use_archive=False,
2337
2327
  field_script=None,
2338
2328
  ):
2339
2329
  if not query:
@@ -2366,7 +2356,7 @@ class ESCollection(Generic[ModelType]):
2366
2356
  if field_script:
2367
2357
  args.append(("field_script", field_script))
2368
2358
 
2369
- result = self._search(args, use_archive=use_archive)
2359
+ result = self._search(args)
2370
2360
 
2371
2361
  # Convert the histogram into a dictionary
2372
2362
  return {
@@ -2379,7 +2369,6 @@ class ESCollection(Generic[ModelType]):
2379
2369
  query="id:*",
2380
2370
  filters=None,
2381
2371
  access_control=None,
2382
- use_archive=False,
2383
2372
  field_script=None,
2384
2373
  ):
2385
2374
  if filters is None:
@@ -2403,7 +2392,7 @@ class ESCollection(Generic[ModelType]):
2403
2392
  if field_script:
2404
2393
  args.append(("field_script", field_script))
2405
2394
 
2406
- result = self._search(args, use_archive=use_archive)
2395
+ result = self._search(args)
2407
2396
  return result["aggregations"][f"{field}_stats"]
2408
2397
 
2409
2398
  def grouped_search(
@@ -2419,7 +2408,6 @@ class ESCollection(Generic[ModelType]):
2419
2408
  filters=None,
2420
2409
  access_control=None,
2421
2410
  as_obj=True,
2422
- use_archive=False,
2423
2411
  track_total_hits=False,
2424
2412
  ):
2425
2413
  if rows is None:
@@ -2462,7 +2450,7 @@ class ESCollection(Generic[ModelType]):
2462
2450
  if filters:
2463
2451
  args.append(("filters", filters))
2464
2452
 
2465
- result = self._search(args, use_archive=use_archive, track_total_hits=track_total_hits)
2453
+ result = self._search(args, track_total_hits=track_total_hits)
2466
2454
 
2467
2455
  return {
2468
2456
  "offset": offset,
@@ -2598,6 +2586,83 @@ class ESCollection(Generic[ModelType]):
2598
2586
  else:
2599
2587
  return True
2600
2588
 
2589
+ def _create_ilm_policy(self, ilm_config):
2590
+ """Create or update the ILM policy for this collection.
2591
+
2592
+ Builds an ILM policy with hot (rollover), optional warm (forcemerge),
2593
+ and optional cold phases. No delete phase — retention is handled by
2594
+ the retention cronjob.
2595
+
2596
+ The ``ilm_config`` parameter (global :class:`ILMConfig`) is used **only**
2597
+ for the hot phase rollover settings (``rollover_max_age`` and
2598
+ ``rollover_max_size``). Warm and cold phase configuration is sourced
2599
+ exclusively from ``self.ilm_config`` (the per-index :class:`ILMIndexConfig`).
2600
+
2601
+ :param ilm_config: The global ILMConfig with rollover settings (hot phase only).
2602
+ """
2603
+ phases: dict[str, Any] = {
2604
+ "hot": {
2605
+ "min_age": "0ms",
2606
+ "actions": {
2607
+ "rollover": {
2608
+ "max_age": ilm_config.rollover_max_age,
2609
+ "max_primary_shard_size": ilm_config.rollover_max_size,
2610
+ }
2611
+ },
2612
+ }
2613
+ }
2614
+
2615
+ if self.ilm_config and self.ilm_config.warm:
2616
+ warm_actions: dict[str, Any] = {}
2617
+ if self.ilm_config.warm_forcemerge_segments is not None:
2618
+ warm_actions["forcemerge"] = {"max_num_segments": self.ilm_config.warm_forcemerge_segments}
2619
+ phases["warm"] = {
2620
+ "min_age": self.ilm_config.warm,
2621
+ "actions": warm_actions,
2622
+ }
2623
+
2624
+ if self.ilm_config and self.ilm_config.cold:
2625
+ # Note: forcemerge is NOT allowed in the cold phase by ES.
2626
+ # Cold phase is typically just for storage tier allocation.
2627
+ phases["cold"] = {
2628
+ "min_age": self.ilm_config.cold,
2629
+ "actions": {},
2630
+ }
2631
+
2632
+ policy = {"phases": phases}
2633
+
2634
+ self.with_retries(
2635
+ self.datastore.client.ilm.put_lifecycle,
2636
+ name=f"{self.name}_policy",
2637
+ policy=policy,
2638
+ )
2639
+ logger.info("ILM policy %s_policy created/updated", self.name)
2640
+
2641
+ def _create_index_template(self, ilm_config):
2642
+ """Create or update a composable index template for ILM-managed rollover.
2643
+
2644
+ The template matches '{name}-*' and includes the full ODM mappings
2645
+ so that rollover indices inherit the correct schema.
2646
+
2647
+ :param ilm_config: The global ILMConfig (unused directly but kept for symmetry).
2648
+ """
2649
+ settings = self._get_index_settings()
2650
+ settings["index"]["lifecycle.name"] = f"{self.name}_policy"
2651
+ settings["index"]["lifecycle.rollover_alias"] = self.name
2652
+
2653
+ mappings = self._get_index_mappings()
2654
+
2655
+ self.with_retries(
2656
+ self.datastore.client.indices.put_index_template,
2657
+ name=f"{self.name}_template",
2658
+ index_patterns=[f"{self.name}-*"],
2659
+ template={
2660
+ "settings": settings,
2661
+ "mappings": mappings,
2662
+ },
2663
+ )
2664
+ logger.info("Index template %s_template created/updated", self.name)
2665
+
2601
2666
  def _get_index_settings(self) -> dict:
2602
2667
  default_stub: dict = deepcopy(default_index)
2603
2668
  settings: dict = default_stub.pop("settings", {})
@@ -2714,8 +2779,15 @@ class ESCollection(Generic[ModelType]):
2714
2779
  """This function should test if the collection that you are trying to access does indeed exist
2715
2780
  and should create it if it does not.
2716
2781
 
2782
+ When ILM is configured for this collection, it sets up the ILM policy,
2783
+ composable index template, and bootstraps a rollover alias instead of
2784
+ using the legacy _hot index naming.
2785
+
2717
2786
  :return:
2718
2787
  """
2788
+ if self.ilm_config:
2789
+ return self._ensure_collection_ilm()
2790
+
2719
2791
  # Create HOT index
2720
2792
  if not self.with_retries(self.datastore.client.indices.exists, index=self.name):
2721
2793
  logger.debug("Index %s does not exist. Creating it now...", self.name.upper())
@@ -2758,6 +2830,158 @@ class ESCollection(Generic[ModelType]):
2758
2830
 
2759
2831
  self._check_fields()
2760
2832
 
2833
+ def _ensure_collection_ilm(self):
2834
+ """Bootstrap an ILM-managed collection with rollover alias.
2835
+
2836
+ 1. Create/update the ILM policy and composable index template.
2837
+ 2. Bootstrap the initial index if needed:
2838
+ - If ILM indices already exist (pattern {name}-0*), skip.
2839
+ - If a legacy _hot index exists, migrate it to {name}-000001.
2840
+ - Otherwise, create {name}-000001 from scratch.
2841
+ """
2842
+ from howler.odm.models.config import config as _config
2843
+
2844
+ ilm_global = _config.datastore.ilm
2845
+
2846
+ # Idempotent: create/update ILM policy and index template
2847
+ self._create_ilm_policy(ilm_global)
2848
+ self._create_index_template(ilm_global)
2849
+
2850
+ ilm_initial_index = f"{self.name}-000001"
2851
+
2852
+ # Check if any ILM-managed index already exists
2853
+ existing_ilm_indices = list(
2854
+ self.with_retries(
2855
+ self.datastore.client.indices.get, index=f"{self.name}-0*", ignore_unavailable=True
2856
+ ).keys()
2857
+ )
2858
+
2859
+ if existing_ilm_indices:
2860
+ # ILM already bootstrapped — ensure the alias exists
2861
+ latest = sorted(existing_ilm_indices)[-1]
2862
+ if not self.with_retries(self.datastore.client.indices.exists_alias, name=self.name):
2863
+ # Find the latest index to set as write index
2864
+ self.with_retries(
2865
+ self.datastore.client.indices.put_alias,
2866
+ index=latest,
2867
+ name=self.name,
2868
+ is_write_index=True,
2869
+ )
2870
+
2871
+ self.index_name = latest
2872
+ logger.debug("ILM collection %s already bootstrapped", self.name.upper())
2873
+ elif self.with_retries(self.datastore.client.indices.exists, index=self.index_name):
2874
+ # Legacy _hot index exists — migrate to ILM
2875
+ logger.info("Migrating %s from legacy _hot index to ILM-managed rollover", self.name.upper())
2876
+
2877
+ # Block writes on the old index
2878
+ self.with_retries(
2879
+ self.datastore.client.indices.put_settings,
2880
+ index=self.index_name,
2881
+ settings=write_block_settings,
2882
+ )
2883
+
2884
+ # Everything after write-block must be wrapped in try/except to ensure
2885
+ # we unblock writes if migration fails — otherwise ingestion is stuck.
2886
+ try:
2887
+ # Clone the _hot index to the new ILM initial index
2888
+ self._safe_index_copy(self.datastore.client.indices.clone, self.index_name, ilm_initial_index)
2889
+
2890
+ # Apply ILM settings to the new index
2891
+ self.with_retries(
2892
+ self.datastore.client.indices.put_settings,
2893
+ index=ilm_initial_index,
2894
+ settings={
2895
+ "index.lifecycle.name": f"{self.name}_policy",
2896
+ "index.lifecycle.rollover_alias": self.name,
2897
+ "index.blocks.write": None,
2898
+ },
2899
+ )
2900
+
2901
+ # Swap alias: remove old _hot, add new ILM index as write index
2902
+ actions = [
2903
+ {"add": {"index": ilm_initial_index, "alias": self.name, "is_write_index": True}},
2904
+ ]
2905
+
2906
+ # Remove old alias if it points to _hot
2907
+ if self.with_retries(self.datastore.client.indices.exists_alias, index=self.index_name, name=self.name):
2908
+ actions.append({"remove": {"index": self.index_name, "alias": self.name}})
2909
+
2910
+ self.with_retries(self.datastore.client.indices.update_aliases, actions=actions)
2911
+
2912
+ except Exception:
2913
+ # Migration failed — rollback to restore ingestion to the old index
2914
+ logger.exception(
2915
+ "Migration of %s to ILM failed. Rolling back write-block on %s.",
2916
+ self.name.upper(),
2917
+ self.index_name,
2918
+ )
2919
+
2920
+ # Unblock writes on the old index so ingestion can resume
2921
+ try:
2922
+ self.with_retries(
2923
+ self.datastore.client.indices.put_settings,
2924
+ index=self.index_name,
2925
+ settings=write_unblock_settings,
2926
+ )
2927
+ logger.info("Rollback successful: writes restored to %s", self.index_name)
2928
+ except Exception as rollback_err:
2929
+ logger.critical(
2930
+ "CRITICAL: Rollback failed for %s — index may be write-blocked! Error: %s",
2931
+ self.index_name,
2932
+ str(rollback_err),
2933
+ )
2934
+
2935
+ # Clean up partially-created ILM index if it exists
2936
+ try:
2937
+ if self.with_retries(self.datastore.client.indices.exists, index=ilm_initial_index):
2938
+ self.with_retries(self.datastore.client.indices.delete, index=ilm_initial_index)
2939
+ logger.info("Cleaned up partial ILM index %s", ilm_initial_index)
2940
+ except Exception:
2941
+ logger.warning(
2942
+ "Could not clean up partial ILM index %s — manual cleanup may be needed",
2943
+ ilm_initial_index,
2944
+ )
2945
+
2946
+ # Re-raise the original exception so startup fails clearly
2947
+ raise
2948
+
2949
+ # Unblock writes on the old index (it stays around until manually removed)
2950
+ self.with_retries(
2951
+ self.datastore.client.indices.put_settings,
2952
+ index=self.index_name,
2953
+ settings=write_unblock_settings,
2954
+ )
2955
+
2956
+ # Update index_name to point to the ILM initial index
2957
+ self.index_name = ilm_initial_index
2958
+
2959
+ logger.info("Migration of %s to ILM complete", self.name.upper())
2960
+ else:
2961
+ # Fresh install — create the initial ILM index with alias
2962
+ logger.debug("Creating ILM-managed index %s...", ilm_initial_index)
2963
+ settings = self._get_index_settings()
2964
+ settings["index"]["lifecycle.name"] = f"{self.name}_policy"
2965
+ settings["index"]["lifecycle.rollover_alias"] = self.name
2966
+
2967
+ try:
2968
+ self.with_retries(
2969
+ self.datastore.client.indices.create,
2970
+ index=ilm_initial_index,
2971
+ mappings=self._get_index_mappings(),
2972
+ settings=settings,
2973
+ aliases={self.name: {"is_write_index": True}},
2974
+ )
2975
+ except elasticsearch.exceptions.RequestError as e:
2976
+ if "resource_already_exists_exception" not in str(e):
2977
+ raise
2978
+ logger.warning("ILM index already exists: %s", ilm_initial_index)
2979
+
2980
+ # Update index_name to point to the ILM initial index
2981
+ self.index_name = ilm_initial_index
2982
+
2983
+ self._check_fields()
2984
+
2761
2985
  def _add_fields(self, missing_fields: Dict):
2762
2986
  no_fix = []
2763
2987
  properties = {}
@@ -2795,6 +3019,13 @@ class ESCollection(Generic[ModelType]):
2795
3019
  **recursive_update(current_template, {"mappings": {"properties": properties}}),
2796
3020
  )
2797
3021
 
3022
+ # When ILM is enabled, also update the composable index template so
3023
+ # future rollover indices inherit the new field mappings.
3024
+ if self.ilm_config:
3025
+ from howler.odm.models.config import config as _config
3026
+
3027
+ self._create_index_template(_config.datastore.ilm)
3028
+
2798
3029
  def wipe(self):
2799
3030
  """This function should completely delete the collection
2800
3031
 
@@ -51,7 +51,8 @@ class HowlerDatastore(object):
51
51
  )
52
52
 
53
53
  for _index, _odm in INDEXES:
54
- self.ds.register(_index, _odm)
54
+ ilm_index_config = config.datastore.ilm.indices.get(_index) if config.datastore.ilm.enabled else None
55
+ self.ds.register(_index, _odm, ilm_config=ilm_index_config)
55
56
 
56
57
  def __enter__(self):
57
58
  return self