howler-api 4.0.0.dev740__tar.gz → 4.0.0.dev799__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 (216) hide show
  1. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/PKG-INFO +6 -3
  2. howler_api-4.0.0.dev799/howler/actions/add_to_bundle.py +136 -0
  3. howler_api-4.0.0.dev799/howler/actions/remove_from_bundle.py +150 -0
  4. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/__init__.py +3 -1
  5. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/socket.py +4 -0
  6. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v1/clue.py +17 -19
  7. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v1/hit.py +134 -1
  8. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v1/search.py +3 -1
  9. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v1/tool.py +45 -5
  10. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v2/ingest.py +4 -9
  11. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/app.py +5 -10
  12. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/datastore/collection.py +26 -16
  13. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/helper/discover.py +4 -4
  14. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/helper/oauth.py +0 -2
  15. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/helper.py +2 -2
  16. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/case.py +1 -1
  17. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/config.py +21 -24
  18. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/random_data.py +8 -7
  19. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/security/__init__.py +2 -10
  20. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/security/utils.py +4 -2
  21. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/services/action_service.py +2 -2
  22. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/services/auth_service.py +6 -4
  23. howler_api-4.0.0.dev799/howler/services/bundle_compat_service.py +273 -0
  24. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/services/case_service.py +0 -3
  25. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/services/config_service.py +4 -0
  26. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/services/event_service.py +3 -0
  27. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/services/hit_service.py +20 -13
  28. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/services/lucene_service.py +2 -1
  29. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/services/observable_service.py +17 -3
  30. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/services/user_service.py +3 -2
  31. howler_api-4.0.0.dev799/howler/telemetry.py +65 -0
  32. howler_api-4.0.0.dev799/howler/utils/constants.py +4 -0
  33. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/pyproject.toml +7 -4
  34. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/README.md +0 -0
  35. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/__init__.py +0 -0
  36. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/actions/__init__.py +0 -0
  37. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/actions/add_label.py +0 -0
  38. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/actions/add_to_case.py +0 -0
  39. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/actions/change_field.py +0 -0
  40. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/actions/demote.py +0 -0
  41. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/actions/example_plugin.py +0 -0
  42. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/actions/prioritization.py +0 -0
  43. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/actions/promote.py +0 -0
  44. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/actions/remove_label.py +0 -0
  45. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/actions/transition.py +0 -0
  46. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/base.py +0 -0
  47. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v1/__init__.py +0 -0
  48. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v1/action.py +0 -0
  49. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v1/analytic.py +0 -0
  50. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v1/auth.py +0 -0
  51. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v1/configs.py +0 -0
  52. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v1/dossier.py +0 -0
  53. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v1/help.py +0 -0
  54. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v1/notebook.py +0 -0
  55. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v1/overview.py +0 -0
  56. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v1/template.py +0 -0
  57. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v1/user.py +0 -0
  58. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v1/utils/__init__.py +0 -0
  59. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v1/utils/etag.py +0 -0
  60. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v1/view.py +0 -0
  61. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v2/__init__.py +0 -0
  62. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v2/case.py +0 -0
  63. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v2/search.py +0 -0
  64. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/common/README.md +0 -0
  65. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/common/__init__.py +0 -0
  66. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/common/classification.py +0 -0
  67. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/common/classification.yml +0 -0
  68. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/common/exceptions.py +0 -0
  69. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/common/loader.py +0 -0
  70. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/common/logging/__init__.py +0 -0
  71. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/common/logging/audit.py +0 -0
  72. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/common/logging/format.py +0 -0
  73. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/common/net.py +0 -0
  74. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/common/net_static.py +0 -0
  75. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/common/random_user.py +0 -0
  76. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/common/swagger.py +0 -0
  77. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/config.py +0 -0
  78. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/cronjobs/__init__.py +0 -0
  79. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/cronjobs/retention.py +0 -0
  80. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/cronjobs/view_cleanup.py +0 -0
  81. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/datastore/README.md +0 -0
  82. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/datastore/__init__.py +0 -0
  83. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/datastore/bulk.py +0 -0
  84. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/datastore/constants.py +0 -0
  85. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/datastore/exceptions.py +0 -0
  86. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/datastore/howler_store.py +0 -0
  87. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/datastore/migrations/fix_process.py +0 -0
  88. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/datastore/operations.py +0 -0
  89. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/datastore/schemas.py +0 -0
  90. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/datastore/store.py +0 -0
  91. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/datastore/support/__init__.py +0 -0
  92. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/datastore/support/build.py +0 -0
  93. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/datastore/support/schemas.py +0 -0
  94. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/datastore/types.py +0 -0
  95. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/error.py +0 -0
  96. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/external/README.md +0 -0
  97. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/external/__init__.py +0 -0
  98. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/external/generate_mitre.py +0 -0
  99. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/external/generate_sigma_rules.py +0 -0
  100. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/external/generate_tlds.py +0 -0
  101. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/external/reindex_data.py +0 -0
  102. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/external/wipe_databases.py +0 -0
  103. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/gunicorn_config.py +0 -0
  104. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/healthz.py +0 -0
  105. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/helper/__init__.py +0 -0
  106. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/helper/azure.py +0 -0
  107. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/helper/hit.py +0 -0
  108. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/helper/search.py +0 -0
  109. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/helper/workflow.py +0 -0
  110. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/helper/ws.py +0 -0
  111. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/README.md +0 -0
  112. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/__init__.py +0 -0
  113. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/base.py +0 -0
  114. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/charter.txt +0 -0
  115. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/constants.py +0 -0
  116. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/howler_enum.py +0 -0
  117. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/mixins.py +0 -0
  118. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/__init__.py +0 -0
  119. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/action.py +0 -0
  120. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/analytic.py +0 -0
  121. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/assemblyline.py +0 -0
  122. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/aws.py +0 -0
  123. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/azure.py +0 -0
  124. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/cbs.py +0 -0
  125. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/clue.py +0 -0
  126. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/dossier.py +0 -0
  127. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/__init__.py +0 -0
  128. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/agent.py +0 -0
  129. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/autonomous_system.py +0 -0
  130. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/client.py +0 -0
  131. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/cloud.py +0 -0
  132. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/code_signature.py +0 -0
  133. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/container.py +0 -0
  134. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/dns.py +0 -0
  135. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/egress.py +0 -0
  136. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/elf.py +0 -0
  137. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/email.py +0 -0
  138. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/error.py +0 -0
  139. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/event.py +0 -0
  140. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/faas.py +0 -0
  141. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/file.py +0 -0
  142. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/geo.py +0 -0
  143. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/group.py +0 -0
  144. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/hash.py +0 -0
  145. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/host.py +0 -0
  146. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/http.py +0 -0
  147. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/ingress.py +0 -0
  148. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/interface.py +0 -0
  149. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/network.py +0 -0
  150. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/observer.py +0 -0
  151. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/organization.py +0 -0
  152. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/os.py +0 -0
  153. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/pe.py +0 -0
  154. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/process.py +0 -0
  155. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/registry.py +0 -0
  156. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/related.py +0 -0
  157. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/rule.py +0 -0
  158. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/server.py +0 -0
  159. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/threat.py +0 -0
  160. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/tls.py +0 -0
  161. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/url.py +0 -0
  162. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/user.py +0 -0
  163. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/user_agent.py +0 -0
  164. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/vulnerability.py +0 -0
  165. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/gcp.py +0 -0
  166. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/hit.py +0 -0
  167. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/howler_data.py +0 -0
  168. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/lead.py +0 -0
  169. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/localized_label.py +0 -0
  170. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/observable.py +0 -0
  171. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/overview.py +0 -0
  172. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/pivot.py +0 -0
  173. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/record.py +0 -0
  174. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/template.py +0 -0
  175. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/user.py +0 -0
  176. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/view.py +0 -0
  177. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/randomizer.py +0 -0
  178. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/patched.py +0 -0
  179. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/plugins/__init__.py +0 -0
  180. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/plugins/config.py +0 -0
  181. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/remote/__init__.py +0 -0
  182. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/remote/datatypes/README.md +0 -0
  183. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/remote/datatypes/__init__.py +0 -0
  184. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/remote/datatypes/counters.py +0 -0
  185. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/remote/datatypes/events.py +0 -0
  186. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/remote/datatypes/hash.py +0 -0
  187. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/remote/datatypes/lock.py +0 -0
  188. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/remote/datatypes/queues/__init__.py +0 -0
  189. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/remote/datatypes/queues/comms.py +0 -0
  190. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/remote/datatypes/queues/multi.py +0 -0
  191. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/remote/datatypes/queues/named.py +0 -0
  192. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/remote/datatypes/queues/priority.py +0 -0
  193. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/remote/datatypes/set.py +0 -0
  194. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/remote/datatypes/user_quota_tracker.py +0 -0
  195. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/security/socket.py +0 -0
  196. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/services/__init__.py +0 -0
  197. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/services/analytic_service.py +0 -0
  198. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/services/docs_service.py +0 -0
  199. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/services/dossier_service.py +0 -0
  200. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/services/jwt_service.py +0 -0
  201. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/services/notebook_service.py +0 -0
  202. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/services/overview_service.py +0 -0
  203. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/services/search_service.py +0 -0
  204. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/services/template_service.py +0 -0
  205. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/utils/__init__.py +0 -0
  206. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/utils/annotations.py +0 -0
  207. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/utils/chunk.py +0 -0
  208. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/utils/compat.py +0 -0
  209. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/utils/dict_utils.py +0 -0
  210. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/utils/isotime.py +0 -0
  211. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/utils/list_utils.py +0 -0
  212. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/utils/lucene.py +0 -0
  213. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/utils/path.py +0 -0
  214. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/utils/socket_utils.py +0 -0
  215. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/utils/str_utils.py +0 -0
  216. {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/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.dev740
3
+ Version: 4.0.0.dev799
4
4
  Summary: Howler - API server
5
5
  License: MIT
6
6
  Keywords: howler,alerting,gc,canada,cse-cst,cse,cst,cyber,cccs
@@ -8,7 +8,7 @@ Author: Canadian Centre for Cyber Security
8
8
  Author-email: howler@cyber.gc.ca
9
9
  Maintainer: Matthew Rafuse
10
10
  Maintainer-email: matthew.rafuse@cyber.gc.ca
11
- Requires-Python: >=3.9.17,<4.0.0
11
+ Requires-Python: >=3.10,<4.0
12
12
  Classifier: Development Status :: 5 - Production/Stable
13
13
  Classifier: Intended Audience :: Developers
14
14
  Classifier: License :: OSI Approved :: MIT License
@@ -21,10 +21,10 @@ Classifier: Programming Language :: Python :: 3.14
21
21
  Classifier: Topic :: Software Development :: Libraries
22
22
  Requires-Dist: apscheduler (==3.11.2)
23
23
  Requires-Dist: authlib (>=1.6.0,<2.0.0)
24
+ Requires-Dist: azure-monitor-opentelemetry (>=1.8.7,<2.0.0)
24
25
  Requires-Dist: bcrypt (==4.3.0)
25
26
  Requires-Dist: chardet (==5.2.0)
26
27
  Requires-Dist: chevron (==0.14.0)
27
- Requires-Dist: elastic-apm[flask] (>=6.22.0,<7.0.0)
28
28
  Requires-Dist: elasticsearch (==8.19.3)
29
29
  Requires-Dist: flasgger (>=0.9.7.1,<0.10.0.0)
30
30
  Requires-Dist: flask (==3.1.3)
@@ -33,6 +33,9 @@ Requires-Dist: gevent (>=25.9.1,<26.0.0)
33
33
  Requires-Dist: gunicorn (==23.0.0)
34
34
  Requires-Dist: luqum (>=1.0.0,<2.0.0)
35
35
  Requires-Dist: mergedeep (>=1.3.4,<2.0.0)
36
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http (==1.40.0)
37
+ Requires-Dist: opentelemetry-instrumentation-flask (==0.61b0)
38
+ Requires-Dist: opentelemetry-sdk (==1.40.0)
36
39
  Requires-Dist: packaging (<25.0)
37
40
  Requires-Dist: passlib (==1.7.4)
38
41
  Requires-Dist: prometheus-client (==0.24.1)
@@ -0,0 +1,136 @@
1
+ """Deprecated add_to_bundle action — delegates to add_to_case via bundle_compat_service."""
2
+
3
+ from typing import Optional
4
+
5
+ from howler.common.exceptions import NotFoundException
6
+ from howler.common.loader import datastore
7
+ from howler.odm.models.action import VALID_TRIGGERS
8
+ from howler.services import bundle_compat_service, case_service
9
+ from howler.utils.str_utils import sanitize_lucene_query
10
+
11
+ OPERATION_ID = "add_to_bundle"
12
+
13
+
14
+ def execute(query: str, bundle_id: Optional[str] = None, **kwargs):
15
+ """Add a set of hits matching the query to the specified bundle (deprecated — uses cases).
16
+
17
+ Args:
18
+ query (str): The query containing the matching hits
19
+ bundle_id (str): The ``howler.id`` of the bundle to add the hits to.
20
+ """
21
+ report = []
22
+
23
+ if not bundle_id:
24
+ return [
25
+ {
26
+ "query": query,
27
+ "outcome": "error",
28
+ "title": "Invalid Bundle ID",
29
+ "message": "Bundle ID cannot be empty.",
30
+ }
31
+ ]
32
+
33
+ try:
34
+ case_id = bundle_compat_service.find_case_for_bundle(bundle_id)
35
+ if case_id is None:
36
+ report.append(
37
+ {
38
+ "query": query,
39
+ "outcome": "error",
40
+ "title": "Invalid Bundle",
41
+ "message": f"Either a hit with ID {bundle_id} does not exist, or it has no associated case.",
42
+ }
43
+ )
44
+ return report
45
+
46
+ ds = datastore()
47
+ matching_hits = ds.hit.search(query, rows=1000)["items"]
48
+
49
+ if not matching_hits:
50
+ report.append(
51
+ {
52
+ "query": query,
53
+ "outcome": "skipped",
54
+ "title": "No Matching Hits",
55
+ "message": "There were no hits matching this query.",
56
+ }
57
+ )
58
+ return report
59
+
60
+ added = []
61
+ skipped = []
62
+ for hit in matching_hits:
63
+ child_label = f"hits/{hit.howler.analytic} ({hit.howler.id})"
64
+ try:
65
+ case_service.append_case_item(
66
+ case_id,
67
+ item_type="hit",
68
+ item_value=hit.howler.id,
69
+ item_path=child_label,
70
+ )
71
+ added.append(hit.howler.id)
72
+ except Exception:
73
+ skipped.append(hit.howler.id)
74
+
75
+ if skipped:
76
+ report.append(
77
+ {
78
+ "query": f"howler.id:({' OR '.join(sanitize_lucene_query(h) for h in skipped)})",
79
+ "outcome": "skipped",
80
+ "title": "Skipped Hits",
81
+ "message": "These hits could not be added (already present or invalid).",
82
+ }
83
+ )
84
+
85
+ if added:
86
+ report.append(
87
+ {
88
+ "query": f"howler.id:({' OR '.join(sanitize_lucene_query(h) for h in added)})",
89
+ "outcome": "success",
90
+ "title": "Executed Successfully",
91
+ "message": "The specified bundle has had all matching hits added.",
92
+ }
93
+ )
94
+
95
+ except NotFoundException as e:
96
+ report.append(
97
+ {
98
+ "query": query,
99
+ "outcome": "error",
100
+ "title": "Failed to Execute",
101
+ "message": str(e),
102
+ }
103
+ )
104
+ except Exception as e:
105
+ report.append(
106
+ {
107
+ "query": query,
108
+ "outcome": "error",
109
+ "title": "Failed to Execute",
110
+ "message": f"Unknown exception occurred: {str(e)}",
111
+ }
112
+ )
113
+
114
+ return report
115
+
116
+
117
+ def specification():
118
+ """Specify various properties of the action, such as title, descriptions, permissions and input steps."""
119
+ return {
120
+ "id": OPERATION_ID,
121
+ "title": "Add to Bundle (Deprecated)",
122
+ "priority": 6,
123
+ "i18nKey": f"operations.{OPERATION_ID}",
124
+ "description": {
125
+ "short": "Add a set of hits to a bundle (deprecated — uses cases)",
126
+ "long": execute.__doc__,
127
+ },
128
+ "roles": ["automation_basic"],
129
+ "steps": [
130
+ {
131
+ "args": {"bundle_id": []},
132
+ "options": {},
133
+ }
134
+ ],
135
+ "triggers": VALID_TRIGGERS,
136
+ }
@@ -0,0 +1,150 @@
1
+ """Deprecated remove_from_bundle action — delegates to case_service for item removal."""
2
+
3
+ from typing import Optional
4
+
5
+ from howler.common.exceptions import NotFoundException
6
+ from howler.common.loader import datastore
7
+ from howler.odm.models.action import VALID_TRIGGERS
8
+ from howler.services import bundle_compat_service, case_service
9
+ from howler.utils.str_utils import sanitize_lucene_query
10
+
11
+ OPERATION_ID = "remove_from_bundle"
12
+
13
+
14
+ def execute(query: str, bundle_id: Optional[str] = None, **kwargs):
15
+ """Remove a set of hits matching the query from the specified bundle (deprecated — uses cases).
16
+
17
+ Args:
18
+ query (str): The query containing the matching hits
19
+ bundle_id (str): The ``howler.id`` of the bundle to remove the hits from.
20
+ """
21
+ report = []
22
+
23
+ if not bundle_id:
24
+ return [
25
+ {
26
+ "query": query,
27
+ "outcome": "error",
28
+ "title": "Invalid Bundle ID",
29
+ "message": "Bundle ID cannot be empty.",
30
+ }
31
+ ]
32
+
33
+ try:
34
+ case_id = bundle_compat_service.find_case_for_bundle(bundle_id)
35
+ if case_id is None:
36
+ report.append(
37
+ {
38
+ "query": query,
39
+ "outcome": "error",
40
+ "title": "Invalid Bundle",
41
+ "message": f"Either a hit with ID {bundle_id} does not exist, or it has no associated case.",
42
+ }
43
+ )
44
+ return report
45
+
46
+ ds = datastore()
47
+ matching_hits = ds.hit.search(query, rows=1000)["items"]
48
+
49
+ if not matching_hits:
50
+ report.append(
51
+ {
52
+ "query": query,
53
+ "outcome": "skipped",
54
+ "title": "No Matching Hits",
55
+ "message": "There were no hits matching this query.",
56
+ }
57
+ )
58
+ return report
59
+
60
+ # Get the case to check which hits are actually in it
61
+ case = ds.case.get(case_id)
62
+ if case is None:
63
+ report.append(
64
+ {
65
+ "query": query,
66
+ "outcome": "error",
67
+ "title": "Case Not Found",
68
+ "message": f"Associated case {case_id} no longer exists.",
69
+ }
70
+ )
71
+ return report
72
+
73
+ case_item_values = {item.value for item in case.items}
74
+ values_to_remove = [h.howler.id for h in matching_hits if h.howler.id in case_item_values]
75
+ skipped_ids = [h.howler.id for h in matching_hits if h.howler.id not in case_item_values]
76
+
77
+ if skipped_ids:
78
+ report.append(
79
+ {
80
+ "query": f"howler.id:({' OR '.join(sanitize_lucene_query(h) for h in skipped_ids)})",
81
+ "outcome": "skipped",
82
+ "title": "Skipped Hits Not in Bundle",
83
+ "message": "These hits are not in the bundle.",
84
+ }
85
+ )
86
+
87
+ if not values_to_remove:
88
+ report.append(
89
+ {
90
+ "query": query,
91
+ "outcome": "skipped",
92
+ "title": "No Matching Hits",
93
+ "message": "None of the matching hits were found in the bundle.",
94
+ }
95
+ )
96
+ return report
97
+
98
+ case_service.remove_case_items(case_id, values_to_remove)
99
+
100
+ report.append(
101
+ {
102
+ "query": query,
103
+ "outcome": "success",
104
+ "title": "Executed Successfully",
105
+ "message": f"Matching hits removed from bundle with id {bundle_id}",
106
+ }
107
+ )
108
+
109
+ except NotFoundException as e:
110
+ report.append(
111
+ {
112
+ "query": query,
113
+ "outcome": "error",
114
+ "title": "Failed to Execute",
115
+ "message": str(e),
116
+ }
117
+ )
118
+ except Exception as e:
119
+ report.append(
120
+ {
121
+ "query": query,
122
+ "outcome": "error",
123
+ "title": "Failed to Execute",
124
+ "message": f"Unknown exception occurred: {str(e)}",
125
+ }
126
+ )
127
+
128
+ return report
129
+
130
+
131
+ def specification():
132
+ """Specify various properties of the action, such as title, descriptions, permissions and input steps."""
133
+ return {
134
+ "id": OPERATION_ID,
135
+ "title": "Remove from Bundle (Deprecated)",
136
+ "priority": 5,
137
+ "i18nKey": f"operations.{OPERATION_ID}",
138
+ "description": {
139
+ "short": "Remove a set of hits from a bundle (deprecated — uses cases)",
140
+ "long": execute.__doc__,
141
+ },
142
+ "roles": ["automation_basic"],
143
+ "steps": [
144
+ {
145
+ "args": {"bundle_id": []},
146
+ "options": {},
147
+ }
148
+ ],
149
+ "triggers": VALID_TRIGGERS,
150
+ }
@@ -10,6 +10,7 @@ from howler import odm
10
10
  from howler.common.loader import APP_NAME
11
11
  from howler.common.logging import get_logger, log_with_traceback
12
12
  from howler.config import QUOTA_TRACKER, get_version
13
+ from howler.utils.constants import TESTING
13
14
  from howler.utils.str_utils import safe_str
14
15
 
15
16
  API_PREFIX = "/api"
@@ -73,7 +74,8 @@ def _make_api_response(
73
74
  resp.set_cookie(k, v, secure=True, httponly=True, samesite="Lax")
74
75
 
75
76
  RAW_API_COUNTER.labels(request.method, str(request.url_rule), status_code).inc()
76
- logger.info("%s %s - %s", request.method, request.path, status_code)
77
+ if not TESTING:
78
+ logger.info("%s %s - %s", request.method, request.path, status_code)
77
79
 
78
80
  return resp
79
81
 
@@ -4,6 +4,7 @@ import os
4
4
  from typing import Any
5
5
 
6
6
  from flask import Blueprint, request
7
+ from opentelemetry import trace
7
8
 
8
9
  import howler.services.event_service as event_service
9
10
  from howler.api import ok, unauthorized
@@ -21,10 +22,12 @@ socket_api = Blueprint("socket", "socket", url_prefix="/socket/v1")
21
22
  socket_api._doc = "Endpoints concerning websocket connectivity between the client and server" # type: ignore
22
23
 
23
24
  logger = get_logger(__file__)
25
+ tracer = trace.get_tracer(__name__)
24
26
 
25
27
  hit_helper = OdmHelper(Hit)
26
28
 
27
29
 
30
+ @tracer.start_as_current_span(f"{__name__}.emit")
28
31
  @socket_api.route("/emit/<event>", methods=["POST"])
29
32
  def emit(event: str):
30
33
  """Emit an event to all listening websockets"""
@@ -46,6 +49,7 @@ def emit(event: str):
46
49
  return ok()
47
50
 
48
51
 
52
+ @tracer.start_as_current_span(f"{__name__}.connect")
49
53
  @socket_api.route("/connect", websocket=True) # type: ignore
50
54
  @websocket_auth(required_priv=["R"])
51
55
  def connect(ws: Server, *args: Any, ws_id: str, **kwargs):
@@ -1,9 +1,7 @@
1
- import sys
2
1
  import time
3
2
  from typing import Callable, Optional
4
3
 
5
4
  import requests
6
- from elasticapm.traces import capture_span
7
5
  from flask import request
8
6
 
9
7
  from howler.api import bad_gateway, make_subapi_blueprint, ok
@@ -13,6 +11,7 @@ from howler.common.swagger import generate_swagger_docs
13
11
  from howler.config import cache, config
14
12
  from howler.plugins import get_plugins
15
13
  from howler.security import api_login
14
+ from howler.utils.constants import TESTING
16
15
 
17
16
  SUB_API = "clue"
18
17
  clue_api = make_subapi_blueprint(SUB_API, api_version=1)
@@ -23,7 +22,7 @@ logger = get_logger(__file__)
23
22
 
24
23
  def skip_cache(*args):
25
24
  "Function to skip cache in testing mode"
26
- return "pytest" in sys.modules
25
+ return TESTING
27
26
 
28
27
 
29
28
  @cache.memoize(15 * 60, unless=skip_cache)
@@ -74,22 +73,21 @@ def proxy_to_clue(path, **kwargs):
74
73
  clue_token = get_token(auth_token)
75
74
 
76
75
  start = time.perf_counter()
77
- with capture_span("clue", span_type="http"):
78
- if request.method.lower() == "get":
79
- response = requests.get(
80
- f"{config.core.clue.url}/{path}",
81
- headers={"Authorization": f"Bearer {clue_token}", "Accept": "application/json"},
82
- params=request.args.to_dict(),
83
- timeout=5 * 60,
84
- )
85
- else:
86
- response = requests.post(
87
- f"{config.core.clue.url}/{path}",
88
- json=request.json,
89
- headers={"Authorization": f"Bearer {clue_token}", "Accept": "application/json"},
90
- params=request.args.to_dict(),
91
- timeout=5 * 60,
92
- )
76
+ if request.method.lower() == "get":
77
+ response = requests.get(
78
+ f"{config.core.clue.url}/{path}",
79
+ headers={"Authorization": f"Bearer {clue_token}", "Accept": "application/json"},
80
+ params=request.args.to_dict(),
81
+ timeout=5 * 60,
82
+ )
83
+ else:
84
+ response = requests.post(
85
+ f"{config.core.clue.url}/{path}",
86
+ json=request.json,
87
+ headers={"Authorization": f"Bearer {clue_token}", "Accept": "application/json"},
88
+ params=request.args.to_dict(),
89
+ timeout=5 * 60,
90
+ )
93
91
 
94
92
  logger.debug("Request to clue completed in %s ms", round(time.perf_counter() - start))
95
93
 
@@ -17,7 +17,7 @@ from howler.api import (
17
17
  ok,
18
18
  )
19
19
  from howler.api.v1.utils.etag import add_etag
20
- from howler.common.exceptions import HowlerException, HowlerValueError, InvalidDataException
20
+ from howler.common.exceptions import HowlerException, HowlerValueError, InvalidDataException, NotFoundException
21
21
  from howler.common.loader import datastore
22
22
  from howler.common.logging import get_logger
23
23
  from howler.common.swagger import generate_swagger_docs
@@ -976,3 +976,136 @@ def remove_react_comment(id: str, comment_id: str, user: dict[str, Any], **kwarg
976
976
  new_hit, version = hit_service.save_hit(hit, version=kwargs.get("server_version"))
977
977
 
978
978
  return ok(new_hit), version
979
+
980
+
981
+ # ---------------------------------------------------------------------------
982
+ # Deprecated bundle shim endpoints
983
+ # ---------------------------------------------------------------------------
984
+
985
+
986
+ def _deprecation_headers(response):
987
+ """Inject deprecation headers into a Flask Response."""
988
+ response.headers["Deprecation"] = "true"
989
+ response.headers["Sunset"] = "2027-01-01"
990
+ return response
991
+
992
+
993
+ @generate_swagger_docs()
994
+ @hit_api.route("/bundle", methods=["POST"])
995
+ @api_login(audit=False, required_priv=["W"])
996
+ def create_bundle(user: User, **kwargs):
997
+ """Create a new bundle (deprecated — creates a case instead).
998
+
999
+ Variables:
1000
+ None
1001
+
1002
+ Arguments:
1003
+ None
1004
+
1005
+ Data Block:
1006
+ {
1007
+ "bundle": {
1008
+ ...hit # A howler hit that will be used as a template for this new bundle
1009
+ },
1010
+ "hits": [...ids] # A list of existing howler hits to add as children to the new bundle
1011
+ }
1012
+
1013
+ Result Example:
1014
+ {
1015
+ ...hit # The created bundle (synthesized from the underlying case)
1016
+ }
1017
+ """
1018
+ from howler.services import bundle_compat_service
1019
+
1020
+ data = request.json
1021
+ if not isinstance(data, dict):
1022
+ return bad_request(err="Invalid data format")
1023
+
1024
+ bundle_hit: Optional[dict[str, Any]] = data.get("bundle")
1025
+ if bundle_hit is None:
1026
+ return bad_request(err="You did not provide a bundle hit.")
1027
+
1028
+ child_hits: list[str] = data.get("hits", [])
1029
+
1030
+ try:
1031
+ result = bundle_compat_service.create_bundle(bundle_hit, child_hits, user=user.uname)
1032
+ return _deprecation_headers(created(result))
1033
+ except HowlerException as e:
1034
+ return bad_request(err=str(e))
1035
+
1036
+
1037
+ @generate_swagger_docs()
1038
+ @hit_api.route("/bundle/<id>", methods=["PUT"])
1039
+ @api_login(audit=False, required_priv=["W"])
1040
+ def update_bundle(id, **kwargs):
1041
+ """Add hits to a bundle (deprecated — adds items to the underlying case).
1042
+
1043
+ Variables:
1044
+ id => The ID of the bundle to update
1045
+
1046
+ Arguments:
1047
+ None
1048
+
1049
+ Data Block:
1050
+ [
1051
+ ...ids
1052
+ ]
1053
+
1054
+ Result Example:
1055
+ {
1056
+ ...hit # The updated bundle (synthesized from the underlying case)
1057
+ }
1058
+ """
1059
+ from howler.services import bundle_compat_service
1060
+ from howler.services.bundle_compat_service import BundleConflictException
1061
+
1062
+ hit_ids = request.json
1063
+ if not isinstance(hit_ids, list):
1064
+ return bad_request(err="Invalid data format")
1065
+
1066
+ try:
1067
+ result = bundle_compat_service.add_to_bundle(id, hit_ids)
1068
+ return _deprecation_headers(ok(result))
1069
+ except BundleConflictException as e:
1070
+ return conflict(err=str(e))
1071
+ except NotFoundException as e:
1072
+ return not_found(err=str(e))
1073
+ except HowlerException as e:
1074
+ return bad_request(err=str(e))
1075
+
1076
+
1077
+ @generate_swagger_docs()
1078
+ @hit_api.route("/bundle/<id>", methods=["DELETE"])
1079
+ @api_login(audit=False, required_priv=["W"])
1080
+ def remove_bundle_children(id, **kwargs):
1081
+ """Remove hits from a bundle (deprecated — removes items from the underlying case).
1082
+
1083
+ Variables:
1084
+ id => The ID of the bundle to update
1085
+
1086
+ Arguments:
1087
+ None
1088
+
1089
+ Data Block:
1090
+ [
1091
+ ...ids OR '*' # A list of ids to remove, or a single '*' to remove all
1092
+ ]
1093
+
1094
+ Result Example:
1095
+ {
1096
+ ...hit # The updated hit (synthesized from the underlying case)
1097
+ }
1098
+ """
1099
+ from howler.services import bundle_compat_service
1100
+
1101
+ hit_ids = request.json
1102
+ if not isinstance(hit_ids, list):
1103
+ return bad_request(err="Invalid data format")
1104
+
1105
+ try:
1106
+ result = bundle_compat_service.remove_from_bundle(id, hit_ids)
1107
+ return _deprecation_headers(ok(result))
1108
+ except NotFoundException as e:
1109
+ return not_found(err=str(e))
1110
+ except HowlerException as e:
1111
+ return bad_request(err=str(e))
@@ -556,11 +556,13 @@ def count(index, **kwargs):
556
556
  params.update({"access_control": user["access_control"]})
557
557
 
558
558
  query = req_data.get("query", None)
559
+ filters = req_data.get("filters", [])
560
+
559
561
  if not query:
560
562
  return bad_request(err="There was no search query.")
561
563
 
562
564
  try:
563
- return ok(collection().count(query, **params))
565
+ return ok(collection().count(query, filters=filters, **params))
564
566
  except (SearchException, BadRequestError) as e:
565
567
  return bad_request(err=f"SearchException: {e}")
566
568
 
@@ -77,8 +77,11 @@ def create_one_or_many_hits(tool_name: str, user: User, **kwargs): # noqa: C901
77
77
  warnings = []
78
78
  # Validate field_map targets
79
79
  hit_fields = Hit.flat_fields()
80
+ _bundle_compat_fields = {"howler.is_bundle", "howler.hits", "howler.bundle_size", "howler.bundles"}
80
81
  for targets in field_map.values():
81
82
  for target in targets:
83
+ if target in _bundle_compat_fields:
84
+ continue
82
85
  # This is checking to see if the target matches one of two cases:
83
86
  # Simple fields - hit.obj.key of type str (should match)
84
87
  # Compound fields - hit.obj of type dict (should also match)
@@ -93,7 +96,9 @@ def create_one_or_many_hits(tool_name: str, user: User, **kwargs): # noqa: C901
93
96
  return bad_request(err=warning)
94
97
 
95
98
  out: list[dict[str, Any]] = []
96
- odms = []
99
+ odms: list[Hit] = []
100
+ bundle_raw: dict[str, Any] | None = None
101
+ bundle_index: int | None = None
97
102
  for hit in hits:
98
103
  cur_id = get_random_id()
99
104
  cur_time = now_as_iso()
@@ -135,8 +140,19 @@ def create_one_or_many_hits(tool_name: str, user: User, **kwargs): # noqa: C901
135
140
  obj[target] = _val
136
141
 
137
142
  try:
143
+ is_bundle = obj.pop("howler.is_bundle", False)
144
+ obj.pop("howler.hits", None)
145
+ obj.pop("howler.bundle_size", None)
146
+ obj.pop("howler.bundles", None)
147
+
138
148
  odm, warns = hit_service.convert_hit(obj, unique=True, ignore_extra_values=ignore_extra_values)
139
149
 
150
+ if is_bundle:
151
+ if bundle_raw is not None:
152
+ return bad_request(err="You can only specify one bundle hit!")
153
+ bundle_raw = obj
154
+ bundle_index = len(odms)
155
+
140
156
  odms.append(odm)
141
157
 
142
158
  out.append(
@@ -155,12 +171,36 @@ def create_one_or_many_hits(tool_name: str, user: User, **kwargs): # noqa: C901
155
171
  if any([obj["error"] for obj in out]):
156
172
  return bad_request(out, warnings=warnings, err="No valid hits were provided")
157
173
  else:
158
- for odm in odms:
159
- hit_service.create_hit(odm.howler.id, odm, user=user.uname)
160
- analytic_service.save_from_hit(odm, user)
174
+ if bundle_index is not None:
175
+ # Route through bundle compat service → creates a case
176
+ from howler.services import bundle_compat_service
177
+
178
+ bundle_odm = odms[bundle_index]
179
+ child_odms = [odm for i, odm in enumerate(odms) if i != bundle_index]
180
+
181
+ for odm in child_odms:
182
+ hit_service.create_hit(odm.howler.id, odm, user=user.uname)
183
+ analytic_service.save_from_hit(odm, user)
184
+
185
+ child_ids = [odm.howler.id for odm in child_odms]
186
+ bundle_data = bundle_odm.as_primitives()
187
+ result = bundle_compat_service.create_bundle(bundle_data, child_ids, user=user.uname)
188
+ warnings.append(bundle_compat_service.DEPRECATION_MESSAGE)
189
+
190
+ # Replace the bundle entry in the output with the created bundle id
191
+ for entry in out:
192
+ if entry.get("id") == bundle_odm.howler.id:
193
+ entry["id"] = result["howler"]["id"]
194
+ entry["_case_id"] = result.get("_case_id")
195
+ else:
196
+ for odm in odms:
197
+ hit_service.create_hit(odm.howler.id, odm, user=user.uname)
198
+ analytic_service.save_from_hit(odm, user)
161
199
 
162
200
  datastore().hit.commit()
163
201
 
164
- action_service.bulk_execute_on_query(f"howler.id:({' OR '.join(entry['id'] for entry in out)})", user=user)
202
+ action_service.bulk_execute_on_query(
203
+ f"howler.id:({' OR '.join(entry['id'] for entry in out if entry['id'])})", user=user
204
+ )
165
205
 
166
206
  return created(out, warnings=warnings)