clue-api 1.7.0.dev450__tar.gz → 1.7.0.dev458__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 (103) hide show
  1. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/PKG-INFO +1 -1
  2. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/models/config.py +5 -1
  3. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/models/network.py +42 -32
  4. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/services/lookup_service.py +61 -42
  5. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/pyproject.toml +1 -1
  6. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/LICENSE +0 -0
  7. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/README.md +0 -0
  8. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/.gitignore +0 -0
  9. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/__init__.py +0 -0
  10. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/api/__init__.py +0 -0
  11. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/api/base.py +0 -0
  12. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/api/v1/__init__.py +0 -0
  13. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/api/v1/actions.py +0 -0
  14. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/api/v1/auth.py +0 -0
  15. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/api/v1/configs.py +0 -0
  16. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/api/v1/fetchers.py +0 -0
  17. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/api/v1/lookup.py +0 -0
  18. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/api/v1/registration.py +0 -0
  19. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/api/v1/static.py +0 -0
  20. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/api/v1/sync.py +0 -0
  21. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/app.py +0 -0
  22. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/cache/__init__.py +0 -0
  23. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/common/__init__.py +0 -0
  24. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/common/bytes_utils.py +0 -0
  25. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/common/classification.py +0 -0
  26. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/common/classification.yml +0 -0
  27. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/common/dict_utils.py +0 -0
  28. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/common/exceptions.py +0 -0
  29. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/common/forge.py +0 -0
  30. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/common/json_utils.py +0 -0
  31. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/common/list_utils.py +0 -0
  32. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/common/logging/__init__.py +0 -0
  33. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/common/logging/audit.py +0 -0
  34. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/common/logging/format.py +0 -0
  35. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/common/regex.py +0 -0
  36. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/common/str_utils.py +0 -0
  37. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/common/swagger.py +0 -0
  38. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/common/uid.py +0 -0
  39. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/config.py +0 -0
  40. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/constants/__init__.py +0 -0
  41. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/constants/env.py +0 -0
  42. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/constants/supported_types.py +0 -0
  43. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/cronjobs/__init__.py +0 -0
  44. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/cronjobs/plugins.py +0 -0
  45. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/error.py +0 -0
  46. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/extensions/__init__.py +0 -0
  47. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/extensions/config.py +0 -0
  48. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/gunicorn_config.py +0 -0
  49. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/healthz.py +0 -0
  50. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/helper/discover.py +0 -0
  51. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/helper/headers.py +0 -0
  52. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/helper/oauth.py +0 -0
  53. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/helper/obo.py +0 -0
  54. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/helper/plugin_requests.py +0 -0
  55. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/models/__init__.py +0 -0
  56. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/models/actions.py +0 -0
  57. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/models/auth_user.py +0 -0
  58. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/models/fetchers.py +0 -0
  59. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/models/graph.py +0 -0
  60. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/models/jwks.py +0 -0
  61. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/models/model_list.py +0 -0
  62. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/models/results/__init__.py +0 -0
  63. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/models/results/base.py +0 -0
  64. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/models/results/file.py +0 -0
  65. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/models/results/graph.py +0 -0
  66. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/models/results/image.py +0 -0
  67. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/models/results/status.py +0 -0
  68. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/models/results/validation.py +0 -0
  69. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/models/schema.py +0 -0
  70. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/models/selector.py +0 -0
  71. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/models/sync.py +0 -0
  72. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/models/validators.py +0 -0
  73. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/patched.py +0 -0
  74. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/plugin/__init__.py +0 -0
  75. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/plugin/celery_app.py +0 -0
  76. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/plugin/helpers/__init__.py +0 -0
  77. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/plugin/helpers/central_server.py +0 -0
  78. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/plugin/helpers/email_render.py +0 -0
  79. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/plugin/helpers/token.py +0 -0
  80. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/plugin/helpers/trino.py +0 -0
  81. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/plugin/models.py +0 -0
  82. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/plugin/utils.py +0 -0
  83. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/py.typed +0 -0
  84. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/remote/__init__.py +0 -0
  85. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/remote/datatypes/__init__.py +0 -0
  86. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/remote/datatypes/cache.py +0 -0
  87. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/remote/datatypes/events.py +0 -0
  88. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/remote/datatypes/hash.py +0 -0
  89. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/remote/datatypes/queues/__init__.py +0 -0
  90. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/remote/datatypes/queues/comms.py +0 -0
  91. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/remote/datatypes/set.py +0 -0
  92. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/remote/datatypes/user_quota_tracker.py +0 -0
  93. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/security/__init__.py +0 -0
  94. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/security/obo.py +0 -0
  95. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/security/utils.py +0 -0
  96. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/services/action_service.py +0 -0
  97. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/services/auth_service.py +0 -0
  98. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/services/config_service.py +0 -0
  99. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/services/fetcher_service.py +0 -0
  100. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/services/jwt_service.py +0 -0
  101. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/services/mongo_service.py +0 -0
  102. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/clue/services/type_service.py +0 -0
  103. {clue_api-1.7.0.dev450 → clue_api-1.7.0.dev458}/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.7.0.dev450
3
+ Version: 1.7.0.dev458
4
4
  Summary: Clue distributed enrichment service
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -406,7 +406,11 @@ class ExternalSource(BaseModel):
406
406
  datahub_link: Url | None = Field(description="Link to datahub entry on this enrichment", default=None)
407
407
  documentation_link: Url | None = Field(description="Link to documentation on this enrichment", default=None)
408
408
  production: bool = Field(
409
- description="Is this source ready for production? This will disable model validation for increased speeds",
409
+ description=(
410
+ "Is this source ready for production? Skips entry validation for increased speed. "
411
+ "Bulk responses also bypass result schema validation. "
412
+ "Classification validation and user-clearance filtering always apply."
413
+ ),
410
414
  default=False,
411
415
  )
412
416
  include_default: bool = Field(
@@ -520,7 +520,7 @@ class QueryResult(ResultMetadata):
520
520
  @field_validator("items")
521
521
  @classmethod
522
522
  def validate_items(cls, items: list[QueryEntry], info: ValidationInfo) -> list[QueryEntry]: # noqa: ANN102
523
- """Validate that if classification data was provided, all annotations match the user's classification.
523
+ """Retain only items accessible to the user, failing closed without user classification.
524
524
 
525
525
  Args:
526
526
  items (list[QueryEntry]): The items to validate.
@@ -529,39 +529,49 @@ class QueryResult(ResultMetadata):
529
529
  Returns:
530
530
  list[QueryEntry]: The validated items field.
531
531
  """
532
- if info.context:
533
- user_classification = info.context.get("user", {}).get("classification", None)
534
- if user_classification:
535
- filtered_results: list[QueryEntry] = []
536
-
537
- for item in items:
538
- if CLASSIFICATION.is_accessible(user_classification, item.classification):
539
- filtered_results.append(item)
540
- else:
541
- logger.debug(
542
- "Removing item at classification %s, inaccessible to user classification %s",
543
- item.classification,
544
- user_classification,
545
- )
546
-
547
- if len(items) > len(filtered_results):
548
- logger.info(
549
- "Dropped %s items due to inaccessible classification (user classification: %s)",
550
- len(items) - len(filtered_results),
551
- user_classification,
552
- )
553
- elif DEBUG:
554
- logger.debug(
555
- "All %s values are accessible by user classification %s", len(items), user_classification
556
- )
557
-
558
- return filtered_results
532
+ if not isinstance(info.context, dict) or "user" not in info.context:
533
+ logger.warning("No user context given, dropping all query items")
534
+ return []
535
+
536
+ return cls.filter_items(items, info.context["user"])
537
+
538
+ @classmethod
539
+ def filter_items(cls, items: list[QueryEntry], user: Any) -> list[QueryEntry]: # noqa: ANN102
540
+ """Apply classification authorization independently of response schema validation."""
541
+ user_classification = user.get("classification") if isinstance(user, dict) else None
542
+ if (
543
+ not isinstance(user_classification, str)
544
+ or not user_classification
545
+ or user_classification.upper().partition("//")[0] in {"INV", CLASSIFICATION.INVALID_CLASSIFICATION}
546
+ or not CLASSIFICATION.is_valid(user_classification)
547
+ ):
548
+ logger.warning("Missing or invalid user classification, dropping all query items")
549
+ return []
550
+
551
+ filtered_results: list[QueryEntry] = []
552
+ for item in items:
553
+ if (
554
+ "classification" in item.model_fields_set
555
+ and isinstance(item.classification, str)
556
+ and item.classification
557
+ and item.classification.upper().partition("//")[0] not in {"INV", CLASSIFICATION.INVALID_CLASSIFICATION}
558
+ and CLASSIFICATION.is_valid(item.classification)
559
+ and CLASSIFICATION.is_accessible(user_classification, item.classification, ignore_invalid=True)
560
+ ):
561
+ filtered_results.append(item)
559
562
  else:
560
- logger.warning("No user classification given, classification parsing will not occur")
561
- else:
562
- logger.warning("No user context given, classification parsing will not occur")
563
+ logger.debug("Removing item with invalid or inaccessible classification")
564
+
565
+ if len(items) > len(filtered_results):
566
+ logger.info(
567
+ "Dropped %s items due to inaccessible classification (user classification: %s)",
568
+ len(items) - len(filtered_results),
569
+ user_classification,
570
+ )
571
+ elif DEBUG:
572
+ logger.debug("All %s values are accessible by user classification %s", len(items), user_classification)
563
573
 
564
- return items
574
+ return filtered_results
565
575
 
566
576
 
567
577
  class PluginResponse(BaseModel):
@@ -72,7 +72,14 @@ def get_client(base_url: str, timeout: float) -> Session:
72
72
 
73
73
 
74
74
  def build_result(
75
- type_name: str, value: str, source: ExternalSource, error: Optional[str] = None, latency: Optional[float] = None
75
+ type_name: str,
76
+ value: str,
77
+ source: ExternalSource,
78
+ error: Optional[str] = None,
79
+ latency: Optional[float] = None,
80
+ *,
81
+ user: dict[str, Any] | None = None,
82
+ items: list[QueryEntry] | None = None,
76
83
  ):
77
84
  """Builds the QueryResult object using the provided values.
78
85
 
@@ -83,6 +90,8 @@ def build_result(
83
90
  error (Optional[str], optional): The error that occured during the request. Defaults to None.
84
91
  latency (Optional[float], optional): The amount of time between the request and the response (in milliseconds).
85
92
  Defaults to None.
93
+ user (dict[str, Any] | None): The user whose classification controls access to items.
94
+ items (list[QueryEntry] | None): The items returned by the source.
86
95
 
87
96
  Returns:
88
97
  QueryResult: The QueryResult object built.
@@ -93,15 +102,19 @@ def build_result(
93
102
  if DEBUG:
94
103
  logger.debug("Building query result for source %s", source.name)
95
104
 
96
- return QueryResult(
97
- type=type_name,
98
- value=value,
99
- source=source.name,
100
- maintainer=source.maintainer,
101
- datahub_link=source.datahub_link,
102
- documentation_link=source.documentation_link,
103
- error=error,
104
- latency=latency or 0,
105
+ return QueryResult.model_validate(
106
+ {
107
+ "type": type_name,
108
+ "value": value,
109
+ "source": source.name,
110
+ "maintainer": source.maintainer,
111
+ "datahub_link": source.datahub_link,
112
+ "documentation_link": source.documentation_link,
113
+ "error": error,
114
+ "latency": latency or 0,
115
+ "items": items or [],
116
+ },
117
+ context={"user": user},
105
118
  )
106
119
 
107
120
 
@@ -275,12 +288,14 @@ def parse_response(source: ExternalSource, user: dict[str, Any], api_response: A
275
288
  )
276
289
 
277
290
  if source.production:
278
- logger.debug(f"Skipping validation for production source {source.name}")
279
- items: list[QueryEntry] = [QueryEntry.model_construct(data) for data in api_response]
280
- else:
281
- items = [QueryEntry.model_validate(data, context={"user": user}) for data in api_response]
291
+ logger.debug("Skipping validation for production source %s", source.name)
292
+ items = [
293
+ QueryEntry.model_construct(**{**data, "classification": data.get("classification")})
294
+ for data in api_response
295
+ ]
296
+ return QueryResult.filter_items(items, user)
282
297
 
283
- return items
298
+ return [QueryEntry.model_validate(data, context={"user": user}) for data in api_response]
284
299
 
285
300
 
286
301
  def parse_bulk_response(
@@ -302,9 +317,6 @@ def parse_bulk_response(
302
317
  """
303
318
  bulk_result: dict[str, dict[str, QueryResult]] = {}
304
319
 
305
- if source.production:
306
- logger.debug(f"Skipping validation for production source {source.name}")
307
-
308
320
  with capture_span(f"{source.name}-bulk", "parsing"):
309
321
  for type in api_response:
310
322
  bulk_result.setdefault(type, {})
@@ -322,6 +334,8 @@ def parse_bulk_response(
322
334
  data = {**data, **api_response[type][value], "latency": latency or 0.0}
323
335
 
324
336
  if source.production:
337
+ logger.debug("Skipping validation for production source %s", source.name)
338
+ data["items"] = parse_response(source, user, data.get("items", []))
325
339
  bulk_result[type][value] = QueryResult.model_construct(**data)
326
340
  else:
327
341
  bulk_result[type][value] = QueryResult.model_validate(
@@ -342,16 +356,12 @@ def handle_validation_error(source: ExternalSource, err: ValidationError) -> str
342
356
  Returns:
343
357
  str: A formatted error message.
344
358
  """
345
- pydantic_errs: list[str] = []
346
-
347
- for validation_err in err.errors():
348
- loc = ".".join(
349
- section if isinstance(section, str) else f"[{str(section)}]" for section in validation_err["loc"]
350
- )
351
- pydantic_errs.append(f'"{loc}": {validation_err["msg"]}')
352
-
353
- err_msg = f"{source.name} returned an improperly formatted response: {', '.join(pydantic_errs)}"
354
- err_id = log_error(logger, err_msg, err)
359
+ err_msg = f"{source.name} returned an improperly formatted response"
360
+ details = [
361
+ {"type": detail["type"], "loc": detail["loc"]}
362
+ for detail in err.errors(include_input=False, include_context=False)
363
+ ]
364
+ err_id = log_error(logger, err_msg, details)
355
365
  return f"{err_msg}. Error ID: {err_id}"
356
366
 
357
367
 
@@ -373,7 +383,7 @@ def query_external(
373
383
  if apm_transaction:
374
384
  execution_context.set_transaction(apm_transaction)
375
385
 
376
- finish_result = functools.partial(build_result, type_name, value, source)
386
+ finish_result = functools.partial(build_result, type_name, value, source, user=user)
377
387
 
378
388
  with capture_span(query_external.__name__, span_type="greenlet"):
379
389
  supported_types = type_service.get_supported_types(
@@ -433,17 +443,16 @@ def query_external(
433
443
  )
434
444
 
435
445
  try:
436
- result = finish_result(latency=(time.perf_counter() - start) * 1000)
437
-
438
446
  api_response = response["api_response"]
439
- if api_response:
440
- result.items = parse_response(source, user, api_response)
447
+ result = finish_result(
448
+ latency=(time.perf_counter() - start) * 1000,
449
+ items=parse_response(source, user, api_response) if api_response else [],
450
+ )
441
451
 
442
452
  logger.debug("Returning valid result from source %s", source)
443
453
 
444
454
  return result
445
455
  except ValidationError as err:
446
- logger.exception("Validation error on response from %s", source)
447
456
  return finish_result(
448
457
  error=handle_validation_error(source, err),
449
458
  latency=(time.perf_counter() - start) * 1000,
@@ -495,7 +504,7 @@ def enrich(type_name: str, value: str, user: dict[str, Any]): # noqa: C901
495
504
  if not query_sources and not source.include_default:
496
505
  continue
497
506
 
498
- finish_result = functools.partial(build_result, type_name, value, source)
507
+ finish_result = functools.partial(build_result, type_name, value, source, user=user)
499
508
 
500
509
  try:
501
510
  access_token, obo_access_token = get_obo_access_token(source, user)
@@ -547,7 +556,11 @@ def enrich(type_name: str, value: str, user: dict[str, Any]): # noqa: C901
547
556
  results[source.name] = result
548
557
  else:
549
558
  results[source.name] = build_result(
550
- type_name, value, source, "Request Timed Out" if not greenlet.exception else str(greenlet.exception)
559
+ type_name,
560
+ value,
561
+ source,
562
+ "Request Timed Out" if not greenlet.exception else str(greenlet.exception),
563
+ user=user,
551
564
  )
552
565
 
553
566
  thread_pool.kill(block=False)
@@ -583,7 +596,9 @@ def bulk_query_external( # noqa: C901
583
596
  bulk_result.setdefault(entry.type, {})
584
597
 
585
598
  if not supported_types or entry.type not in supported_types:
586
- bulk_result[entry.type][entry.value] = build_result(entry.type, entry.value, source, "invalid_type")
599
+ bulk_result[entry.type][entry.value] = build_result(
600
+ entry.type, entry.value, source, "invalid_type", user=user
601
+ )
587
602
  continue
588
603
 
589
604
  filtered_data.append(entry)
@@ -609,7 +624,9 @@ def bulk_query_external( # noqa: C901
609
624
  if quota_error := user_service.check_quota(source, user):
610
625
  for entry in data:
611
626
  bulk_result.setdefault(entry.type, {})
612
- bulk_result[entry.type][entry.value] = build_result(entry.type, entry.value, source, error=quota_error)
627
+ bulk_result[entry.type][entry.value] = build_result(
628
+ entry.type, entry.value, source, error=quota_error, user=user
629
+ )
613
630
 
614
631
  return bulk_result
615
632
 
@@ -652,7 +669,7 @@ def bulk_query_external( # noqa: C901
652
669
  if error:
653
670
  for entry in data:
654
671
  bulk_result[entry.type][entry.value] = build_result(
655
- entry.type, entry.value, source, error=error, latency=latency
672
+ entry.type, entry.value, source, error=error, latency=latency, user=user
656
673
  )
657
674
 
658
675
  return bulk_result
@@ -663,7 +680,7 @@ def bulk_query_external( # noqa: C901
663
680
  if not api_response:
664
681
  for entry in data:
665
682
  bulk_result[entry.type][entry.value] = build_result(
666
- entry.type, entry.value, source, latency=latency
683
+ entry.type, entry.value, source, latency=latency, user=user
667
684
  )
668
685
  else:
669
686
  bulk_result = parse_bulk_response(source, user, api_response, latency)
@@ -672,7 +689,7 @@ def bulk_query_external( # noqa: C901
672
689
 
673
690
  for entry in data:
674
691
  bulk_result[entry.type][entry.value] = build_result(
675
- entry.type, entry.value, source, error=error_message, latency=latency
692
+ entry.type, entry.value, source, error=error_message, latency=latency, user=user
676
693
  )
677
694
 
678
695
  return bulk_result
@@ -745,7 +762,7 @@ def bulk_enrich(data: list[Selector], user: dict[str, Any]): # noqa: C901
745
762
  except AuthenticationException as err:
746
763
  for entry in data:
747
764
  bulk_result[entry.type][entry.value][source.name] = build_result(
748
- entry.type, entry.value, source, err.message
765
+ entry.type, entry.value, source, err.message, user=user
749
766
  )
750
767
  continue
751
768
 
@@ -782,6 +799,7 @@ def bulk_enrich(data: list[Selector], user: dict[str, Any]): # noqa: C901
782
799
  f"Selector classification ({entry.classification}) exceeds max classification "
783
800
  f"of source: {source.name} ({source.max_classification})."
784
801
  ),
802
+ user=user,
785
803
  )
786
804
 
787
805
  continue
@@ -828,6 +846,7 @@ def bulk_enrich(data: list[Selector], user: dict[str, Any]): # noqa: C901
828
846
  source,
829
847
  "Request Timed Out" if not greenlet.exception else str(greenlet.exception),
830
848
  (time.perf_counter() - start) * 1000,
849
+ user=user,
831
850
  )
832
851
 
833
852
  continue
@@ -141,7 +141,7 @@ log_cli_level = "WARN"
141
141
  [tool.poetry]
142
142
  package-mode = true
143
143
  name = "clue-api"
144
- version = "1.7.0.dev450"
144
+ version = "1.7.0.dev458"
145
145
  description = "Clue distributed enrichment service"
146
146
  authors = ["Canadian Centre for Cyber Security <contact@cyber.gc.ca>"]
147
147
  license = "MIT"
File without changes