howler-api 3.2.0.dev585__tar.gz → 3.2.0.dev595__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 (211) hide show
  1. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/PKG-INFO +2 -2
  2. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/actions/demote.py +2 -2
  3. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/actions/transition.py +3 -5
  4. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/api/__init__.py +2 -1
  5. howler_api-3.2.0.dev595/howler/api/v1/__init__.py +44 -0
  6. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/api/v1/action.py +1 -2
  7. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/api/v1/analytic.py +17 -102
  8. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/api/v1/hit.py +11 -215
  9. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/api/v1/tool.py +1 -18
  10. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/api/v1/user.py +1 -1
  11. howler_api-3.2.0.dev595/howler/api/v2/__init__.py +44 -0
  12. howler_api-3.2.0.dev595/howler/api/v2/case.py +286 -0
  13. howler_api-3.2.0.dev595/howler/api/v2/ingest.py +327 -0
  14. howler_api-3.2.0.dev595/howler/api/v2/search.py +341 -0
  15. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/app.py +10 -0
  16. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/common/loader.py +4 -5
  17. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/datastore/collection.py +8 -9
  18. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/datastore/howler_store.py +29 -14
  19. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/datastore/store.py +4 -1
  20. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/datastore/types.py +1 -6
  21. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/helper/discover.py +11 -7
  22. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/helper/hit.py +4 -4
  23. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/helper/search.py +12 -2
  24. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/base.py +40 -12
  25. howler_api-3.2.0.dev595/howler/odm/constants.py +20 -0
  26. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/helper.py +115 -5
  27. howler_api-3.2.0.dev595/howler/odm/models/case.py +179 -0
  28. howler_api-3.2.0.dev595/howler/odm/models/hit.py +29 -0
  29. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/howler_data.py +3 -31
  30. howler_api-3.2.0.dev595/howler/odm/models/observable.py +129 -0
  31. howler_api-3.2.0.dev585/howler/odm/models/hit.py → howler_api-3.2.0.dev595/howler/odm/models/record.py +1 -21
  32. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/random_data.py +347 -45
  33. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/remote/datatypes/__init__.py +3 -2
  34. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/remote/datatypes/queues/comms.py +2 -1
  35. howler_api-3.2.0.dev595/howler/services/case_service.py +558 -0
  36. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/services/config_service.py +3 -3
  37. howler_api-3.2.0.dev585/howler/api/v1/__init__.py → howler_api-3.2.0.dev595/howler/services/docs_service.py +37 -45
  38. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/services/hit_service.py +90 -168
  39. howler_api-3.2.0.dev595/howler/services/observable_service.py +128 -0
  40. howler_api-3.2.0.dev595/howler/services/search_service.py +225 -0
  41. howler_api-3.2.0.dev595/howler/utils/compat.py +21 -0
  42. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/pyproject.toml +3 -3
  43. howler_api-3.2.0.dev585/howler/actions/add_to_bundle.py +0 -159
  44. howler_api-3.2.0.dev585/howler/actions/remove_from_bundle.py +0 -133
  45. howler_api-3.2.0.dev585/howler/cronjobs/rules.py +0 -274
  46. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/README.md +0 -0
  47. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/__init__.py +0 -0
  48. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/actions/__init__.py +0 -0
  49. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/actions/add_label.py +0 -0
  50. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/actions/change_field.py +0 -0
  51. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/actions/example_plugin.py +0 -0
  52. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/actions/prioritization.py +0 -0
  53. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/actions/promote.py +0 -0
  54. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/actions/remove_label.py +0 -0
  55. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/api/base.py +0 -0
  56. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/api/socket.py +0 -0
  57. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/api/v1/auth.py +0 -0
  58. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/api/v1/clue.py +0 -0
  59. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/api/v1/configs.py +0 -0
  60. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/api/v1/dossier.py +0 -0
  61. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/api/v1/help.py +0 -0
  62. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/api/v1/notebook.py +0 -0
  63. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/api/v1/overview.py +0 -0
  64. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/api/v1/search.py +0 -0
  65. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/api/v1/template.py +0 -0
  66. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/api/v1/utils/__init__.py +0 -0
  67. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/api/v1/utils/etag.py +0 -0
  68. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/api/v1/view.py +0 -0
  69. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/common/README.md +0 -0
  70. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/common/__init__.py +0 -0
  71. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/common/classification.py +0 -0
  72. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/common/classification.yml +0 -0
  73. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/common/exceptions.py +0 -0
  74. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/common/logging/__init__.py +0 -0
  75. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/common/logging/audit.py +0 -0
  76. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/common/logging/format.py +0 -0
  77. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/common/net.py +0 -0
  78. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/common/net_static.py +0 -0
  79. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/common/random_user.py +0 -0
  80. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/common/swagger.py +0 -0
  81. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/config.py +0 -0
  82. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/cronjobs/__init__.py +0 -0
  83. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/cronjobs/retention.py +0 -0
  84. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/cronjobs/view_cleanup.py +0 -0
  85. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/datastore/README.md +0 -0
  86. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/datastore/__init__.py +0 -0
  87. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/datastore/bulk.py +0 -0
  88. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/datastore/constants.py +0 -0
  89. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/datastore/exceptions.py +0 -0
  90. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/datastore/migrations/fix_process.py +0 -0
  91. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/datastore/operations.py +0 -0
  92. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/datastore/schemas.py +0 -0
  93. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/datastore/support/__init__.py +0 -0
  94. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/datastore/support/build.py +0 -0
  95. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/datastore/support/schemas.py +0 -0
  96. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/error.py +0 -0
  97. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/external/__init__.py +0 -0
  98. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/external/generate_mitre.py +0 -0
  99. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/external/generate_sigma_rules.py +0 -0
  100. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/external/generate_tlds.py +0 -0
  101. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/external/reindex_data.py +0 -0
  102. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/external/wipe_databases.py +0 -0
  103. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/gunicorn_config.py +0 -0
  104. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/healthz.py +0 -0
  105. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/helper/__init__.py +0 -0
  106. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/helper/azure.py +0 -0
  107. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/helper/oauth.py +0 -0
  108. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/helper/workflow.py +0 -0
  109. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/helper/ws.py +0 -0
  110. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/README.md +0 -0
  111. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/__init__.py +0 -0
  112. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/charter.txt +0 -0
  113. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/howler_enum.py +0 -0
  114. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/__init__.py +0 -0
  115. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/action.py +0 -0
  116. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/analytic.py +0 -0
  117. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/assemblyline.py +0 -0
  118. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/aws.py +0 -0
  119. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/azure.py +0 -0
  120. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/cbs.py +0 -0
  121. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/clue.py +0 -0
  122. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/config.py +0 -0
  123. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/dossier.py +0 -0
  124. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/__init__.py +0 -0
  125. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/agent.py +0 -0
  126. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/autonomous_system.py +0 -0
  127. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/client.py +0 -0
  128. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/cloud.py +0 -0
  129. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/code_signature.py +0 -0
  130. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/container.py +0 -0
  131. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/dns.py +0 -0
  132. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/egress.py +0 -0
  133. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/elf.py +0 -0
  134. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/email.py +0 -0
  135. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/error.py +0 -0
  136. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/event.py +0 -0
  137. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/faas.py +0 -0
  138. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/file.py +0 -0
  139. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/geo.py +0 -0
  140. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/group.py +0 -0
  141. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/hash.py +0 -0
  142. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/host.py +0 -0
  143. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/http.py +0 -0
  144. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/ingress.py +0 -0
  145. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/interface.py +0 -0
  146. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/network.py +0 -0
  147. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/observer.py +0 -0
  148. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/organization.py +0 -0
  149. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/os.py +0 -0
  150. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/pe.py +0 -0
  151. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/process.py +0 -0
  152. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/registry.py +0 -0
  153. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/related.py +0 -0
  154. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/rule.py +0 -0
  155. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/server.py +0 -0
  156. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/threat.py +0 -0
  157. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/tls.py +0 -0
  158. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/url.py +0 -0
  159. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/user.py +0 -0
  160. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/user_agent.py +0 -0
  161. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/ecs/vulnerability.py +0 -0
  162. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/gcp.py +0 -0
  163. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/lead.py +0 -0
  164. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/localized_label.py +0 -0
  165. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/overview.py +0 -0
  166. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/pivot.py +0 -0
  167. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/template.py +0 -0
  168. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/user.py +0 -0
  169. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/models/view.py +0 -0
  170. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/odm/randomizer.py +0 -0
  171. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/patched.py +0 -0
  172. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/plugins/__init__.py +0 -0
  173. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/plugins/config.py +0 -0
  174. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/remote/__init__.py +0 -0
  175. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/remote/datatypes/README.md +0 -0
  176. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/remote/datatypes/counters.py +0 -0
  177. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/remote/datatypes/events.py +0 -0
  178. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/remote/datatypes/hash.py +0 -0
  179. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/remote/datatypes/lock.py +0 -0
  180. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/remote/datatypes/queues/__init__.py +0 -0
  181. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/remote/datatypes/queues/multi.py +0 -0
  182. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/remote/datatypes/queues/named.py +0 -0
  183. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/remote/datatypes/queues/priority.py +0 -0
  184. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/remote/datatypes/set.py +0 -0
  185. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/remote/datatypes/user_quota_tracker.py +0 -0
  186. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/security/__init__.py +0 -0
  187. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/security/socket.py +0 -0
  188. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/security/utils.py +0 -0
  189. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/services/__init__.py +0 -0
  190. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/services/action_service.py +0 -0
  191. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/services/analytic_service.py +0 -0
  192. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/services/auth_service.py +0 -0
  193. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/services/dossier_service.py +0 -0
  194. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/services/event_service.py +0 -0
  195. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/services/jwt_service.py +0 -0
  196. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/services/lucene_service.py +0 -0
  197. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/services/notebook_service.py +0 -0
  198. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/services/overview_service.py +0 -0
  199. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/services/template_service.py +0 -0
  200. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/services/user_service.py +0 -0
  201. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/utils/__init__.py +0 -0
  202. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/utils/annotations.py +0 -0
  203. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/utils/chunk.py +0 -0
  204. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/utils/dict_utils.py +0 -0
  205. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/utils/isotime.py +0 -0
  206. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/utils/list_utils.py +0 -0
  207. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/utils/lucene.py +0 -0
  208. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/utils/path.py +0 -0
  209. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/utils/socket_utils.py +0 -0
  210. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/utils/str_utils.py +0 -0
  211. {howler_api-3.2.0.dev585 → howler_api-3.2.0.dev595}/howler/utils/uid.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: howler-api
3
- Version: 3.2.0.dev585
3
+ Version: 3.2.0.dev595
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.3.1)
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)
@@ -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
@@ -96,7 +96,7 @@ def execute(
96
96
  "howler.assignment",
97
97
  user.get("uname", "automation") if user else "automation",
98
98
  ),
99
- odm_helper.update("howler.status", HitStatus.RESOLVED),
99
+ odm_helper.update("howler.status", Status.RESOLVED),
100
100
  ],
101
101
  )
102
102
 
@@ -8,8 +8,8 @@ from howler.helper.workflow import Workflow, WorkflowException
8
8
  from howler.odm.models.action import VALID_TRIGGERS
9
9
  from howler.odm.models.howler_data import (
10
10
  Assessment,
11
- HitStatus,
12
11
  HitStatusTransition,
12
+ Status,
13
13
  Vote,
14
14
  )
15
15
  from howler.odm.models.user import User
@@ -180,15 +180,13 @@ def specification():
180
180
  "steps": [
181
181
  {
182
182
  "args": {"status": []},
183
- "options": {"status": HitStatus.list()},
183
+ "options": {"status": Status.list()},
184
184
  "validation": {"error": {"query": "-howler.status:$status"}},
185
185
  },
186
186
  {
187
187
  "args": {"transition": []},
188
188
  "options": {
189
- "transition": {
190
- f"status:{status}": hit_service.get_transitions(status) for status in HitStatus.list()
191
- },
189
+ "transition": {f"status:{status}": hit_service.get_transitions(status) for status in Status.list()},
192
190
  },
193
191
  },
194
192
  {
@@ -24,7 +24,8 @@ logger = get_logger(__file__)
24
24
 
25
25
  def make_subapi_blueprint(name, api_version=1):
26
26
  """Create a flask Blueprint for a subapi in a standard way."""
27
- return Blueprint(name, name, url_prefix="/".join([API_PREFIX, f"v{api_version}", name]))
27
+ full_name = f"v{api_version}_{name}"
28
+ return Blueprint(full_name, full_name, url_prefix="/".join([API_PREFIX, f"v{api_version}", name]))
28
29
 
29
30
 
30
31
  def _make_api_response(
@@ -0,0 +1,44 @@
1
+ from flask import Blueprint
2
+
3
+ from howler.api import ok
4
+ from howler.security import api_login
5
+ from howler.services import docs_service
6
+
7
+ API_PREFIX = "/api/v1"
8
+ apiv1 = Blueprint("apiv1", __name__, url_prefix=API_PREFIX)
9
+ apiv1._doc = "Api Documentation Version 1" # type: ignore[attr-defined] # type: ignore
10
+
11
+
12
+ @apiv1.route("/")
13
+ @api_login(audit=False, required_priv=["R", "W"], required_type=["user", "admin"])
14
+ def get_api_documentation(**kwargs):
15
+ """Full API doc.
16
+
17
+ Loop through all registered API paths and display their documentation.
18
+ Returns a list of API definition.
19
+
20
+ Variables:
21
+ None
22
+
23
+ Arguments:
24
+ None
25
+
26
+ Result Example:
27
+ [
28
+ {
29
+ 'name': "Api Doc", # Name of the api
30
+ 'path': "/api/path/<variable>/", # API path
31
+ 'ui_only': false, # Is UI only API
32
+ 'methods': ["GET", "POST"], # Allowed HTTP methods
33
+ 'description': "API doc.", # API documentation
34
+ 'id': "api_doc", # Unique ID for the API
35
+ 'function': "apiv1.api_doc", # Function called in the code
36
+ 'protected': False, # Does the API require login?
37
+ 'required_type': ['user'], # Type of users allowed to use API
38
+ 'complete' : True # Is the API stable?
39
+ },
40
+ ]
41
+ """
42
+ user_types = kwargs["user"]["type"]
43
+
44
+ return ok(docs_service.build_route_docs("v1", user_types))
@@ -231,8 +231,7 @@ def execute_action(id: str, **kwargs) -> Response:
231
231
  if not isinstance(execute_req, dict):
232
232
  return bad_request(err="Incorrect data structure!")
233
233
 
234
- action: Action = datastore().action.get(id)
235
-
234
+ action = datastore().action.get(id)
236
235
  if not action:
237
236
  return not_found(err="The specified action does not exist")
238
237
 
@@ -15,11 +15,9 @@ from howler.common.exceptions import HowlerException
15
15
  from howler.common.loader import datastore
16
16
  from howler.common.logging import get_logger
17
17
  from howler.common.swagger import generate_swagger_docs
18
- from howler.cronjobs.rules import register_rules
19
18
  from howler.datastore.exceptions import DataStoreException
20
19
  from howler.datastore.operations import OdmHelper
21
20
  from howler.odm.models.analytic import Analytic, Comment, Notebook, TriageOptions
22
- from howler.odm.models.template import Template
23
21
  from howler.odm.models.user import User
24
22
  from howler.security import api_login
25
23
  from howler.services import analytic_service, user_service
@@ -104,7 +102,8 @@ def update_analytic(id: str, user: User, **kwargs):
104
102
  """
105
103
  storage = datastore()
106
104
 
107
- if not storage.analytic.exists(id):
105
+ existing_analytic = storage.analytic.get(id)
106
+ if not existing_analytic:
108
107
  return not_found(err="This analytic does not exist")
109
108
 
110
109
  new_data = request.json
@@ -113,8 +112,6 @@ def update_analytic(id: str, user: User, **kwargs):
113
112
  return bad_request(err="You must provide updated data.")
114
113
 
115
114
  try:
116
- existing_analytic: Analytic = storage.analytic.get_if_exists(id)
117
-
118
115
  existing_analytic.description = new_data.get("description", existing_analytic.description)
119
116
 
120
117
  if existing_analytic.triage_settings is not None:
@@ -126,21 +123,8 @@ def update_analytic(id: str, user: User, **kwargs):
126
123
  {**existing_triage_data, **new_data.get("triage_settings", {})}
127
124
  )
128
125
 
129
- updated_rule = False
130
- if existing_analytic.rule_type:
131
- updated_rule = existing_analytic.rule != new_data.get(
132
- "rule", existing_analytic.rule
133
- ) or existing_analytic.rule_crontab != new_data.get("rule_crontab", existing_analytic.rule_crontab)
134
-
135
- existing_analytic.rule = new_data.get("rule", existing_analytic.rule)
136
- existing_analytic.rule_crontab = new_data.get("rule_crontab", existing_analytic.rule_crontab)
137
-
138
126
  storage.analytic.save(existing_analytic.analytic_id, existing_analytic)
139
127
 
140
- if updated_rule:
141
- # The registration process automatically deletes and resets the rule cronjob
142
- register_rules(existing_analytic)
143
-
144
128
  return ok(existing_analytic)
145
129
  except HowlerException as e:
146
130
  return bad_request(err=str(e))
@@ -169,62 +153,7 @@ def create_rule(user: User, **kwargs):
169
153
  ...analytic # The created analytic rule
170
154
  }
171
155
  """
172
- storage = datastore()
173
-
174
- new_data: Optional[dict[str, Any]] = request.json
175
-
176
- if not new_data:
177
- return bad_request(err="You must provide rule data.")
178
-
179
- required_keys = {
180
- "name",
181
- "description",
182
- "rule",
183
- "rule_type",
184
- "rule_crontab",
185
- }
186
-
187
- for key in required_keys:
188
- if key not in new_data or not new_data[key]:
189
- return bad_request(err=f"You must provide a {key} for your rule.")
190
-
191
- extra_keys = set(new_data.keys()) - required_keys
192
-
193
- if len(extra_keys) > 0:
194
- return bad_request(err=f"Additional fields ({', '.join(extra_keys)}) are not permitted.")
195
-
196
- new_analytic = Analytic(
197
- {
198
- **new_data,
199
- "tags": ["rule"],
200
- "owner": user["uname"],
201
- "contributors": [user["uname"]],
202
- "detections": ["Rule"],
203
- }
204
- )
205
-
206
- new_template = Template(
207
- {
208
- "analytic": new_data["name"],
209
- "detection": "Rule",
210
- "type": "global",
211
- "owner": user["uname"],
212
- # TODO: Allow custom keys
213
- "keys": ["event.kind", "event.module", "event.reason", "event.type"],
214
- }
215
- )
216
-
217
- try:
218
- storage.analytic.save(new_analytic.analytic_id, new_analytic)
219
- # Have to commit so the analytic is available during registration
220
- storage.analytic.commit()
221
- register_rules(new_analytic)
222
-
223
- storage.template.save(new_template.template_id, new_template)
224
-
225
- return ok(new_analytic)
226
- except HowlerException as e:
227
- return bad_request(err=str(e))
156
+ raise NotImplementedError()
228
157
 
229
158
 
230
159
  @generate_swagger_docs()
@@ -248,23 +177,7 @@ def delete_rule(id: str, user: User, **kwargs):
248
177
  {
249
178
  }
250
179
  """
251
- if not analytic_service.does_analytic_exist(id):
252
- return not_found(err=f"Analytic {id} does not exist")
253
-
254
- analytic = analytic_service.get_analytic(id, as_odm=True)
255
-
256
- if not analytic.rule:
257
- return bad_request(err="This is not a rule analytic, and cannot be deleted.")
258
-
259
- if user["uname"] != analytic.owner and "admin" not in user["type"]:
260
- return forbidden(err="You cannot delete this analytic.")
261
-
262
- try:
263
- datastore().analytic.delete(analytic.analytic_id)
264
- except DataStoreException as e:
265
- return bad_request(err=str(e))
266
-
267
- return no_content()
180
+ raise NotImplementedError()
268
181
 
269
182
 
270
183
  @generate_swagger_docs()
@@ -557,7 +470,7 @@ def set_analytic_owner(id: str, user: dict[str, Any], **kwargs):
557
470
  @generate_swagger_docs()
558
471
  @analytic_api.route("/<id>/favourite", methods=["POST"])
559
472
  @api_login(required_priv=["R", "W"])
560
- def set_as_favourite(id, **kwargs):
473
+ def set_as_favourite(id: str, user: User | None, **kwargs):
561
474
  """Add an analytic to a list of the user's favourites
562
475
 
563
476
  Variables:
@@ -576,16 +489,17 @@ def set_as_favourite(id, **kwargs):
576
489
  """
577
490
  storage = datastore()
578
491
 
579
- existing_analytic: Analytic = storage.analytic.get_if_exists(id)
492
+ existing_analytic = storage.analytic.get(id)
580
493
  if not existing_analytic:
581
494
  return not_found(err="This analytic does not exist")
582
495
 
583
- try:
584
- current_user = storage.user.get_if_exists(kwargs["user"]["uname"])
496
+ if not user:
497
+ return forbidden(err="User was not found.")
585
498
 
586
- current_user.favourite_analytics.append(id)
499
+ try:
500
+ user.favourite_analytics.append(id)
587
501
 
588
- storage.user.save(current_user["uname"], current_user)
502
+ storage.user.save(user.uname, user)
589
503
 
590
504
  return ok()
591
505
  except ValueError as e:
@@ -595,7 +509,7 @@ def set_as_favourite(id, **kwargs):
595
509
  @generate_swagger_docs()
596
510
  @analytic_api.route("/<id>/favourite", methods=["DELETE"])
597
511
  @api_login(required_priv=["R", "W"])
598
- def remove_as_favourite(id, **kwargs):
512
+ def remove_as_favourite(id: str, user: User | None, **kwargs):
599
513
  """Remove an analytic from a list of the user's favourites
600
514
 
601
515
  Variables:
@@ -614,12 +528,13 @@ def remove_as_favourite(id, **kwargs):
614
528
  if not storage.analytic.exists(id):
615
529
  return not_found(err="This analytic does not exist")
616
530
 
617
- try:
618
- current_user = storage.user.get_if_exists(kwargs["user"]["uname"])
531
+ if not user:
532
+ return forbidden(err="User was not found.")
619
533
 
620
- current_user["favourite_analytics"] = list(filter(lambda f: f != id, current_user.favourite_analytics))
534
+ try:
535
+ user.favourite_analytics = list(filter(lambda f: f != id, user.favourite_analytics))
621
536
 
622
- storage.user.save(current_user["uname"], current_user)
537
+ storage.user.save(user.uname, user)
623
538
 
624
539
  return no_content()
625
540
  except ValueError as e:
@@ -147,11 +147,9 @@ def delete_hits(user: User, **kwargs):
147
147
  None
148
148
 
149
149
  Data Block:
150
- {
151
- [
152
- hitId, hitId, hitId
153
- ]
154
- }
150
+ [
151
+ hitId, hitId, hitId
152
+ ]
155
153
 
156
154
  Result Example:
157
155
  {
@@ -166,17 +164,11 @@ def delete_hits(user: User, **kwargs):
166
164
  if "admin" not in user["type"]:
167
165
  return forbidden(err="Cannot delete hit, only admin is allowed to delete")
168
166
 
167
+ hit_ids = list(set(hit_ids))
169
168
  non_existing_hit_ids = [hit_id for hit_id in hit_ids if not hit_service.exists(hit_id)]
170
169
 
171
- if len(non_existing_hit_ids) == 1:
172
- return not_found(err=f"Hit id {non_existing_hit_ids[0]} does not exist.")
173
-
174
- if len(non_existing_hit_ids) > 1:
175
- return not_found(err=f"Hit ids {', '.join(non_existing_hit_ids)} do not exist.")
176
-
177
- for hit_id in hit_ids:
178
- if not hit_service.exists(hit_id):
179
- return not_found(err=f"Hit id {hit_id} does not exist.")
170
+ if non_existing_hit_ids:
171
+ return not_found(err=f"Hit id(s) {', '.join(non_existing_hit_ids)} do not exist.")
180
172
 
181
173
  hit_service.delete_hits(hit_ids)
182
174
 
@@ -247,7 +239,7 @@ def validate_hits(**kwargs):
247
239
 
248
240
  @generate_swagger_docs()
249
241
  @hit_api.route("/<id>", methods=["GET"])
250
- @api_login(audit=False, required_priv=["R"])
242
+ @api_login(audit=True, required_priv=["R"])
251
243
  @add_etag(getter=hit_service.get_hit)
252
244
  def get_hit(id: str, server_version: str, **kwargs):
253
245
  """Get a hit.
@@ -522,7 +514,7 @@ def add_label(id, label_set, user, **kwargs):
522
514
  "success": True # Adding the label succeeded
523
515
  }
524
516
  """
525
- if not hit_service.does_hit_exist(id):
517
+ if not hit_service.exists(id):
526
518
  return not_found(err=f"Hit {id} does not exist")
527
519
 
528
520
  existing_hit: Hit = hit_service.get_hit(id, as_odm=True)
@@ -586,7 +578,7 @@ def remove_labels(id, label_set, user, **kwargs):
586
578
  "success": True # Removing the labels succeeded
587
579
  }
588
580
  """
589
- if not hit_service.does_hit_exist(id):
581
+ if not hit_service.exists(id):
590
582
  return not_found(err=f"Hit {id} does not exist")
591
583
 
592
584
  if f"howler.labels.{label_set}" not in hit_service.get_hit(id, as_odm=True).flat_fields():
@@ -803,7 +795,7 @@ def edit_comment(id: str, comment_id: str, user: dict[str, Any], **kwargs):
803
795
  if len(comment_value) > MAX_COMMENT_LEN:
804
796
  return bad_request(err="Comment is too long.")
805
797
 
806
- if not hit_service.does_hit_exist(id):
798
+ if not hit_service.exists(id):
807
799
  return not_found(err=f"Hit {id} does not exist")
808
800
 
809
801
  hit: Hit = kwargs["cached_hit"]
@@ -864,7 +856,7 @@ def delete_comments(id: str, user: User, **kwargs):
864
856
  ...hit # The new data for the hit
865
857
  }
866
858
  """
867
- if not hit_service.does_hit_exist(id):
859
+ if not hit_service.exists(id):
868
860
  return not_found(err=f"Hit {id} does not exist")
869
861
 
870
862
  comment_ids: list[str] = request.json or []
@@ -984,199 +976,3 @@ def remove_react_comment(id: str, comment_id: str, user: dict[str, Any], **kwarg
984
976
  new_hit, version = hit_service.save_hit(hit, version=kwargs.get("server_version"))
985
977
 
986
978
  return ok(new_hit), version
987
-
988
-
989
- @generate_swagger_docs()
990
- @hit_api.route("/bundle", methods=["POST"])
991
- @api_login(audit=False, required_priv=["W"])
992
- def create_bundle(user: User, **kwargs):
993
- """Create a new bundle
994
-
995
- Variables:
996
- None
997
-
998
- Arguments:
999
- None
1000
-
1001
- Data Block:
1002
- {
1003
- "bundle": {
1004
- ...hit # A howler hit that will be used as a template for this new bundle
1005
- },
1006
- "hits": [...ids] # A list of existing howler hits to add as children to the new bundle
1007
- }
1008
-
1009
- Result Example:
1010
- {
1011
- ...hit # The created bundle
1012
- }
1013
- """
1014
- data = request.json
1015
- if not isinstance(data, dict):
1016
- return bad_request(err="Invalid data format")
1017
-
1018
- bundle_hit: Optional[dict[str, Any]] = data.get("bundle")
1019
-
1020
- if bundle_hit is None:
1021
- return bad_request(err="You did not provide a bundle hit.")
1022
-
1023
- try:
1024
- odm, _ = hit_service.convert_hit(bundle_hit, unique=True)
1025
- odm.howler.is_bundle = True
1026
-
1027
- child_hits = data.get("hits", [])
1028
-
1029
- if len(odm.howler.hits) < 1 and len(child_hits) < 1:
1030
- return bad_request(err="You did not provide any child hits.")
1031
-
1032
- for hit_id in child_hits:
1033
- if hit_id not in odm.howler.hits:
1034
- odm.howler.hits.append(hit_id)
1035
-
1036
- hit_service.create_hit(odm.howler.id, odm, user=user.uname)
1037
- analytic_service.save_from_hit(odm, user)
1038
-
1039
- for hit_id in odm.howler.hits:
1040
- child_hit: Hit = hit_service.get_hit(hit_id, as_odm=True)
1041
-
1042
- if child_hit.howler.is_bundle:
1043
- return bad_request(
1044
- err=f"You cannot specify a bundle as a child of another bundle - {child_hit.howler.id} is a bundle."
1045
- )
1046
-
1047
- child_hit.howler.bundles.append(odm.howler.id)
1048
- datastore().hit.save(child_hit.howler.id, child_hit)
1049
-
1050
- return created(odm)
1051
- except HowlerException as e:
1052
- return bad_request(err=str(e))
1053
-
1054
-
1055
- @generate_swagger_docs()
1056
- @hit_api.route("/bundle/<id>", methods=["PUT"])
1057
- @api_login(audit=False, required_priv=["W"])
1058
- @add_etag(getter=hit_service.get_hit, check_if_match=False)
1059
- def update_bundle(id, **kwargs):
1060
- """Update a hit's child hits. Can be used to convert an existing hit into a bundle, or to update an existing bundle.
1061
-
1062
- Variables:
1063
- id => The ID of the bundle to update
1064
-
1065
- Arguments:
1066
- None
1067
-
1068
- Data Block:
1069
- [
1070
- ...ids
1071
- ]
1072
-
1073
- Result Example:
1074
- {
1075
- ...hit # The updated bundle
1076
- }
1077
- """
1078
- bundle_hit: Hit = cast(Hit, kwargs.get("cached_hit", None))
1079
- if not bundle_hit:
1080
- return not_found(err="This bundle does not exist.")
1081
-
1082
- hit_ids = request.json
1083
- if not isinstance(hit_ids, list):
1084
- return bad_request(err="Invalid data format")
1085
-
1086
- new_hit_list = bundle_hit.howler.as_primitives().get("hits", [])
1087
- if bundle_hit.howler.is_bundle:
1088
- for hit_id in hit_ids:
1089
- if hit_id not in new_hit_list:
1090
- new_hit_list.append(hit_id)
1091
- else:
1092
- return conflict(err=f"The hit {hit_id} is already in the bundle {bundle_hit.howler.id}.")
1093
- else:
1094
- new_hit_list = hit_ids
1095
-
1096
- bundle_hit.howler.hits = new_hit_list
1097
- bundle_hit.howler.is_bundle = True
1098
-
1099
- try:
1100
- for hit_id in new_hit_list:
1101
- child_hit: Hit = hit_service.get_hit(hit_id, as_odm=True)
1102
-
1103
- if child_hit.howler.is_bundle:
1104
- return bad_request(
1105
- err=f"You cannot specify a bundle as a child of another bundle - {child_hit.howler.id} is a bundle."
1106
- )
1107
-
1108
- new_bundle_list = child_hit.howler.as_primitives().get("bundles", [])
1109
- new_bundle_list.append(bundle_hit.howler.id)
1110
- child_hit.howler.bundles = new_bundle_list
1111
- datastore().hit.save(child_hit.howler.id, child_hit)
1112
-
1113
- datastore().hit.save(bundle_hit.howler.id, bundle_hit)
1114
-
1115
- return ok(bundle_hit)
1116
- except HowlerException as e:
1117
- return bad_request(err=str(e))
1118
-
1119
-
1120
- @generate_swagger_docs()
1121
- @hit_api.route("/bundle/<id>", methods=["DELETE"])
1122
- @api_login(audit=False, required_priv=["W"])
1123
- @add_etag(getter=hit_service.get_hit, check_if_match=False)
1124
- def remove_bundle_children(id, **kwargs):
1125
- """Remove a bundle's child hits.
1126
-
1127
- Can be used to convert an existing bundle back into a normal hit, or to remove a subset of
1128
- existing hits from the bundle.
1129
-
1130
- Variables:
1131
- id => The ID of the bundle to update
1132
-
1133
- Arguments:
1134
- None
1135
-
1136
- Data Block:
1137
- [
1138
- ...ids OR '*' # A list of ids to remove, or a single '*' to remove all
1139
- ]
1140
-
1141
- Result Example:
1142
- {
1143
- ...hit # The updated hit
1144
- }
1145
- """
1146
- bundle_hit = kwargs.get("cached_hit", None)
1147
- if not bundle_hit:
1148
- return not_found(err="This bundle does not exist.")
1149
-
1150
- hit_ids = request.json
1151
- if not isinstance(hit_ids, list):
1152
- return bad_request(err="Invalid data format")
1153
-
1154
- new_hit_list = bundle_hit.howler.get("hits", [])
1155
- if bundle_hit.howler.is_bundle:
1156
- if hit_ids == ["*"]:
1157
- hit_ids = new_hit_list
1158
- new_hit_list = []
1159
- else:
1160
- new_hit_list = [_id for _id in new_hit_list if _id not in hit_ids]
1161
- else:
1162
- return bad_request(err="The specified hit must be a bundle.")
1163
-
1164
- bundle_hit.howler.hits = new_hit_list
1165
- bundle_hit.howler.is_bundle = len(new_hit_list) > 0
1166
-
1167
- try:
1168
- for hit_id in hit_ids:
1169
- child_hit: Hit = hit_service.get_hit(hit_id, as_odm=True)
1170
-
1171
- try:
1172
- child_hit.howler.bundles.remove(bundle_hit.howler.id)
1173
- except ValueError:
1174
- logger.warning("Bundle isn't included in child %s!", bundle_hit.howler.id)
1175
-
1176
- datastore().hit.save(child_hit.howler.id, child_hit)
1177
-
1178
- datastore().hit.save(bundle_hit.howler.id, bundle_hit)
1179
-
1180
- return ok(bundle_hit)
1181
- except HowlerException as e:
1182
- return bad_request(err=str(e))
@@ -94,7 +94,6 @@ def create_one_or_many_hits(tool_name: str, user: User, **kwargs): # noqa: C901
94
94
 
95
95
  out: list[dict[str, Any]] = []
96
96
  odms = []
97
- bundle_hit: Optional[Hit] = None
98
97
  for hit in hits:
99
98
  cur_id = get_random_id()
100
99
  cur_time = now_as_iso()
@@ -138,12 +137,7 @@ def create_one_or_many_hits(tool_name: str, user: User, **kwargs): # noqa: C901
138
137
  try:
139
138
  odm, warns = hit_service.convert_hit(obj, unique=True, ignore_extra_values=ignore_extra_values)
140
139
 
141
- if odm.howler.is_bundle and bundle_hit is None:
142
- bundle_hit = odm
143
- elif odm.howler.is_bundle:
144
- return bad_request(err="You can only specify one bundle hit!")
145
- else:
146
- odms.append(odm)
140
+ odms.append(odm)
147
141
 
148
142
  out.append(
149
143
  {
@@ -162,20 +156,9 @@ def create_one_or_many_hits(tool_name: str, user: User, **kwargs): # noqa: C901
162
156
  return bad_request(out, warnings=warnings, err="No valid hits were provided")
163
157
  else:
164
158
  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
-
170
159
  hit_service.create_hit(odm.howler.id, odm, user=user.uname)
171
-
172
160
  analytic_service.save_from_hit(odm, user)
173
161
 
174
- if bundle_hit:
175
- hit_service.create_hit(bundle_hit.howler.id, bundle_hit, user=user.uname)
176
-
177
- analytic_service.save_from_hit(bundle_hit, user)
178
-
179
162
  datastore().hit.commit()
180
163
 
181
164
  action_service.bulk_execute_on_query(f"howler.id:({' OR '.join(entry['id'] for entry in out)})", user=user)
@@ -321,7 +321,7 @@ def get_user_avatar(username, **_):
321
321
  "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD..."
322
322
  """
323
323
  storage = datastore()
324
- avatar: str = storage.user_avatar.get(username)
324
+ avatar = storage.user_avatar.get(username)
325
325
 
326
326
  if avatar:
327
327
  resp = ok(avatar)