clue-api 1.4.0.dev180__tar.gz → 1.4.0.dev184__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 (95) hide show
  1. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/PKG-INFO +1 -1
  2. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/api/v1/actions.py +1 -1
  3. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/api/v1/fetchers.py +38 -0
  4. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/models/fetchers.py +15 -3
  5. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/plugin/__init__.py +109 -10
  6. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/services/action_service.py +1 -1
  7. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/services/fetcher_service.py +68 -0
  8. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/pyproject.toml +1 -1
  9. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/LICENSE +0 -0
  10. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/README.md +0 -0
  11. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/.gitignore +0 -0
  12. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/__init__.py +0 -0
  13. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/api/__init__.py +0 -0
  14. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/api/base.py +0 -0
  15. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/api/v1/__init__.py +0 -0
  16. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/api/v1/auth.py +0 -0
  17. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/api/v1/configs.py +0 -0
  18. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/api/v1/lookup.py +0 -0
  19. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/api/v1/registration.py +0 -0
  20. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/api/v1/static.py +0 -0
  21. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/app.py +0 -0
  22. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/cache/__init__.py +0 -0
  23. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/common/__init__.py +0 -0
  24. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/common/bytes_utils.py +0 -0
  25. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/common/classification.py +0 -0
  26. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/common/classification.yml +0 -0
  27. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/common/dict_utils.py +0 -0
  28. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/common/exceptions.py +0 -0
  29. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/common/forge.py +0 -0
  30. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/common/json_utils.py +0 -0
  31. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/common/list_utils.py +0 -0
  32. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/common/logging/__init__.py +0 -0
  33. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/common/logging/audit.py +0 -0
  34. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/common/logging/format.py +0 -0
  35. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/common/regex.py +0 -0
  36. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/common/str_utils.py +0 -0
  37. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/common/swagger.py +0 -0
  38. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/common/uid.py +0 -0
  39. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/config.py +0 -0
  40. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/constants/__init__.py +0 -0
  41. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/constants/env.py +0 -0
  42. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/constants/supported_types.py +0 -0
  43. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/cronjobs/__init__.py +0 -0
  44. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/cronjobs/plugins.py +0 -0
  45. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/error.py +0 -0
  46. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/extensions/__init__.py +0 -0
  47. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/extensions/config.py +0 -0
  48. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/gunicorn_config.py +0 -0
  49. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/healthz.py +0 -0
  50. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/helper/discover.py +0 -0
  51. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/helper/headers.py +0 -0
  52. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/helper/oauth.py +0 -0
  53. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/models/__init__.py +0 -0
  54. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/models/actions.py +0 -0
  55. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/models/config.py +0 -0
  56. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/models/graph.py +0 -0
  57. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/models/model_list.py +0 -0
  58. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/models/network.py +0 -0
  59. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/models/results/__init__.py +0 -0
  60. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/models/results/base.py +0 -0
  61. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/models/results/file.py +0 -0
  62. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/models/results/graph.py +0 -0
  63. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/models/results/image.py +0 -0
  64. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/models/results/status.py +0 -0
  65. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/models/results/validation.py +0 -0
  66. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/models/selector.py +0 -0
  67. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/models/validators.py +0 -0
  68. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/patched.py +0 -0
  69. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/plugin/celery_app.py +0 -0
  70. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/plugin/helpers/__init__.py +0 -0
  71. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/plugin/helpers/central_server.py +0 -0
  72. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/plugin/helpers/email_render.py +0 -0
  73. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/plugin/helpers/token.py +0 -0
  74. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/plugin/helpers/trino.py +0 -0
  75. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/plugin/models.py +0 -0
  76. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/plugin/utils.py +0 -0
  77. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/py.typed +0 -0
  78. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/remote/__init__.py +0 -0
  79. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/remote/datatypes/__init__.py +0 -0
  80. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/remote/datatypes/cache.py +0 -0
  81. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/remote/datatypes/events.py +0 -0
  82. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/remote/datatypes/hash.py +0 -0
  83. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/remote/datatypes/queues/__init__.py +0 -0
  84. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/remote/datatypes/queues/comms.py +0 -0
  85. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/remote/datatypes/set.py +0 -0
  86. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/remote/datatypes/user_quota_tracker.py +0 -0
  87. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/security/__init__.py +0 -0
  88. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/security/obo.py +0 -0
  89. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/security/utils.py +0 -0
  90. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/services/auth_service.py +0 -0
  91. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/services/config_service.py +0 -0
  92. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/services/jwt_service.py +0 -0
  93. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/services/lookup_service.py +0 -0
  94. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/services/type_service.py +0 -0
  95. {clue_api-1.4.0.dev180 → clue_api-1.4.0.dev184}/clue/services/user_service.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clue-api
3
- Version: 1.4.0.dev180
3
+ Version: 1.4.0.dev184
4
4
  Summary: Clue distributed enrichment service
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -104,7 +104,7 @@ def get_action_status(plugin_id: str, action_id: str, task_id: str, **kwargs) ->
104
104
  task_id (str): the ID of the specific task to get the status of
105
105
 
106
106
  Arguments:
107
- task_id (str): the celery task id to get the status of
107
+ None
108
108
 
109
109
 
110
110
  Result Example:
@@ -92,3 +92,41 @@ def run_fetcher(plugin_id: str, fetcher_id: str, **kwargs):
92
92
 
93
93
  logger.warning("Unknown error from fetcher %s.%s: %s", plugin_id, fetcher_id, err.message)
94
94
  return bad_gateway(err=err.message)
95
+
96
+
97
+ @generate_swagger_docs(responses={200: "Successfully fetched status of fetcher"})
98
+ @fetchers_api.route("/<plugin_id>/<fetcher_id>/status/<task_id>", methods=["GET"])
99
+ @api_login()
100
+ def get_fetcher_status(plugin_id: str, fetcher_id: str, task_id: str, **kwargs):
101
+ """Get the status or result of a fetcher
102
+
103
+ Variables:
104
+ plugin_id (str): the ID of the plugin who owns the action to execute
105
+ fetcher_id (str): the ID of the action to execute
106
+ task_id (str): the ID of the specific task to get the status of
107
+
108
+ Arguments:
109
+ None
110
+
111
+ Result Example:
112
+ {
113
+ "outcome": "success | failure", # was this execution a success or failure?
114
+ "format": "link", # What format is the output in?
115
+ "output": "http://example.com" # The output of the action. Can be any data structure.
116
+ }
117
+ """
118
+ try:
119
+ return ok(fetcher_service.get_fetcher_status(plugin_id, fetcher_id, task_id, kwargs["user"]))
120
+ except NotFoundException as err:
121
+ return not_found(err=err.message)
122
+ except ClueException as err:
123
+ if err.status_code == 400:
124
+ logger.warning(
125
+ "Bad request from fetcher %s.%s with task_id: %s: %s", plugin_id, fetcher_id, task_id, err.message
126
+ )
127
+ return bad_request(err=err.message)
128
+
129
+ logger.warning(
130
+ "Unknown error from fetcher %s.%s with task_id: %s: %s", plugin_id, fetcher_id, task_id, err.message
131
+ )
132
+ return bad_gateway(err=err.message)
@@ -30,6 +30,7 @@ class FetcherDefinition(BaseModel):
30
30
  description: str = Field(description="A basic description of the fetcher's usage.")
31
31
  format: str = Field(description="The output format of the fetcher's result.")
32
32
  supported_types: set[str] = Field(description="A list of types this fetcher supports.")
33
+ async_result: bool = Field(description="Does this fetcher run asynchronously?", default=False)
33
34
  extra_data: Optional[Dict[str, JsonValue]] = Field(
34
35
  default=None, description="Extra data you want to define for a fetcher"
35
36
  )
@@ -101,14 +102,18 @@ class FetcherDefinition(BaseModel):
101
102
 
102
103
 
103
104
  class FetcherResult(BaseModel, Generic[DATA]):
104
- outcome: Literal["success", "failure"] = Field(description="Did the fetcher succeed or fail?")
105
+ outcome: Literal["success", "failure", "pending"] = Field(
106
+ description="Did the fetcher succeed or fail, or is it pending?"
107
+ )
105
108
  data: DATA | None = Field(description="The output of the fetcher.", default=None)
106
109
  error: str | None = Field(description="If the fetcher failed, contains the relevant error message.", default=None)
107
- format: str = Field(
110
+ format: str | None = Field(
108
111
  description="What is the format of the output? Used to indicate what component to use when rendering "
109
112
  "the output.",
113
+ default=None,
110
114
  )
111
115
  link: Optional[Url] = Field(description="Link to more information on the fetcher", default=None)
116
+ task_id: str | None = Field(description="The task id if the fetcher result is pending.", default=None)
112
117
 
113
118
  @model_validator(mode="after")
114
119
  def validate_model(self: Self, info: ValidationInfo) -> Self: # noqa: C901
@@ -123,6 +128,9 @@ class FetcherResult(BaseModel, Generic[DATA]):
123
128
  if self.outcome == "success" and self.data is None:
124
129
  raise ClueValueError("Successful fetcher results must return data.")
125
130
 
131
+ if not self.task_id and self.outcome == "pending":
132
+ raise ClueValueError("task_id must be set if outcome is pending.")
133
+
126
134
  if self.outcome == "failure":
127
135
  if self.data is not None:
128
136
  raise ClueValueError("Failed fetcher results cannot return data.")
@@ -133,7 +141,7 @@ class FetcherResult(BaseModel, Generic[DATA]):
133
141
  elif self.error:
134
142
  raise ClueValueError("Errors can only be specified if the outcome is failure.")
135
143
 
136
- self.data = validate_result(self.format, self.data, info)
144
+ self.data = validate_result(self.format, self.data, info) if self.format else None
137
145
 
138
146
  return self
139
147
 
@@ -141,3 +149,7 @@ class FetcherResult(BaseModel, Generic[DATA]):
141
149
  def error_result(err: str) -> "FetcherResult":
142
150
  "Helper function to generate a failed fetcher result"
143
151
  return FetcherResult(outcome="failure", format="error", error=err)
152
+
153
+
154
+ class FetcherStatusRequest(BaseModel):
155
+ task_id: str = Field(description="The task id to get the status for.")
@@ -36,7 +36,7 @@ from clue.models.actions import (
36
36
  ExecuteRequest,
37
37
  )
38
38
  from clue.models.config import Config
39
- from clue.models.fetchers import FetcherDefinition, FetcherResult
39
+ from clue.models.fetchers import FetcherDefinition, FetcherResult, FetcherStatusRequest
40
40
  from clue.models.network import QueryEntry
41
41
  from clue.models.selector import Selector
42
42
  from clue.plugin.celery_app import celery_init_app
@@ -55,7 +55,8 @@ OVERRIDABLE_FUNCTIONS = [
55
55
  "liveness", # Kubernetes liveness probe endpoint
56
56
  "readiness", # Kubernetes readiness probe endpoint
57
57
  "run_action", # Function to execute plugin actions
58
- "get_status", # Function to check the status or result of a pending action
58
+ "get_action_status", # Function to check the status or result of a pending action
59
+ "get_fetcher_status", # Function to check the status or result of a pending fetcher
59
60
  "run_fetcher", # Function to execute plugin fetchers
60
61
  "setup_actions", # Runtime action definition generation
61
62
  "validate_token", # Custom authentication token validation
@@ -294,7 +295,13 @@ class CluePlugin:
294
295
  user parameters) that instance will be passed instead, and casting the argument will be necessary.
295
296
  """
296
297
 
297
- get_status: Callable[[Action, ActionStatusRequest, str | None], ActionResult] | None
298
+ get_action_status: Callable[[Action, ActionStatusRequest, str | None], ActionResult] | None
299
+ """The function to get the status and result of running actions.
300
+
301
+ Accepts the selected action definition as well as an ActionStatusRequest instance which contains the
302
+ specific task id to check the status of.
303
+ """
304
+ get_fetcher_status: Callable[[FetcherDefinition, FetcherStatusRequest, str | None], FetcherResult] | None
298
305
  """The function to get the status and result of running actions.
299
306
 
300
307
  Accepts the selected action definition as well as an ActionStatusRequest instance which contains the
@@ -338,7 +345,9 @@ class CluePlugin:
338
345
  logger: logging.Logger | None = None,
339
346
  readiness: Callable[[], Response] = default_readiness,
340
347
  run_action: Callable[[Action, ExecuteRequest, str | None], ActionResult] | None = None,
341
- get_status: Callable[[Action, ActionStatusRequest, str | None], ActionResult] | None = None,
348
+ get_action_status: Callable[[Action, ActionStatusRequest, str | None], ActionResult] | None = None,
349
+ get_fetcher_status: Callable[[FetcherDefinition, FetcherStatusRequest, str | None], FetcherResult]
350
+ | None = None,
342
351
  run_fetcher: Callable[[FetcherDefinition, Selector, str | None], FetcherResult] | None = None,
343
352
  setup_actions: Callable[[list[Action], str | None], list[Action]] | None = None,
344
353
  supported_types: set[str] | str | None = None,
@@ -439,7 +448,8 @@ class CluePlugin:
439
448
  self.enrich = enrich
440
449
  self.enable_celery = enable_celery
441
450
  self.run_action = run_action
442
- self.get_status = get_status
451
+ self.get_action_status = get_action_status
452
+ self.get_fetcher_status = get_fetcher_status
443
453
  self.validate_token = validate_token
444
454
 
445
455
  self.fetchers = fetchers
@@ -714,14 +724,20 @@ class CluePlugin:
714
724
  )
715
725
  self.app.add_url_rule(
716
726
  "/actions/<action_id>/status/<task_id>",
717
- self.get_action_status.__name__,
718
- self.get_action_status,
727
+ self.action_status.__name__,
728
+ self.action_status,
719
729
  methods=["GET"],
720
730
  )
721
731
  self.app.add_url_rule("/fetchers/", self.get_fetchers.__name__, self.get_fetchers, methods=["GET"])
722
732
  self.app.add_url_rule(
723
733
  "/fetchers/<fetcher_id>", self.execute_fetcher.__name__, self.execute_fetcher, methods=["POST"]
724
734
  )
735
+ self.app.add_url_rule(
736
+ "/fetchers/<fetcher_id>/status/<task_id>",
737
+ self.fetcher_status.__name__,
738
+ self.fetcher_status,
739
+ methods=["GET"],
740
+ )
725
741
  self.app.add_url_rule("/types/", self.get_type_names.__name__, self.get_type_names, methods=["GET"])
726
742
  self.app.add_url_rule("/lookup/<type_name>/<value>/", self.lookup.__name__, self.lookup, methods=["GET"])
727
743
  self.app.add_url_rule("/lookup/", self.bulk_lookup.__name__, self.bulk_lookup, methods=["POST"])
@@ -1177,7 +1193,7 @@ class CluePlugin:
1177
1193
 
1178
1194
  return self.make_api_response(result)
1179
1195
 
1180
- def get_action_status(self: Self, action_id: str, task_id: str): # noqa: C901
1196
+ def action_status(self: Self, action_id: str, task_id: str): # noqa: C901
1181
1197
  """Retrieves the status of the specified action.
1182
1198
 
1183
1199
  Args:
@@ -1192,7 +1208,7 @@ class CluePlugin:
1192
1208
  {}, err="task id not provided. task id is required for this request.", status_code=400
1193
1209
  )
1194
1210
 
1195
- if not self.get_status:
1211
+ if not self.get_action_status:
1196
1212
  return self.make_api_response(
1197
1213
  {}, err=f"{self.app_name} does not support the get action status functions.", status_code=400
1198
1214
  )
@@ -1234,7 +1250,7 @@ class CluePlugin:
1234
1250
  task_id,
1235
1251
  )
1236
1252
 
1237
- result = self.get_status(action_to_check, status_request, token)
1253
+ result = self.get_action_status(action_to_check, status_request, token)
1238
1254
  except json.JSONDecodeError as e:
1239
1255
  self.logger.warning("JSON decoding error while getting status: %s", str(e))
1240
1256
 
@@ -1375,6 +1391,89 @@ class CluePlugin:
1375
1391
 
1376
1392
  return self.make_api_response(result, status_code=status_code)
1377
1393
 
1394
+ def fetcher_status(self: Self, fetcher_id: str, task_id: str): # noqa: C901
1395
+ """Gets the status of a pending fetcher with the specified task_id
1396
+
1397
+ Args:
1398
+ fetcher_id (str): The ID of the fetcher to get the status of
1399
+ task_id (str): The ID of the pending task
1400
+
1401
+ Returns:
1402
+ Response: A Response object with a FetcherResult as the body.
1403
+ """
1404
+ if not task_id:
1405
+ return self.make_api_response(
1406
+ {}, err="task id not provided. task id is required for this request.", status_code=400
1407
+ )
1408
+
1409
+ if not self.get_fetcher_status:
1410
+ return self.make_api_response(
1411
+ {}, err=f"{self.app_name} does not support the get fetcher status functions.", status_code=400
1412
+ )
1413
+
1414
+ if not self.fetchers:
1415
+ return self.make_api_response({}, err=f"{self.app_name} does not support any fetchers.", status_code=400)
1416
+
1417
+ # Find the requested fetcher by ID
1418
+ fetcher = next((fetcher for fetcher in self.fetchers if fetcher.id == fetcher_id), None)
1419
+ if not fetcher:
1420
+ return self.make_api_response({}, err=f"Fetcher {fetcher_id} does not exist", status_code=404)
1421
+
1422
+ token: str | None = None
1423
+ if self.validate_token:
1424
+ self.logger.debug("Executing plugin-provided token validator")
1425
+
1426
+ token, error = self.validate_token()
1427
+
1428
+ if error:
1429
+ return self.make_api_response(None, f"Error on token validation: {error}", status_code=401)
1430
+
1431
+ self.logger.debug("Token is valid")
1432
+ else:
1433
+ self.logger.warning("No token validation provided. The access token will not be provided to the fetcher.")
1434
+
1435
+ status_code = 200
1436
+ try:
1437
+ status_request = FetcherStatusRequest(task_id=task_id)
1438
+
1439
+ self.logger.info("Getting status for fetcher '%s'", fetcher_id)
1440
+
1441
+ result = self.get_fetcher_status(fetcher, status_request, token)
1442
+ except json.JSONDecodeError as e:
1443
+ self.logger.warning("JSON decoding error during execution: %s", str(e))
1444
+
1445
+ status_code = 400
1446
+ result = FetcherResult(
1447
+ outcome="failure",
1448
+ format="error",
1449
+ error=f"Invalid request format. Response body must be valid JSON. Error: {str(e)}",
1450
+ )
1451
+ except ValidationError as err:
1452
+ self.logger.warning("Validation error during execution: %s", str(err))
1453
+ status_code = 400
1454
+ result = FetcherResult(outcome="failure", format="error", error=str(err))
1455
+ except ClueException as e:
1456
+ self.logger.exception("ClueException during execution:")
1457
+ status_code = 500
1458
+ result = FetcherResult(
1459
+ outcome="failure", format="error", error=f"Error encountered during execution: {e.message}"
1460
+ )
1461
+ except Exception as e:
1462
+ self.logger.exception("%s during execution:", e.__class__.__name__)
1463
+ status_code = 500
1464
+ result = FetcherResult(
1465
+ outcome="failure", format="error", error=f"An unknown error occurred during execution: {str(e)}"
1466
+ )
1467
+ finally:
1468
+ self.logger.info("Fetcher completed.")
1469
+
1470
+ self.logger.info("Fetcher outcome: %s", result.outcome)
1471
+
1472
+ if result.error:
1473
+ self.logger.info("Error Message: %s", result.error)
1474
+
1475
+ return self.make_api_response(result, status_code=status_code)
1476
+
1378
1477
  def use(self, func: Callable):
1379
1478
  """Register a function to be used by the CluePlugin for specific operations.
1380
1479
 
@@ -196,7 +196,7 @@ def get_action_status(plugin_id: str, action_id: str, task_id: str, user: dict[s
196
196
  Args:
197
197
  plugin_id (str): The ID of the plugin.
198
198
  action_id (str): The ID of the action to run.
199
- task_id (str): The celery task id to fetch the status for
199
+ task_id (str): The task id to fetch the status for
200
200
  user (dict[str, Any]): The user dict of the user running the action.
201
201
 
202
202
  Raises:
@@ -201,3 +201,71 @@ def run_fetcher(plugin_id: str, fetcher_id: str, user: dict[str, Any]) -> Fetche
201
201
  raise ClueException(
202
202
  f"Something went wrong when running fetcher from plugin '{plugin_id}': {err.__class__.__name__}."
203
203
  ) from err
204
+
205
+
206
+ def get_fetcher_status(plugin_id: str, fetcher_id: str, task_id: str, user: dict[str, Any]) -> FetcherResult:
207
+ """Executes a specified fetcher.
208
+
209
+ Args:
210
+ plugin_id (str): The ID of the plugin.
211
+ fetcher_id (str): The ID of the action to run.
212
+ task_id (str): The task id to fetch the status for
213
+ user (dict[str, Any]): The user dict of the user running the action.
214
+
215
+ Raises:
216
+ NotFoundException: Raised whenever the plugin or the action doesn't exist.
217
+ ClueException: Raised whenever an error is returned by the plugin endpoint.
218
+
219
+ Returns:
220
+ ActionResult: The result of the action.
221
+ """
222
+ plugin = next((source for source in config.api.external_sources if source.name == plugin_id), None)
223
+
224
+ if not plugin:
225
+ raise NotFoundException(f"Plugin {plugin_id} does not exist.")
226
+
227
+ access_token = request.headers.get("Authorization", type=str)
228
+ if access_token:
229
+ access_token = access_token.split(" ")[1]
230
+
231
+ obo_access_token = None
232
+ if access_token:
233
+ obo_access_token, error = auth_service.check_obo(plugin, access_token, user["uname"])
234
+
235
+ if error:
236
+ logger.error("%s: %s", plugin.name, error)
237
+ raise AuthenticationException("Invalid token provided for this enrichment.")
238
+
239
+ headers = {"Accept": "application/json"}
240
+ if obo_access_token or access_token:
241
+ headers["Authorization"] = f"Bearer {obo_access_token or access_token}"
242
+
243
+ try:
244
+ req_url = urljoin(plugin.url, f"fetchers/{fetcher_id}/status/{task_id}")
245
+ logger.debug("Getting status for action %s with task_id %s for user %s", req_url, task_id, user["uname"])
246
+
247
+ response = requests.get(
248
+ req_url,
249
+ headers=headers,
250
+ timeout=request.args.get("max_timeout", 60.0, type=float),
251
+ )
252
+
253
+ result = response.json()
254
+
255
+ if not response.ok:
256
+ raise ClueException(
257
+ result["api_error_message"] or result["api_response"].get("error", ""), status_code=response.status_code
258
+ )
259
+
260
+ return FetcherResult.model_validate(result["api_response"], context={"is_response": True})
261
+ except ValidationError as err:
262
+ logger.exception("Invalid Request Body:")
263
+ raise ClueValueError(
264
+ "Validation error encountered on response body.",
265
+ status_code=400,
266
+ ) from err
267
+ except (JSONDecodeError, exceptions.ConnectionError) as err:
268
+ logger.exception(f"Something went wrong when getting the status of the fetcher from plugin '{plugin_id}'")
269
+ raise ClueException(
270
+ f"Something went wrong getting the status of fetcher from plugin '{plugin_id}': {err.__class__.__name__}."
271
+ ) from err
@@ -139,7 +139,7 @@ log_cli_level = "WARN"
139
139
  [tool.poetry]
140
140
  package-mode = true
141
141
  name = "clue-api"
142
- version = "1.4.0.dev180"
142
+ version = "1.4.0.dev184"
143
143
  description = "Clue distributed enrichment service"
144
144
  authors = ["Canadian Centre for Cyber Security <contact@cyber.gc.ca>"]
145
145
  license = "MIT"
File without changes