howler-api 4.0.0.dev1176__tar.gz → 4.0.0.dev1182__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 (223) hide show
  1. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/PKG-INFO +1 -1
  2. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/actions/add_to_bundle.py +10 -6
  3. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/actions/add_to_case.py +15 -10
  4. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/actions/remove_from_bundle.py +11 -7
  5. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v2/case.py +49 -21
  6. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/datastore/collection.py +7 -4
  7. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/mixins.py +3 -3
  8. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/case.py +28 -5
  9. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/random_data.py +247 -129
  10. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/analytic_service.py +6 -2
  11. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/bundle_compat_service.py +27 -26
  12. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/case_service.py +378 -299
  13. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/correlation_service.py +16 -3
  14. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/dossier_service.py +11 -2
  15. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/hit_service.py +6 -2
  16. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/user_service.py +7 -1
  17. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/pyproject.toml +1 -1
  18. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/README.md +0 -0
  19. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/__init__.py +0 -0
  20. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/actions/__init__.py +0 -0
  21. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/actions/add_label.py +0 -0
  22. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/actions/change_field.py +0 -0
  23. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/actions/demote.py +0 -0
  24. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/actions/example_plugin.py +0 -0
  25. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/actions/prioritization.py +0 -0
  26. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/actions/promote.py +0 -0
  27. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/actions/remove_label.py +0 -0
  28. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/actions/transition.py +0 -0
  29. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/__init__.py +0 -0
  30. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/base.py +0 -0
  31. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/socket.py +0 -0
  32. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/__init__.py +0 -0
  33. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/action.py +0 -0
  34. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/analytic.py +0 -0
  35. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/auth.py +0 -0
  36. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/clue.py +0 -0
  37. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/configs.py +0 -0
  38. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/dossier.py +0 -0
  39. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/help.py +0 -0
  40. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/hit.py +0 -0
  41. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/notebook.py +0 -0
  42. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/overview.py +0 -0
  43. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/search.py +0 -0
  44. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/template.py +0 -0
  45. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/tool.py +0 -0
  46. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/user.py +0 -0
  47. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/utils/__init__.py +0 -0
  48. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/utils/etag.py +0 -0
  49. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/utils/params.py +0 -0
  50. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/view.py +0 -0
  51. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v2/__init__.py +0 -0
  52. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v2/fuzzy.py +0 -0
  53. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v2/ingest.py +0 -0
  54. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v2/search.py +0 -0
  55. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/app.py +0 -0
  56. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/common/README.md +0 -0
  57. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/common/__init__.py +0 -0
  58. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/common/classification.py +0 -0
  59. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/common/classification.yml +0 -0
  60. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/common/exceptions.py +0 -0
  61. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/common/loader.py +0 -0
  62. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/common/logging/__init__.py +0 -0
  63. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/common/logging/audit.py +0 -0
  64. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/common/logging/format.py +0 -0
  65. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/common/net.py +0 -0
  66. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/common/net_static.py +0 -0
  67. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/common/random_user.py +0 -0
  68. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/common/swagger.py +0 -0
  69. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/config.py +0 -0
  70. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/cronjobs/__init__.py +0 -0
  71. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/cronjobs/action_queue_worker.py +0 -0
  72. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/cronjobs/correlation.py +0 -0
  73. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/cronjobs/retention.py +0 -0
  74. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/cronjobs/view_cleanup.py +0 -0
  75. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/datastore/README.md +0 -0
  76. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/datastore/__init__.py +0 -0
  77. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/datastore/bulk.py +0 -0
  78. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/datastore/constants.py +0 -0
  79. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/datastore/exceptions.py +0 -0
  80. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/datastore/howler_store.py +0 -0
  81. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/datastore/migrations/fix_process.py +0 -0
  82. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/datastore/operations.py +0 -0
  83. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/datastore/schemas.py +0 -0
  84. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/datastore/store.py +0 -0
  85. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/datastore/support/__init__.py +0 -0
  86. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/datastore/support/build.py +0 -0
  87. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/datastore/support/schemas.py +0 -0
  88. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/datastore/types.py +0 -0
  89. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/error.py +0 -0
  90. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/external/README.md +0 -0
  91. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/external/__init__.py +0 -0
  92. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/external/generate_mitre.py +0 -0
  93. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/external/generate_sigma_rules.py +0 -0
  94. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/external/generate_tlds.py +0 -0
  95. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/external/reindex_data.py +0 -0
  96. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/external/wipe_databases.py +0 -0
  97. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/gunicorn_config.py +0 -0
  98. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/healthz.py +0 -0
  99. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/helper/__init__.py +0 -0
  100. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/helper/azure.py +0 -0
  101. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/helper/discover.py +0 -0
  102. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/helper/hit.py +0 -0
  103. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/helper/oauth.py +0 -0
  104. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/helper/search.py +0 -0
  105. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/helper/workflow.py +0 -0
  106. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/helper/ws.py +0 -0
  107. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/README.md +0 -0
  108. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/__init__.py +0 -0
  109. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/base.py +0 -0
  110. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/charter.txt +0 -0
  111. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/constants.py +0 -0
  112. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/helper.py +0 -0
  113. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/howler_enum.py +0 -0
  114. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/__init__.py +0 -0
  115. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/action.py +0 -0
  116. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/analytic.py +0 -0
  117. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/assemblyline.py +0 -0
  118. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/aws.py +0 -0
  119. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/azure.py +0 -0
  120. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/cbs.py +0 -0
  121. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/clue.py +0 -0
  122. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/config.py +0 -0
  123. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/dossier.py +0 -0
  124. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/__init__.py +0 -0
  125. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/agent.py +0 -0
  126. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/autonomous_system.py +0 -0
  127. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/client.py +0 -0
  128. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/cloud.py +0 -0
  129. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/code_signature.py +0 -0
  130. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/container.py +0 -0
  131. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/dns.py +0 -0
  132. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/egress.py +0 -0
  133. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/elf.py +0 -0
  134. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/email.py +0 -0
  135. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/error.py +0 -0
  136. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/event.py +0 -0
  137. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/faas.py +0 -0
  138. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/file.py +0 -0
  139. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/geo.py +0 -0
  140. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/group.py +0 -0
  141. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/hash.py +0 -0
  142. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/host.py +0 -0
  143. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/http.py +0 -0
  144. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/ingress.py +0 -0
  145. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/interface.py +0 -0
  146. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/network.py +0 -0
  147. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/observer.py +0 -0
  148. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/organization.py +0 -0
  149. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/os.py +0 -0
  150. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/pe.py +0 -0
  151. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/process.py +0 -0
  152. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/registry.py +0 -0
  153. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/related.py +0 -0
  154. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/rule.py +0 -0
  155. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/server.py +0 -0
  156. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/threat.py +0 -0
  157. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/tls.py +0 -0
  158. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/url.py +0 -0
  159. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/user.py +0 -0
  160. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/user_agent.py +0 -0
  161. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/vulnerability.py +0 -0
  162. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/event.py +0 -0
  163. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/gcp.py +0 -0
  164. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/hit.py +0 -0
  165. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/howler_data.py +0 -0
  166. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/lead.py +0 -0
  167. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/localized_label.py +0 -0
  168. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/overview.py +0 -0
  169. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/pivot.py +0 -0
  170. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/record.py +0 -0
  171. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/template.py +0 -0
  172. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/user.py +0 -0
  173. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/view.py +0 -0
  174. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/randomizer.py +0 -0
  175. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/patched.py +0 -0
  176. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/plugins/__init__.py +0 -0
  177. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/plugins/config.py +0 -0
  178. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/remote/__init__.py +0 -0
  179. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/remote/datatypes/README.md +0 -0
  180. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/remote/datatypes/__init__.py +0 -0
  181. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/remote/datatypes/counters.py +0 -0
  182. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/remote/datatypes/events.py +0 -0
  183. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/remote/datatypes/hash.py +0 -0
  184. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/remote/datatypes/lock.py +0 -0
  185. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/remote/datatypes/queues/__init__.py +0 -0
  186. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/remote/datatypes/queues/comms.py +0 -0
  187. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/remote/datatypes/queues/multi.py +0 -0
  188. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/remote/datatypes/queues/named.py +0 -0
  189. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/remote/datatypes/queues/priority.py +0 -0
  190. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/remote/datatypes/set.py +0 -0
  191. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/remote/datatypes/user_quota_tracker.py +0 -0
  192. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/security/__init__.py +0 -0
  193. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/security/socket.py +0 -0
  194. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/security/utils.py +0 -0
  195. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/__init__.py +0 -0
  196. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/action_service.py +0 -0
  197. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/auth_service.py +0 -0
  198. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/comms_service.py +0 -0
  199. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/config_service.py +0 -0
  200. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/docs_service.py +0 -0
  201. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/event_service.py +0 -0
  202. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/fuzzy_service.py +0 -0
  203. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/jwt_service.py +0 -0
  204. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/lucene_service.py +0 -0
  205. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/notebook_service.py +0 -0
  206. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/overview_service.py +0 -0
  207. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/search_service.py +0 -0
  208. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/template_service.py +0 -0
  209. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/viewer_service.py +0 -0
  210. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/telemetry.py +0 -0
  211. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/utils/__init__.py +0 -0
  212. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/utils/annotations.py +0 -0
  213. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/utils/chunk.py +0 -0
  214. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/utils/compat.py +0 -0
  215. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/utils/constants.py +0 -0
  216. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/utils/dict_utils.py +0 -0
  217. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/utils/isotime.py +0 -0
  218. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/utils/list_utils.py +0 -0
  219. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/utils/lucene.py +0 -0
  220. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/utils/path.py +0 -0
  221. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/utils/socket_utils.py +0 -0
  222. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/utils/str_utils.py +0 -0
  223. {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/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.dev1176
3
+ Version: 4.0.0.dev1182
4
4
  Summary: Howler - API server
5
5
  License: MIT
6
6
  Keywords: howler,alerting,gc,canada,cse-cst,cse,cst,cyber,cccs
@@ -6,6 +6,7 @@ from howler.actions import check_hit_limit
6
6
  from howler.common.exceptions import NotFoundException
7
7
  from howler.common.loader import datastore
8
8
  from howler.odm.models.action import VALID_TRIGGERS
9
+ from howler.odm.models.case import CaseItemTypes
9
10
  from howler.odm.models.user import User
10
11
  from howler.services import bundle_compat_service, case_service
11
12
  from howler.utils.str_utils import sanitize_lucene_query
@@ -36,8 +37,8 @@ def execute(query: str, bundle_id: Optional[str] = None, user: Optional[User] =
36
37
  ]
37
38
 
38
39
  try:
39
- case_id = bundle_compat_service.find_case_for_bundle(bundle_id)
40
- if case_id is None:
40
+ case = bundle_compat_service.find_case_for_bundle(bundle_id)
41
+ if case is None:
41
42
  report.append(
42
43
  {
43
44
  "query": query,
@@ -69,16 +70,19 @@ def execute(query: str, bundle_id: Optional[str] = None, user: Optional[User] =
69
70
  )
70
71
  return report
71
72
 
73
+ folder = case_service.get_parent_from_path(case, "hits", create_if_missing=True)
74
+
72
75
  added = []
73
76
  skipped = []
74
77
  for hit in matching_hits:
75
- child_label = f"hits/{hit.howler.analytic} ({hit.howler.id})"
78
+ name = f"{hit.howler.analytic} ({hit.howler.id})"
76
79
  try:
77
80
  case_service.append_case_item(
78
- case_id,
79
- item_type="hit",
81
+ case,
82
+ item_type=CaseItemTypes.HIT,
83
+ item_name=name,
80
84
  item_value=hit.howler.id,
81
- item_path=child_label,
85
+ item_parent=folder.id if folder else None,
82
86
  )
83
87
  added.append(hit.howler.id)
84
88
  except Exception:
@@ -10,7 +10,7 @@ from howler.services import case_service
10
10
  OPERATION_ID = "add_to_case"
11
11
 
12
12
 
13
- def execute(
13
+ def execute( # noqa: C901
14
14
  query: str,
15
15
  case_id: Optional[str] = None,
16
16
  path: str = "related",
@@ -24,7 +24,7 @@ def execute(
24
24
  case_id (str): The ID of the case to add the alerts to.
25
25
  path (str): The path within the case at which to place the alerts. Defaults to "related".
26
26
  title_template (str): A Mustache-compatible template string used to generate each item's
27
- path suffix (title). The hit's fields are available as template variables.
27
+ name. The hit's fields are available as template variables.
28
28
  Defaults to "{{howler.analytic}} ({{howler.id}})".
29
29
  """
30
30
  if not case_id:
@@ -39,7 +39,8 @@ def execute(
39
39
 
40
40
  ds = datastore()
41
41
 
42
- if ds.case.get(case_id) is None:
42
+ case = ds.case.get(case_id)
43
+ if case is None:
43
44
  return [
44
45
  {
45
46
  "query": query,
@@ -65,19 +66,23 @@ def execute(
65
66
  skipped = []
66
67
  added = []
67
68
 
68
- normalized_path = path.rstrip("/")
69
-
70
69
  for hit in hits:
71
- hit_data = hit.as_primitives()
72
- title = chevron.render(title_template, hit_data)
73
- item_path = f"{normalized_path}/{title}" if normalized_path else title
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
74
76
 
75
77
  try:
78
+ parent = case_service.get_parent_from_path(case, path, create_if_missing=True)
79
+
76
80
  case_service.append_case_item(
77
- case_id,
81
+ case,
78
82
  item_type="hit",
79
83
  item_value=hit.howler.id,
80
- item_path=item_path,
84
+ item_name=name,
85
+ item_parent=parent.id if parent else None,
81
86
  )
82
87
  added.append(hit.howler.id)
83
88
  except InvalidDataException as e:
@@ -36,8 +36,8 @@ def execute(query: str, bundle_id: Optional[str] = None, user: Optional[User] =
36
36
  ]
37
37
 
38
38
  try:
39
- case_id = bundle_compat_service.find_case_for_bundle(bundle_id)
40
- if case_id is None:
39
+ case = bundle_compat_service.find_case_for_bundle(bundle_id)
40
+ if case is None:
41
41
  report.append(
42
42
  {
43
43
  "query": query,
@@ -69,19 +69,20 @@ def execute(query: str, bundle_id: Optional[str] = None, user: Optional[User] =
69
69
  )
70
70
  return report
71
71
 
72
- # Get the case to check which hits are actually in it
73
- case = ds.case.get(case_id)
74
- if case is None:
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
75
  report.append(
76
76
  {
77
77
  "query": query,
78
78
  "outcome": "error",
79
79
  "title": "Case Not Found",
80
- "message": f"Associated case {case_id} no longer exists.",
80
+ "message": f"Associated case {case.case_id} no longer exists.",
81
81
  }
82
82
  )
83
83
  return report
84
84
 
85
+ case = refreshed_case
85
86
  case_item_values = {item.value for item in case.items}
86
87
  values_to_remove = [h.howler.id for h in matching_hits if h.howler.id in case_item_values]
87
88
  skipped_ids = [h.howler.id for h in matching_hits if h.howler.id not in case_item_values]
@@ -107,7 +108,10 @@ def execute(query: str, bundle_id: Optional[str] = None, user: Optional[User] =
107
108
  )
108
109
  return report
109
110
 
110
- case_service.remove_case_items(case_id, values_to_remove)
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)
111
115
 
112
116
  report.append(
113
117
  {
@@ -7,7 +7,7 @@ from howler.common.loader import datastore
7
7
  from howler.common.logging import get_logger
8
8
  from howler.common.swagger import generate_swagger_docs
9
9
  from howler.datastore.exceptions import DataStoreException
10
- from howler.odm.models.case import CaseItem
10
+ from howler.odm.models.case import Case, CaseItem
11
11
  from howler.odm.models.user import User
12
12
  from howler.security import api_login
13
13
  from howler.services import case_service
@@ -221,9 +221,10 @@ def append_item(id: str, user: User, **kwargs): # noqa: C901
221
221
 
222
222
  Data Block:
223
223
  {
224
- "type": "hit", # Type of item to append: "hit", "event", "case", "table", "lead", or "reference"
225
- "value": "item-id-123" # The ID or reference value for the item,
226
- "path": "example/path/Title"
224
+ "type": "hit", # Type of item: hit, event, case, folder, markdown, reference, table, or lead
225
+ "value": "item-id-123", # The ID or reference value for the item
226
+ "parent": "folder-uuid" # Optional: parent folder item ID (null for root)
227
+ "path": "example/path/Title" # Optional: path to create the item at (will ensure path exists)
227
228
  }
228
229
 
229
230
  Result Example:
@@ -236,15 +237,25 @@ def append_item(id: str, user: User, **kwargs): # noqa: C901
236
237
  except UnsupportedMediaType:
237
238
  return bad_request(err="Invalid JSON body")
238
239
 
239
- for field in ["value", "type", "path"]:
240
+ if not body or not isinstance(body, dict):
241
+ return bad_request(err="Request body must be a JSON object.")
242
+
243
+ for field in ["value", "type", "name"]:
240
244
  if field not in body:
241
245
  return bad_request(err=f"CaseItem '{field}' is required")
242
246
 
243
247
  try:
248
+ if path := body.pop("path", None):
249
+ parent = case_service.get_parent_from_path(id, path)
250
+
251
+ body["parent"] = parent.id if parent else None
252
+
244
253
  return ok(case_service.append_case_item(id, item=CaseItem(body)))
245
254
  except DataStoreException as e:
246
255
  logger.exception("Save Error")
247
256
  return internal_error(err=str(e))
257
+ except NotFoundException as e:
258
+ return not_found(err=str(e))
248
259
  except InvalidDataException as e:
249
260
  return bad_request(err=str(e))
250
261
 
@@ -267,7 +278,8 @@ def delete_item(case_id: str, **kwargs):
267
278
 
268
279
  Data Block:
269
280
  {
270
- "values": ["item-id-123", "item-id-456"] # The values of the items to delete
281
+ "ids": ["uuid-1", "uuid-2"], # The UUIDs of the items to delete
282
+ "force": false # Optional: force-delete non-empty folders
271
283
  }
272
284
 
273
285
  Result Example:
@@ -277,15 +289,21 @@ def delete_item(case_id: str, **kwargs):
277
289
  """
278
290
  body = request.json
279
291
 
280
- if not body or not isinstance(body, dict) or "values" not in body:
281
- return bad_request(err="Request body must be a JSON object with a 'values' field.")
292
+ if not body or not isinstance(body, dict):
293
+ return bad_request(err="Request body must be a JSON object.")
282
294
 
283
- values = body["values"]
284
- if not isinstance(values, list) or not values:
285
- return bad_request(err="'values' must be a non-empty list.")
295
+ force = body.get("force", False)
296
+ if not isinstance(force, bool):
297
+ return bad_request(err="'force' must be a boolean.")
298
+
299
+ ids = body.get("ids")
300
+ if not ids or not isinstance(ids, list):
301
+ return bad_request(err="'ids' must be a non-empty list.")
302
+ elif not all(isinstance(item_id, str) for item_id in ids):
303
+ return bad_request(err="All items in 'ids' must be strings.")
286
304
 
287
305
  try:
288
- return ok(case_service.remove_case_items(case_id, values))
306
+ return ok(case_service.remove_case_items(case_id, ids, force=force))
289
307
  except DataStoreException as e:
290
308
  logger.exception("Save Error")
291
309
  return internal_error(err=str(e))
@@ -297,10 +315,9 @@ def delete_item(case_id: str, **kwargs):
297
315
  @case_api.route("/<case_id>/items", methods=["PUT"])
298
316
  @api_login(required_priv=["R", "W"])
299
317
  def rename_item(case_id: str, **kwargs):
300
- """Rename (re-path) an item within a case
318
+ """Move an item within a case
301
319
 
302
- Updates the path of a single item identified by its value. The new path must
303
- not already be used by another item in the case.
320
+ Updates the parent of a single item identified by its id.
304
321
 
305
322
  Variables:
306
323
  case_id => The id of the case to modify
@@ -310,8 +327,9 @@ def rename_item(case_id: str, **kwargs):
310
327
 
311
328
  Data Block:
312
329
  {
313
- "value": "item-id-123", # The value of the item to rename
314
- "new_path": "folder/New Name" # The new path for the item
330
+ "id": "uuid-of-item", # The UUID of the item to update
331
+ "parent": "uuid-or-null", # Move: the UUID of the target folder, or null for root
332
+ "name": "Display Name" # Rename: the new display name for the item
315
333
  }
316
334
 
317
335
  Result Example:
@@ -324,12 +342,22 @@ def rename_item(case_id: str, **kwargs):
324
342
  if not body or not isinstance(body, dict):
325
343
  return bad_request(err="Request body must be a JSON object.")
326
344
 
327
- for field in ["value", "new_path"]:
328
- if field not in body:
329
- return bad_request(err=f"'{field}' is required.")
345
+ if "id" not in body:
346
+ return bad_request(err="'id' is required.")
330
347
 
348
+ item_id = body["id"]
331
349
  try:
332
- return ok(case_service.rename_case_item(case_id, item_value=body["value"], new_path=body["new_path"]))
350
+ result: Case | None = None
351
+ if "name" in body:
352
+ result = case_service.rename_case_item(case_id, item_id, body["name"])
353
+
354
+ if "parent" in body:
355
+ result = case_service.move_case_item(case_id, item_id, body["parent"])
356
+
357
+ if not result:
358
+ return bad_request(err="At least one of 'name' or 'parent' is required.")
359
+
360
+ return ok(result)
333
361
  except DataStoreException as e:
334
362
  logger.exception("Save Error")
335
363
  return internal_error(err=str(e))
@@ -836,11 +836,11 @@ class ESCollection(Generic[ModelType]):
836
836
  alias_actions.append({"remove_index": {"index": new_name}})
837
837
  self.with_retries(self.datastore.client.indices.update_aliases, actions=alias_actions)
838
838
 
839
- if self.with_retries(self.datastore.client.indices.exists, index=new_name):
839
+ if bool(self.with_retries(self.datastore.client.indices.exists, index=new_name)):
840
840
  logger.warning("Deleting reindex target %s", new_name)
841
841
  self.with_retries(self.datastore.client.indices.delete, index=new_name)
842
842
  finally:
843
- if self.with_retries(self.datastore.client.indices.exists, index=index):
843
+ if bool(self.with_retries(self.datastore.client.indices.exists, index=index)):
844
844
  logger.warning("Unblock writes to the index")
845
845
  self.with_retries(
846
846
  self.datastore.client.indices.put_settings,
@@ -848,7 +848,7 @@ class ESCollection(Generic[ModelType]):
848
848
  settings=write_unblock_settings,
849
849
  )
850
850
  except Exception:
851
- if source_writes_blocked and self.with_retries(self.datastore.client.indices.exists, index=index):
851
+ if source_writes_blocked and bool(self.with_retries(self.datastore.client.indices.exists, index=index)):
852
852
  logger.warning("Unblock writes to source index %s after failed reindex", index)
853
853
  self.with_retries(
854
854
  self.datastore.client.indices.put_settings,
@@ -1233,7 +1233,7 @@ class ESCollection(Generic[ModelType]):
1233
1233
  return self.normalize(data, as_obj=as_obj), version
1234
1234
  return self.normalize(data, as_obj=as_obj)
1235
1235
 
1236
- def save(self, key, data, version=None, refresh=None):
1236
+ def save(self, key, data, version=None, refresh: Literal["true", "false", "wait_for"] | None = None):
1237
1237
  """Save to document to the datastore using the key as its document id.
1238
1238
 
1239
1239
  The document data will be normalized before being saved in the datastore.
@@ -1241,6 +1241,9 @@ class ESCollection(Generic[ModelType]):
1241
1241
  :param key: ID of the document to save
1242
1242
  :param data: raw data or instance of the model class to save as the document
1243
1243
  :param version: version of the document to save over, if the version check fails this will raise an exception
1244
+ :param refresh: 'true' | 'false' | 'wait_for' | None
1245
+ Whether to refresh the datastore before returning. 'wait_for' will wait for the change to be visible
1246
+ in search.
1244
1247
  :return: True if the document was saved properly
1245
1248
  """
1246
1249
  if " " in key:
@@ -9,7 +9,7 @@ Elasticsearch datastore without boilerplate.
9
9
  from __future__ import annotations
10
10
 
11
11
  from operator import attrgetter
12
- from typing import Generic, TypeVar, overload
12
+ from typing import Generic, Literal, TypeVar, overload
13
13
 
14
14
  from howler.common.exceptions import HowlerRuntimeError
15
15
  from howler.common.loader import datastore
@@ -82,7 +82,7 @@ class DatastoreMixin(Generic[ModelType]):
82
82
  """
83
83
  return datastore()
84
84
 
85
- def save(self) -> bool:
85
+ def save(self, refresh: Literal["true", "false", "wait_for"] = "wait_for") -> bool:
86
86
  """Persist the current model instance to the datastore.
87
87
 
88
88
  Determines the target index from the lowercase class name, extracts the
@@ -94,4 +94,4 @@ class DatastoreMixin(Generic[ModelType]):
94
94
  id_field = self.__class__._Model__id_field # type: ignore[attr-defined]
95
95
  current_id = attrgetter(id_field)(self)
96
96
 
97
- return self.ds[index_name].save(current_id, self)
97
+ return self.ds[index_name].save(current_id, self, refresh=refresh)
@@ -7,7 +7,7 @@ from howler.odm.constants import CaseEscalation, Status
7
7
  from howler.odm.mixins import DatastoreMixin
8
8
  from howler.utils.compat import StrEnum
9
9
 
10
- CASE_ITEM_TYPES = {"event", "hit", "case", "lead", "reference"}
10
+ CASE_ITEM_TYPES = {"event", "hit", "case", "lead", "reference", "folder", "markdown"}
11
11
 
12
12
  RULE_INDEX_TYPES = {"hit", "event"}
13
13
 
@@ -43,6 +43,8 @@ class CaseItemTypes(StrEnum):
43
43
  validation by analysts.
44
44
  REFERENCE: An external reference such as a URL, document, or resource that
45
45
  provides additional context or evidence.
46
+ FOLDER: An organizational folder for grouping case items hierarchically.
47
+ MARKDOWN: A markdown document whose value contains the markdown content directly.
46
48
  """
47
49
 
48
50
  EVENT = "event"
@@ -51,6 +53,8 @@ class CaseItemTypes(StrEnum):
51
53
  CASE = "case"
52
54
  LEAD = "lead"
53
55
  REFERENCE = "reference"
56
+ FOLDER = "folder"
57
+ MARKDOWN = "markdown"
54
58
 
55
59
 
56
60
  @odm.model(index=True, store=True, description="Log definition.")
@@ -73,11 +77,19 @@ class CaseLog(odm.Model):
73
77
  super().__init__(data, *args, **kwargs)
74
78
 
75
79
 
76
- @odm.model(index=True, store=True, description="A path-scoped item included in a case.")
80
+ @odm.model(index=True, store=True, description="An item included in a case.")
77
81
  class CaseItem(odm.Model):
78
- path: str = odm.Keyword(description="Path of the item in the case hierarchy.", default="")
82
+ id: str = odm.UUID(description="Unique identifier for this item.")
83
+ parent: Optional[str] = odm.Optional(
84
+ odm.Keyword(description="ID of the parent folder item, or null for root-level items."),
85
+ default=None,
86
+ )
87
+ name: Optional[str] = odm.Optional(
88
+ odm.Keyword(description="Display name for the item. Optional; the UI falls back to value when absent."),
89
+ default=None,
90
+ )
79
91
  type: str = odm.Enum(values=CaseItemTypes, description="Type of case item.")
80
- value: str = odm.Keyword(description="String reference value for the item (ID, URL, or token).")
92
+ value: str = odm.Keyword(description="String reference value for the item (ID, URL, or token), or markdown.")
81
93
  visible: bool = odm.Boolean(default=True, description="Whether the item is visible/accessible in the frontend.")
82
94
  classification: Optional[str] = odm.Optional(
83
95
  odm.Classification(
@@ -88,6 +100,17 @@ class CaseItem(odm.Model):
88
100
  )
89
101
  )
90
102
 
103
+ def __init__(self, data: dict = None, *args, **kwargs):
104
+ # Enforce: case items must be root-level (parent=null).
105
+ if data and data.get("type") == CaseItemTypes.CASE and data.get("parent") is not None:
106
+ raise HowlerValueError("Case items must be root-level (parent must be null)")
107
+
108
+ # Set value equal to name for folder items
109
+ if data and data.get("type") == CaseItemTypes.FOLDER:
110
+ data["value"] = data.get("name")
111
+
112
+ super().__init__(data, *args, **kwargs)
113
+
91
114
 
92
115
  @odm.model(index=True, store=True, description="Rule used to place/query data into case paths.")
93
116
  class CaseRule(odm.Model):
@@ -131,7 +154,7 @@ class CaseTask(odm.Model):
131
154
  complete: bool = odm.Boolean(default=False, description="Whether the task is complete.")
132
155
  assignment: str | None = odm.Keyword(description="Assigned discipline or user ID.", optional=True)
133
156
  summary: str = odm.Text(description="Task summary.")
134
- path: str = odm.Keyword(description="Associated case item path.", optional=True)
157
+ item: str = odm.Keyword(description="Associated case item id.", optional=True)
135
158
 
136
159
 
137
160
  @odm.model(index=True, store=True, description="Enrichment annotations associated with a case path.")