howler-api 4.0.0.dev1246__tar.gz → 4.0.0.dev1258__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 (230) hide show
  1. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/PKG-INFO +2 -2
  2. howler_api-4.0.0.dev1258/howler/actions/add_to_bundle.py +152 -0
  3. howler_api-4.0.0.dev1258/howler/actions/add_to_case.py +141 -0
  4. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/actions/demote.py +2 -2
  5. howler_api-4.0.0.dev1258/howler/actions/remove_from_bundle.py +166 -0
  6. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/actions/transition.py +5 -7
  7. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/api/__init__.py +26 -9
  8. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/api/socket.py +56 -16
  9. howler_api-4.0.0.dev1258/howler/api/v1/__init__.py +44 -0
  10. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/api/v1/action.py +1 -2
  11. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/api/v1/analytic.py +15 -158
  12. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/api/v1/hit.py +52 -128
  13. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/api/v1/search.py +2 -31
  14. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/api/v1/tool.py +47 -22
  15. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/api/v1/user.py +1 -1
  16. howler_api-4.0.0.dev1258/howler/api/v1/utils/etag.py +106 -0
  17. howler_api-4.0.0.dev1258/howler/api/v2/__init__.py +44 -0
  18. howler_api-4.0.0.dev1258/howler/api/v2/case.py +512 -0
  19. howler_api-4.0.0.dev1258/howler/api/v2/fuzzy.py +121 -0
  20. howler_api-4.0.0.dev1258/howler/api/v2/ingest.py +387 -0
  21. howler_api-4.0.0.dev1258/howler/api/v2/search.py +323 -0
  22. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/app.py +17 -0
  23. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/common/loader.py +5 -6
  24. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/config.py +0 -1
  25. howler_api-4.0.0.dev1258/howler/cronjobs/correlation.py +36 -0
  26. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/datastore/collection.py +53 -35
  27. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/datastore/howler_store.py +29 -14
  28. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/datastore/types.py +1 -6
  29. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/helper/discover.py +14 -14
  30. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/helper/hit.py +4 -4
  31. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/helper/search.py +13 -3
  32. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/base.py +26 -2
  33. howler_api-4.0.0.dev1258/howler/odm/constants.py +31 -0
  34. howler_api-4.0.0.dev1258/howler/odm/helper.py +945 -0
  35. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/howler_enum.py +3 -0
  36. howler_api-4.0.0.dev1258/howler/odm/mixins.py +100 -0
  37. howler_api-4.0.0.dev1258/howler/odm/models/case.py +265 -0
  38. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/config.py +14 -0
  39. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/event.py +1 -1
  40. howler_api-4.0.0.dev1258/howler/odm/models/event.py +121 -0
  41. howler_api-4.0.0.dev1258/howler/odm/models/hit.py +31 -0
  42. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/howler_data.py +3 -56
  43. howler_api-4.0.0.dev1246/howler/odm/models/hit.py → howler_api-4.0.0.dev1258/howler/odm/models/record.py +15 -28
  44. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/view.py +8 -0
  45. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/random_data.py +83 -49
  46. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/randomizer.py +6 -2
  47. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/remote/datatypes/__init__.py +11 -4
  48. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/remote/datatypes/queues/comms.py +2 -1
  49. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/security/socket.py +27 -2
  50. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/services/analytic_service.py +6 -2
  51. howler_api-4.0.0.dev1258/howler/services/bundle_compat_service.py +269 -0
  52. howler_api-4.0.0.dev1258/howler/services/case_service.py +1169 -0
  53. howler_api-4.0.0.dev1258/howler/services/comms_service.py +134 -0
  54. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/services/config_service.py +6 -4
  55. howler_api-4.0.0.dev1258/howler/services/correlation_service.py +219 -0
  56. howler_api-4.0.0.dev1246/howler/api/v1/__init__.py → howler_api-4.0.0.dev1258/howler/services/docs_service.py +38 -46
  57. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/services/dossier_service.py +11 -2
  58. howler_api-4.0.0.dev1258/howler/services/event_service.py +147 -0
  59. howler_api-4.0.0.dev1258/howler/services/fuzzy_service.py +426 -0
  60. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/services/hit_service.py +94 -177
  61. howler_api-4.0.0.dev1258/howler/services/search_service.py +390 -0
  62. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/services/user_service.py +7 -1
  63. howler_api-4.0.0.dev1258/howler/services/viewer_service.py +43 -0
  64. howler_api-4.0.0.dev1258/howler/utils/net_utils.py +40 -0
  65. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/utils/socket_utils.py +12 -28
  66. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/utils/str_utils.py +1 -1
  67. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/pyproject.toml +5 -5
  68. howler_api-4.0.0.dev1246/howler/actions/add_to_bundle.py +0 -171
  69. howler_api-4.0.0.dev1246/howler/actions/remove_from_bundle.py +0 -144
  70. howler_api-4.0.0.dev1246/howler/api/v1/utils/etag.py +0 -84
  71. howler_api-4.0.0.dev1246/howler/cronjobs/rules.py +0 -279
  72. howler_api-4.0.0.dev1246/howler/odm/helper.py +0 -417
  73. howler_api-4.0.0.dev1246/howler/services/event_service.py +0 -96
  74. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/README.md +0 -0
  75. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/__init__.py +0 -0
  76. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/actions/__init__.py +0 -0
  77. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/actions/add_label.py +0 -0
  78. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/actions/change_field.py +0 -0
  79. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/actions/example_plugin.py +0 -0
  80. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/actions/prioritization.py +0 -0
  81. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/actions/promote.py +0 -0
  82. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/actions/remove_label.py +0 -0
  83. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/api/base.py +0 -0
  84. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/api/v1/auth.py +0 -0
  85. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/api/v1/clue.py +0 -0
  86. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/api/v1/configs.py +0 -0
  87. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/api/v1/dossier.py +0 -0
  88. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/api/v1/help.py +0 -0
  89. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/api/v1/notebook.py +0 -0
  90. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/api/v1/overview.py +0 -0
  91. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/api/v1/template.py +0 -0
  92. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/api/v1/utils/__init__.py +0 -0
  93. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/api/v1/utils/params.py +0 -0
  94. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/api/v1/view.py +0 -0
  95. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/common/README.md +0 -0
  96. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/common/__init__.py +0 -0
  97. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/common/classification.py +0 -0
  98. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/common/classification.yml +0 -0
  99. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/common/exceptions.py +0 -0
  100. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/common/logging/__init__.py +0 -0
  101. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/common/logging/audit.py +0 -0
  102. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/common/logging/format.py +0 -0
  103. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/common/net.py +0 -0
  104. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/common/net_static.py +0 -0
  105. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/common/random_user.py +0 -0
  106. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/common/swagger.py +0 -0
  107. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/cronjobs/__init__.py +0 -0
  108. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/cronjobs/action_queue_worker.py +0 -0
  109. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/cronjobs/retention.py +0 -0
  110. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/cronjobs/view_cleanup.py +0 -0
  111. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/datastore/README.md +0 -0
  112. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/datastore/__init__.py +0 -0
  113. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/datastore/bulk.py +0 -0
  114. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/datastore/constants.py +0 -0
  115. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/datastore/exceptions.py +0 -0
  116. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/datastore/migrations/fix_process.py +0 -0
  117. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/datastore/operations.py +0 -0
  118. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/datastore/schemas.py +0 -0
  119. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/datastore/store.py +0 -0
  120. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/datastore/support/__init__.py +0 -0
  121. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/datastore/support/build.py +0 -0
  122. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/datastore/support/schemas.py +0 -0
  123. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/error.py +0 -0
  124. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/external/README.md +0 -0
  125. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/external/__init__.py +0 -0
  126. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/external/generate_mitre.py +0 -0
  127. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/external/generate_sigma_rules.py +0 -0
  128. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/external/generate_tlds.py +0 -0
  129. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/external/reindex_data.py +0 -0
  130. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/external/wipe_databases.py +0 -0
  131. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/gunicorn_config.py +0 -0
  132. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/healthz.py +0 -0
  133. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/helper/__init__.py +0 -0
  134. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/helper/azure.py +0 -0
  135. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/helper/oauth.py +0 -0
  136. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/helper/workflow.py +0 -0
  137. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/helper/ws.py +0 -0
  138. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/README.md +0 -0
  139. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/__init__.py +0 -0
  140. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/charter.txt +0 -0
  141. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/__init__.py +0 -0
  142. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/action.py +0 -0
  143. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/analytic.py +0 -0
  144. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/assemblyline.py +0 -0
  145. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/aws.py +0 -0
  146. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/azure.py +0 -0
  147. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/cbs.py +0 -0
  148. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/clue.py +0 -0
  149. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/dossier.py +0 -0
  150. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/__init__.py +0 -0
  151. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/agent.py +0 -0
  152. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/autonomous_system.py +0 -0
  153. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/client.py +0 -0
  154. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/cloud.py +0 -0
  155. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/code_signature.py +0 -0
  156. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/container.py +0 -0
  157. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/dns.py +0 -0
  158. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/egress.py +0 -0
  159. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/elf.py +0 -0
  160. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/email.py +0 -0
  161. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/error.py +0 -0
  162. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/faas.py +0 -0
  163. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/file.py +0 -0
  164. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/geo.py +0 -0
  165. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/group.py +0 -0
  166. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/hash.py +0 -0
  167. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/host.py +0 -0
  168. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/http.py +0 -0
  169. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/ingress.py +0 -0
  170. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/interface.py +0 -0
  171. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/network.py +0 -0
  172. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/observer.py +0 -0
  173. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/organization.py +0 -0
  174. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/os.py +0 -0
  175. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/pe.py +0 -0
  176. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/process.py +0 -0
  177. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/registry.py +0 -0
  178. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/related.py +0 -0
  179. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/rule.py +0 -0
  180. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/server.py +0 -0
  181. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/threat.py +0 -0
  182. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/tls.py +0 -0
  183. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/url.py +0 -0
  184. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/user.py +0 -0
  185. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/user_agent.py +0 -0
  186. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/ecs/vulnerability.py +0 -0
  187. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/gcp.py +0 -0
  188. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/lead.py +0 -0
  189. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/localized_label.py +0 -0
  190. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/overview.py +0 -0
  191. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/pivot.py +0 -0
  192. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/template.py +0 -0
  193. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/odm/models/user.py +0 -0
  194. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/patched.py +0 -0
  195. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/plugins/__init__.py +0 -0
  196. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/plugins/config.py +0 -0
  197. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/remote/__init__.py +0 -0
  198. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/remote/datatypes/README.md +0 -0
  199. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/remote/datatypes/counters.py +0 -0
  200. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/remote/datatypes/events.py +0 -0
  201. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/remote/datatypes/hash.py +0 -0
  202. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/remote/datatypes/lock.py +0 -0
  203. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/remote/datatypes/queues/__init__.py +0 -0
  204. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/remote/datatypes/queues/multi.py +0 -0
  205. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/remote/datatypes/queues/named.py +0 -0
  206. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/remote/datatypes/queues/priority.py +0 -0
  207. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/remote/datatypes/set.py +0 -0
  208. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/remote/datatypes/user_quota_tracker.py +0 -0
  209. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/security/__init__.py +0 -0
  210. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/security/utils.py +0 -0
  211. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/services/__init__.py +0 -0
  212. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/services/action_service.py +0 -0
  213. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/services/auth_service.py +0 -0
  214. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/services/jwt_service.py +0 -0
  215. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/services/lucene_service.py +0 -0
  216. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/services/notebook_service.py +0 -0
  217. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/services/overview_service.py +0 -0
  218. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/services/template_service.py +0 -0
  219. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/telemetry.py +0 -0
  220. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/utils/__init__.py +0 -0
  221. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/utils/annotations.py +0 -0
  222. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/utils/chunk.py +0 -0
  223. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/utils/compat.py +0 -0
  224. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/utils/constants.py +0 -0
  225. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/utils/dict_utils.py +0 -0
  226. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/utils/isotime.py +0 -0
  227. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/utils/list_utils.py +0 -0
  228. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/utils/lucene.py +0 -0
  229. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/howler/utils/path.py +0 -0
  230. {howler_api-4.0.0.dev1246 → howler_api-4.0.0.dev1258}/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.dev1246
3
+ Version: 4.0.0.dev1258
4
4
  Summary: Howler - API server
5
5
  License: MIT
6
6
  Keywords: howler,alerting,gc,canada,cse-cst,cse,cst,cyber,cccs
@@ -29,7 +29,7 @@ Requires-Dist: elasticsearch (==8.19.3)
29
29
  Requires-Dist: flasgger (>=0.9.7.1,<0.10.0.0)
30
30
  Requires-Dist: flask (==3.1.3)
31
31
  Requires-Dist: flask-caching (==2.4.0)
32
- Requires-Dist: gevent (==23.9.1)
32
+ 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)
@@ -0,0 +1,152 @@
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.actions import check_hit_limit
6
+ from howler.common.exceptions import NotFoundException
7
+ from howler.common.loader import datastore
8
+ from howler.odm.models.action import VALID_TRIGGERS
9
+ from howler.odm.models.case import CaseItemTypes
10
+ from howler.odm.models.user import User
11
+ from howler.services import bundle_compat_service, case_service
12
+ from howler.utils.str_utils import sanitize_lucene_query
13
+
14
+ OPERATION_ID = "add_to_bundle"
15
+ MAX_HITS_BASIC = 10
16
+ MAX_HITS_ADVANCED = 1000
17
+ SKIP_CENTRAL_LIMIT = True # This operation transforms the query, handles limit check locally
18
+
19
+
20
+ def execute(query: str, bundle_id: Optional[str] = None, user: Optional[User] = None, **kwargs): # noqa: C901
21
+ """Add a set of hits matching the query to the specified bundle (deprecated — uses cases).
22
+
23
+ Args:
24
+ query (str): The query containing the matching hits
25
+ bundle_id (str): The ``howler.id`` of the bundle to add the hits to.
26
+ """
27
+ report = []
28
+
29
+ if not bundle_id:
30
+ return [
31
+ {
32
+ "query": query,
33
+ "outcome": "error",
34
+ "title": "Invalid Bundle ID",
35
+ "message": "Bundle ID cannot be empty.",
36
+ }
37
+ ]
38
+
39
+ try:
40
+ case = bundle_compat_service.find_case_for_bundle(bundle_id)
41
+ if case is None:
42
+ report.append(
43
+ {
44
+ "query": query,
45
+ "outcome": "error",
46
+ "title": "Invalid Bundle",
47
+ "message": f"Either a hit with ID {bundle_id} does not exist, or it has no associated case.",
48
+ }
49
+ )
50
+ return report
51
+
52
+ ds = datastore()
53
+
54
+ # Check hit limit against the query before searching
55
+ if user:
56
+ limit_error = check_hit_limit(query, user, MAX_HITS_BASIC, MAX_HITS_ADVANCED)
57
+ if limit_error:
58
+ return [limit_error]
59
+
60
+ matching_hits = ds.hit.search(query, rows=MAX_HITS_ADVANCED)["items"]
61
+
62
+ if not matching_hits:
63
+ report.append(
64
+ {
65
+ "query": query,
66
+ "outcome": "skipped",
67
+ "title": "No Matching Hits",
68
+ "message": "There were no hits matching this query.",
69
+ }
70
+ )
71
+ return report
72
+
73
+ folder = case_service.get_parent_from_path(case, "hits", create_if_missing=True)
74
+
75
+ added = []
76
+ skipped = []
77
+ for hit in matching_hits:
78
+ name = f"{hit.howler.analytic} ({hit.howler.id})"
79
+ try:
80
+ case_service.append_case_item(
81
+ case,
82
+ item_type=CaseItemTypes.HIT,
83
+ item_name=name,
84
+ item_value=hit.howler.id,
85
+ item_parent=folder.id if folder else None,
86
+ )
87
+ added.append(hit.howler.id)
88
+ except Exception: # pragma: no cover
89
+ skipped.append(hit.howler.id)
90
+
91
+ if skipped:
92
+ report.append(
93
+ {
94
+ "query": f"howler.id:({' OR '.join(sanitize_lucene_query(h) for h in skipped)})",
95
+ "outcome": "skipped",
96
+ "title": "Skipped Hits",
97
+ "message": "These hits could not be added (already present or invalid).",
98
+ }
99
+ )
100
+
101
+ if added:
102
+ report.append(
103
+ {
104
+ "query": f"howler.id:({' OR '.join(sanitize_lucene_query(h) for h in added)})",
105
+ "outcome": "success",
106
+ "title": "Executed Successfully",
107
+ "message": "The specified bundle has had all matching hits added.",
108
+ }
109
+ )
110
+
111
+ except NotFoundException as e:
112
+ report.append(
113
+ {
114
+ "query": query,
115
+ "outcome": "error",
116
+ "title": "Failed to Execute",
117
+ "message": str(e),
118
+ }
119
+ )
120
+ except Exception as e:
121
+ report.append(
122
+ {
123
+ "query": query,
124
+ "outcome": "error",
125
+ "title": "Failed to Execute",
126
+ "message": f"Unknown exception occurred: {str(e)}",
127
+ }
128
+ )
129
+
130
+ return report
131
+
132
+
133
+ def specification():
134
+ """Specify various properties of the action, such as title, descriptions, permissions and input steps."""
135
+ return {
136
+ "id": OPERATION_ID,
137
+ "title": "Add to Bundle (Deprecated)",
138
+ "priority": 6,
139
+ "i18nKey": f"operations.{OPERATION_ID}",
140
+ "description": {
141
+ "short": "Add a set of hits to a bundle (deprecated — uses cases)",
142
+ "long": execute.__doc__,
143
+ },
144
+ "roles": ["automation_basic", "actionrunner_basic"],
145
+ "steps": [
146
+ {
147
+ "args": {"bundle_id": []},
148
+ "options": {},
149
+ }
150
+ ],
151
+ "triggers": VALID_TRIGGERS,
152
+ }
@@ -0,0 +1,141 @@
1
+ from typing import Optional
2
+
3
+ import chevron
4
+
5
+ from howler.common.exceptions import InvalidDataException, NotFoundException
6
+ from howler.common.loader import datastore
7
+ from howler.odm.models.action import VALID_TRIGGERS
8
+ from howler.services import case_service
9
+
10
+ OPERATION_ID = "add_to_case"
11
+
12
+
13
+ def execute( # noqa: C901
14
+ query: str,
15
+ case_id: Optional[str] = None,
16
+ path: str = "related",
17
+ title_template: str = "{{howler.analytic}} ({{howler.id}})",
18
+ **kwargs,
19
+ ):
20
+ """Add matching alerts to a given case.
21
+
22
+ Args:
23
+ query (str): The query on which to apply this automation.
24
+ case_id (str): The ID of the case to add the alerts to.
25
+ path (str): The path within the case at which to place the alerts. Defaults to "related".
26
+ title_template (str): A Mustache-compatible template string used to generate each item's
27
+ name. The hit's fields are available as template variables.
28
+ Defaults to "{{howler.analytic}} ({{howler.id}})".
29
+ """
30
+ if not case_id:
31
+ return [
32
+ {
33
+ "query": query,
34
+ "outcome": "error",
35
+ "title": "Missing Case ID",
36
+ "message": "A case_id must be provided.",
37
+ }
38
+ ]
39
+
40
+ ds = datastore()
41
+
42
+ case = ds.case.get(case_id)
43
+ if case is None:
44
+ return [
45
+ {
46
+ "query": query,
47
+ "outcome": "error",
48
+ "title": "Case Not Found",
49
+ "message": f"No case with ID '{case_id}' exists.",
50
+ }
51
+ ]
52
+
53
+ hits = ds.hit.search(query, rows=1000)["items"]
54
+
55
+ if not hits:
56
+ return [
57
+ {
58
+ "query": query,
59
+ "outcome": "skipped",
60
+ "title": "No Matching Hits",
61
+ "message": "No hits matched the query, so the action was skipped.",
62
+ }
63
+ ]
64
+
65
+ report = []
66
+ skipped = []
67
+ added = []
68
+
69
+ for hit in hits:
70
+ title = chevron.render(title_template, hit.as_primitives())
71
+ item_path = f"{path.rstrip('/')}/{title}" if path else title
72
+ try:
73
+ path, name = item_path.rsplit("/", maxsplit=1)
74
+ except ValueError:
75
+ name = item_path
76
+
77
+ try:
78
+ parent = case_service.get_parent_from_path(case, path, create_if_missing=True)
79
+
80
+ case_service.append_case_item(
81
+ case,
82
+ item_type="hit",
83
+ item_value=hit.howler.id,
84
+ item_name=name,
85
+ item_parent=parent.id if parent else None,
86
+ )
87
+ added.append(hit.howler.id)
88
+ except InvalidDataException as e:
89
+ skipped.append(f"{hit.howler.id}: {e}")
90
+ except NotFoundException as e: # pragma: no cover
91
+ skipped.append(f"{hit.howler.id}: {e}")
92
+ except Exception as e: # pragma: no cover
93
+ skipped.append(f"{hit.howler.id}: {e}")
94
+
95
+ if added:
96
+ report.append(
97
+ {
98
+ "query": f"howler.id:({' OR '.join(added)})",
99
+ "outcome": "success",
100
+ "title": "Added to Case",
101
+ "message": f"{len(added)} alert(s) successfully added to case '{case_id}'.",
102
+ }
103
+ )
104
+
105
+ if skipped:
106
+ report.append(
107
+ {
108
+ "query": query,
109
+ "outcome": "skipped",
110
+ "title": "Skipped Alerts",
111
+ "message": f"{len(skipped)} alert(s) could not be added: {'; '.join(skipped)}",
112
+ }
113
+ )
114
+
115
+ return report
116
+
117
+
118
+ def specification():
119
+ """Specify various properties of the action, such as title, descriptions, permissions and input steps."""
120
+ return {
121
+ "id": OPERATION_ID,
122
+ "title": "Add to Case",
123
+ "priority": 9,
124
+ "i18nKey": f"operations.{OPERATION_ID}",
125
+ "description": {
126
+ "short": "Add matching alerts to a case",
127
+ "long": execute.__doc__,
128
+ },
129
+ "roles": ["automation_basic"],
130
+ "steps": [
131
+ {
132
+ "args": {
133
+ "case_id": [],
134
+ "path": [],
135
+ "title_template": [],
136
+ },
137
+ "options": {},
138
+ }
139
+ ],
140
+ "triggers": VALID_TRIGGERS,
141
+ }
@@ -9,7 +9,7 @@ from howler.odm.models.howler_data import (
9
9
  Assessment,
10
10
  AssessmentEscalationMap,
11
11
  Escalation,
12
- HitStatus,
12
+ Status,
13
13
  )
14
14
  from howler.odm.models.user import User
15
15
  from howler.utils.str_utils import sanitize_lucene_query
@@ -99,7 +99,7 @@ def execute(
99
99
  "howler.assignment",
100
100
  user.get("uname", "automation") if user else "automation",
101
101
  ),
102
- odm_helper.update("howler.status", HitStatus.RESOLVED),
102
+ odm_helper.update("howler.status", Status.RESOLVED),
103
103
  ],
104
104
  )
105
105
 
@@ -0,0 +1,166 @@
1
+ """Deprecated remove_from_bundle action — delegates to case_service for item removal."""
2
+
3
+ from typing import Optional
4
+
5
+ from howler.actions import check_hit_limit
6
+ from howler.common.exceptions import NotFoundException
7
+ from howler.common.loader import datastore
8
+ from howler.odm.models.action import VALID_TRIGGERS
9
+ from howler.odm.models.user import User
10
+ from howler.services import bundle_compat_service, case_service
11
+ from howler.utils.str_utils import sanitize_lucene_query
12
+
13
+ OPERATION_ID = "remove_from_bundle"
14
+ MAX_HITS_BASIC = 10
15
+ MAX_HITS_ADVANCED = 1000
16
+ SKIP_CENTRAL_LIMIT = True # This operation transforms the query, handles limit check locally
17
+
18
+
19
+ def execute(query: str, bundle_id: Optional[str] = None, user: Optional[User] = None, **kwargs): # noqa: C901
20
+ """Remove a set of hits matching the query from the specified bundle (deprecated — uses cases).
21
+
22
+ Args:
23
+ query (str): The query containing the matching hits
24
+ bundle_id (str): The ``howler.id`` of the bundle to remove the hits from.
25
+ """
26
+ report = []
27
+
28
+ if not bundle_id:
29
+ return [
30
+ {
31
+ "query": query,
32
+ "outcome": "error",
33
+ "title": "Invalid Bundle ID",
34
+ "message": "Bundle ID cannot be empty.",
35
+ }
36
+ ]
37
+
38
+ try:
39
+ case = bundle_compat_service.find_case_for_bundle(bundle_id)
40
+ if case is None:
41
+ report.append(
42
+ {
43
+ "query": query,
44
+ "outcome": "error",
45
+ "title": "Invalid Bundle",
46
+ "message": f"Either a hit with ID {bundle_id} does not exist, or it has no associated case.",
47
+ }
48
+ )
49
+ return report
50
+
51
+ ds = datastore()
52
+
53
+ # Check hit limit against the query before searching
54
+ if user:
55
+ limit_error = check_hit_limit(query, user, MAX_HITS_BASIC, MAX_HITS_ADVANCED)
56
+ if limit_error:
57
+ return [limit_error]
58
+
59
+ matching_hits = ds.hit.search(query, rows=MAX_HITS_ADVANCED)["items"]
60
+
61
+ if not matching_hits:
62
+ report.append(
63
+ {
64
+ "query": query,
65
+ "outcome": "skipped",
66
+ "title": "No Matching Hits",
67
+ "message": "There were no hits matching this query.",
68
+ }
69
+ )
70
+ return report
71
+
72
+ # Refresh case from datastore to ensure latest state
73
+ refreshed_case = ds.case.get(case.case_id)
74
+ if refreshed_case is None:
75
+ report.append(
76
+ {
77
+ "query": query,
78
+ "outcome": "error",
79
+ "title": "Case Not Found",
80
+ "message": f"Associated case {case.case_id} no longer exists.",
81
+ }
82
+ )
83
+ return report
84
+
85
+ case = refreshed_case
86
+ case_item_values = {item.value for item in case.items}
87
+ values_to_remove = [h.howler.id for h in matching_hits if h.howler.id in case_item_values]
88
+ skipped_ids = [h.howler.id for h in matching_hits if h.howler.id not in case_item_values]
89
+
90
+ if skipped_ids:
91
+ report.append(
92
+ {
93
+ "query": f"howler.id:({' OR '.join(sanitize_lucene_query(h) for h in skipped_ids)})",
94
+ "outcome": "skipped",
95
+ "title": "Skipped Hits Not in Bundle",
96
+ "message": "These hits are not in the bundle.",
97
+ }
98
+ )
99
+
100
+ if not values_to_remove:
101
+ report.append(
102
+ {
103
+ "query": query,
104
+ "outcome": "skipped",
105
+ "title": "No Matching Hits",
106
+ "message": "None of the matching hits were found in the bundle.",
107
+ }
108
+ )
109
+ return report
110
+
111
+ # remove_case_items expects item UUIDs (CaseItem.id), not hit value strings.
112
+ value_set = set(values_to_remove)
113
+ item_ids_to_remove = [item.id for item in case.items if item.value in value_set]
114
+ case_service.remove_case_items(case, item_ids_to_remove)
115
+
116
+ report.append(
117
+ {
118
+ "query": query,
119
+ "outcome": "success",
120
+ "title": "Executed Successfully",
121
+ "message": f"Matching hits removed from bundle with id {bundle_id}",
122
+ }
123
+ )
124
+
125
+ except NotFoundException as e:
126
+ report.append(
127
+ {
128
+ "query": query,
129
+ "outcome": "error",
130
+ "title": "Failed to Execute",
131
+ "message": str(e),
132
+ }
133
+ )
134
+ except Exception as e: # pragma: no cover
135
+ report.append(
136
+ {
137
+ "query": query,
138
+ "outcome": "error",
139
+ "title": "Failed to Execute",
140
+ "message": f"Unknown exception occurred: {str(e)}",
141
+ }
142
+ )
143
+
144
+ return report
145
+
146
+
147
+ def specification():
148
+ """Specify various properties of the action, such as title, descriptions, permissions and input steps."""
149
+ return {
150
+ "id": OPERATION_ID,
151
+ "title": "Remove from Bundle (Deprecated)",
152
+ "priority": 5,
153
+ "i18nKey": f"operations.{OPERATION_ID}",
154
+ "description": {
155
+ "short": "Remove a set of hits from a bundle (deprecated — uses cases)",
156
+ "long": execute.__doc__,
157
+ },
158
+ "roles": ["automation_basic", "actionrunner_basic"],
159
+ "steps": [
160
+ {
161
+ "args": {"bundle_id": []},
162
+ "options": {},
163
+ }
164
+ ],
165
+ "triggers": VALID_TRIGGERS,
166
+ }
@@ -9,12 +9,12 @@ from howler.helper.workflow import Workflow, WorkflowException
9
9
  from howler.odm.models.action import VALID_TRIGGERS
10
10
  from howler.odm.models.howler_data import (
11
11
  Assessment,
12
- HitStatus,
13
12
  HitStatusTransition,
13
+ Status,
14
14
  Vote,
15
15
  )
16
16
  from howler.odm.models.user import User
17
- from howler.services import event_service, hit_service
17
+ from howler.services import comms_service, hit_service
18
18
  from howler.utils.list_utils import flatten_list
19
19
 
20
20
  OPERATION_ID = "transition"
@@ -147,7 +147,7 @@ def execute(
147
147
  if total_processed % 10 == 0:
148
148
  log.debug("Transition executed on %s hits", total_processed)
149
149
  if request_id is not None:
150
- event_service.emit(
150
+ comms_service.emit(
151
151
  "automation",
152
152
  {
153
153
  "request_id": request_id,
@@ -193,15 +193,13 @@ def specification():
193
193
  "steps": [
194
194
  {
195
195
  "args": {"status": []},
196
- "options": {"status": HitStatus.list()},
196
+ "options": {"status": Status.list()},
197
197
  "validation": {"error": {"query": "-howler.status:$status"}},
198
198
  },
199
199
  {
200
200
  "args": {"transition": []},
201
201
  "options": {
202
- "transition": {
203
- f"status:{status}": hit_service.get_transitions(status) for status in HitStatus.list()
204
- },
202
+ "transition": {f"status:{status}": hit_service.get_transitions(status) for status in Status.list()},
205
203
  },
206
204
  },
207
205
  {
@@ -25,7 +25,29 @@ logger = get_logger(__file__)
25
25
 
26
26
  def make_subapi_blueprint(name, api_version=1):
27
27
  """Create a flask Blueprint for a subapi in a standard way."""
28
- return Blueprint(name, name, url_prefix="/".join([API_PREFIX, f"v{api_version}", name]))
28
+ full_name = f"v{api_version}_{name}"
29
+ return Blueprint(full_name, full_name, url_prefix="/".join([API_PREFIX, f"v{api_version}", name]))
30
+
31
+
32
+ def _format_api_error_message(err: Exception) -> str:
33
+ """Format an exception for API responses using the innermost traceback frame only."""
34
+ trace = exc_info()[2]
35
+ trace_lines = format_tb(trace)
36
+ if trace_lines:
37
+ trace_lines = trace_lines[-1:]
38
+
39
+ return "".join(["\n"] + trace_lines + ["%s: %s\n" % (err.__class__.__name__, str(err))]).rstrip("\n")
40
+
41
+
42
+ def _coerce_response_data(data: Any) -> Any:
43
+ """Convert ODM model responses into JSON-serializable primitives."""
44
+ if isinstance(data, odm.Model):
45
+ return data.as_primitives()
46
+
47
+ if isinstance(data, list) and len(data) > 0 and isinstance(data[0], odm.Model):
48
+ return [item.as_primitives() for item in data]
49
+
50
+ return data
29
51
 
30
52
 
31
53
  def _make_api_response(
@@ -43,17 +65,12 @@ def _make_api_response(
43
65
  if quota_user and quota_set and not request.path.startswith("/api/v1/clue"):
44
66
  QUOTA_TRACKER.end(quota_user)
45
67
 
46
- if type(err) is Exception: # pragma: no cover
68
+ if isinstance(err, Exception): # pragma: no cover
47
69
  trace = exc_info()[2]
48
- err = "".join(["\n"] + format_tb(trace) + ["%s: %s\n" % (err.__class__.__name__, str(err))]).rstrip("\n")
70
+ err = _format_api_error_message(err)
49
71
  log_with_traceback(trace, "Exception", is_exception=True)
50
72
 
51
- if isinstance(data, odm.Model):
52
- data = data.as_primitives()
53
-
54
- if isinstance(data, list) and len(data) > 0 and isinstance(data[0], odm.Model):
55
- for i in range(len(data)):
56
- data[i] = data[i].as_primitives()
73
+ data = _coerce_response_data(data)
57
74
 
58
75
  resp = make_response(
59
76
  jsonify(