clue-api 1.0.0.dev7__py3-none-any.whl

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 (91) hide show
  1. clue/.gitignore +21 -0
  2. clue/__init__.py +0 -0
  3. clue/api/__init__.py +211 -0
  4. clue/api/base.py +99 -0
  5. clue/api/v1/__init__.py +82 -0
  6. clue/api/v1/actions.py +92 -0
  7. clue/api/v1/auth.py +243 -0
  8. clue/api/v1/configs.py +83 -0
  9. clue/api/v1/fetchers.py +94 -0
  10. clue/api/v1/lookup.py +221 -0
  11. clue/api/v1/registration.py +109 -0
  12. clue/api/v1/static.py +94 -0
  13. clue/app.py +166 -0
  14. clue/cache/__init__.py +129 -0
  15. clue/common/__init__.py +0 -0
  16. clue/common/classification.py +1006 -0
  17. clue/common/classification.yml +130 -0
  18. clue/common/dict_utils.py +130 -0
  19. clue/common/exceptions.py +199 -0
  20. clue/common/forge.py +152 -0
  21. clue/common/json_utils.py +10 -0
  22. clue/common/list_utils.py +11 -0
  23. clue/common/logging/__init__.py +291 -0
  24. clue/common/logging/audit.py +157 -0
  25. clue/common/logging/format.py +42 -0
  26. clue/common/regex.py +31 -0
  27. clue/common/str_utils.py +213 -0
  28. clue/common/swagger.py +139 -0
  29. clue/common/uid.py +47 -0
  30. clue/config.py +60 -0
  31. clue/constants/__init__.py +0 -0
  32. clue/constants/supported_types.py +38 -0
  33. clue/cronjobs/__init__.py +30 -0
  34. clue/cronjobs/plugins.py +32 -0
  35. clue/error.py +129 -0
  36. clue/gunicorn_config.py +29 -0
  37. clue/healthz.py +74 -0
  38. clue/helper/discover.py +53 -0
  39. clue/helper/headers.py +30 -0
  40. clue/helper/oauth.py +128 -0
  41. clue/models/__init__.py +0 -0
  42. clue/models/actions.py +243 -0
  43. clue/models/config.py +456 -0
  44. clue/models/fetchers.py +136 -0
  45. clue/models/graph.py +162 -0
  46. clue/models/model_list.py +52 -0
  47. clue/models/network.py +430 -0
  48. clue/models/results/__init__.py +34 -0
  49. clue/models/results/base.py +10 -0
  50. clue/models/results/graph.py +26 -0
  51. clue/models/results/image.py +22 -0
  52. clue/models/results/status.py +55 -0
  53. clue/models/results/validation.py +57 -0
  54. clue/models/selector.py +67 -0
  55. clue/models/utils.py +52 -0
  56. clue/models/validators.py +19 -0
  57. clue/patched.py +8 -0
  58. clue/plugin/__init__.py +1008 -0
  59. clue/plugin/helpers/__init__.py +0 -0
  60. clue/plugin/helpers/central_server.py +27 -0
  61. clue/plugin/helpers/email_render.py +228 -0
  62. clue/plugin/helpers/token.py +34 -0
  63. clue/plugin/helpers/trino.py +103 -0
  64. clue/plugin/interactive.py +270 -0
  65. clue/plugin/models.py +19 -0
  66. clue/plugin/utils.py +78 -0
  67. clue/remote/__init__.py +0 -0
  68. clue/remote/datatypes/__init__.py +130 -0
  69. clue/remote/datatypes/cache.py +62 -0
  70. clue/remote/datatypes/events.py +118 -0
  71. clue/remote/datatypes/hash.py +193 -0
  72. clue/remote/datatypes/queues/__init__.py +0 -0
  73. clue/remote/datatypes/queues/comms.py +62 -0
  74. clue/remote/datatypes/set.py +96 -0
  75. clue/remote/datatypes/user_quota_tracker.py +54 -0
  76. clue/security/__init__.py +211 -0
  77. clue/security/obo.py +95 -0
  78. clue/security/utils.py +34 -0
  79. clue/services/action_service.py +186 -0
  80. clue/services/auth_service.py +348 -0
  81. clue/services/config_service.py +38 -0
  82. clue/services/fetcher_service.py +203 -0
  83. clue/services/jwt_service.py +233 -0
  84. clue/services/lookup_service.py +786 -0
  85. clue/services/type_service.py +165 -0
  86. clue/services/user_service.py +152 -0
  87. clue_api-1.0.0.dev7.dist-info/METADATA +111 -0
  88. clue_api-1.0.0.dev7.dist-info/RECORD +91 -0
  89. clue_api-1.0.0.dev7.dist-info/WHEEL +4 -0
  90. clue_api-1.0.0.dev7.dist-info/entry_points.txt +8 -0
  91. clue_api-1.0.0.dev7.dist-info/licenses/LICENSE +11 -0
@@ -0,0 +1,130 @@
1
+ # This the default classification engine provided with Clue,
2
+ # it showcases all the different features of the classification engine
3
+ # while providing a useful configuration
4
+
5
+ # Turn on/off classification enforcement. When this flag is off, this
6
+ # completely disables the classification engine, any documents added while
7
+ # the classification engine is off gets the default unrestricted value
8
+ enforce: false
9
+
10
+ # Turn on/off dynamic group creation. This feature allow you to dynamically create classification groups based on
11
+ # features from the user.
12
+ dynamic_groups: false
13
+
14
+ # Set the type of dynamic groups to be used
15
+ # email: groups will be based of the user's email domain
16
+ # group: groups will be created out the the user's group values
17
+ # all: groups will be created out of both the email domain and the group values
18
+ dynamic_groups_type: email
19
+
20
+ # List of Classification level:
21
+ # Graded list were a smaller number is less restricted then an higher number.
22
+ levels:
23
+ # List of alternate names for the current marking
24
+ - aliases:
25
+ - UNRESTRICTED
26
+ - UNCLASSIFIED
27
+ - U
28
+ - TLP:W
29
+ - TLP:WHITE
30
+ # Stylesheet applied in the UI for the different levels
31
+ css:
32
+ # Name of the color scheme used for display (default, primary, secondary, success, info, warning, error)
33
+ color: default
34
+ # Description of the classification level
35
+ description: Subject to standard copyright rules, TLP:CLEAR information may be distributed without restriction.
36
+ # Interger value of the Classification level (higher is more classified)
37
+ lvl: 100
38
+ # Long name of the classification item
39
+ name: TLP:CLEAR
40
+ # Short name of the classification item
41
+ short_name: TLP:C
42
+ - aliases: []
43
+ css:
44
+ color: success
45
+ description:
46
+ Recipients may share TLP:GREEN information with peers and partner organizations
47
+ within their sector or community, but not via publicly accessible channels. Information
48
+ in this category can be circulated widely within a particular community. TLP:GREEN
49
+ information may not be released outside of the community.
50
+ lvl: 110
51
+ name: TLP:GREEN
52
+ short_name: TLP:G
53
+ - aliases: []
54
+ css:
55
+ color: warning
56
+ description:
57
+ Recipients may only share TLP:AMBER information with members of their
58
+ own organization and with clients or customers who need to know the information
59
+ to protect themselves or prevent further harm.
60
+ lvl: 120
61
+ name: TLP:AMBER
62
+ short_name: TLP:A
63
+ - aliases:
64
+ - RESTRICTED
65
+ css:
66
+ color: warning
67
+ description:
68
+ Recipients may only share TLP:AMBER+STRICT information with members of their
69
+ own organization.
70
+ lvl: 125
71
+ name: TLP:AMBER+STRICT
72
+ short_name: TLP:A+S
73
+
74
+ # List of required tokens:
75
+ # A user requesting access to an item must have all the
76
+ # required tokens the item has to gain access to it
77
+ required:
78
+ - aliases: []
79
+ description: Produced using a commercial tool with limited distribution
80
+ name: COMMERCIAL
81
+ short_name: CMR
82
+ # The minimum classification level an item must have
83
+ # for this token to be valid. (optional)
84
+ # require_lvl: 100
85
+ # This is a token that is required but will display in the groups part
86
+ # of the classification string. (optional)
87
+ # is_required_group: true
88
+
89
+ # List of groups:
90
+ # A user requesting access to an item must be part of a least
91
+ # of one the group the item is part of to gain access
92
+ groups:
93
+ - aliases: []
94
+ # This is a special flag that when set to true, if any groups are selected
95
+ # in a classification. This group will automatically be selected too. (optional)
96
+ auto_select: true
97
+ description: Employees of CSE
98
+ name: CSE
99
+ short_name: CSE
100
+ # Assuming that this groups is the only group selected, this is the display name
101
+ # that will be used in the classification (that values has to be in the aliases
102
+ # of this group and only this group) (optional)
103
+ # solitary_display_name: ANY
104
+
105
+ # List of subgroups:
106
+ # A user requesting access to an item must be part of a least
107
+ # of one the subgroup the item is part of to gain access
108
+ subgroups:
109
+ - aliases: []
110
+ description: Member of Incident Response team
111
+ name: IR TEAM
112
+ short_name: IR
113
+ - aliases: []
114
+ description: Member of the Canadian Centre for Cyber Security
115
+ # This is a special flag that auto-select the corresponding group
116
+ # when this subgroup is selected (optional)
117
+ require_group: CSE
118
+ name: CCCS
119
+ short_name: CCCS
120
+ # This is a special flag that makes sure that none other then the
121
+ # corresponding group is selected when this subgroup is selected (optional)
122
+ # limited_to_group: CSE
123
+
124
+ # Default restricted classification
125
+ restricted: TLP:A+S//CMR
126
+
127
+ # Default unrestricted classification:
128
+ # When no classification are provided or that the classification engine is
129
+ # disabled, this is the classification value each items will get
130
+ unrestricted: TLP:C
@@ -0,0 +1,130 @@
1
+ from collections.abc import Mapping
2
+ from typing import Any, AnyStr, Optional, cast
3
+ from typing import Mapping as _Mapping
4
+
5
+
6
+ def strip_nulls(d: Any):
7
+ """Remove null values from a dict"""
8
+ if isinstance(d, dict):
9
+ return {k: strip_nulls(v) for k, v in d.items() if v is not None}
10
+ else:
11
+ return d
12
+
13
+
14
+ def recursive_update(
15
+ d: Optional[dict[str, Any]],
16
+ u: Optional[_Mapping[str, Any]],
17
+ stop_keys: list[AnyStr] = [],
18
+ allow_recursion: bool = True,
19
+ ) -> dict[str, Any]:
20
+ "Recursively update a dict with another value"
21
+ if d is None:
22
+ return cast(dict, u or {})
23
+
24
+ if u is None:
25
+ return d
26
+
27
+ for k, v in u.items():
28
+ if isinstance(v, Mapping) and allow_recursion:
29
+ d[k] = recursive_update(d.get(k, {}), v, stop_keys=stop_keys, allow_recursion=k not in stop_keys)
30
+ else:
31
+ d[k] = v
32
+
33
+ return d
34
+
35
+
36
+ def get_recursive_delta(
37
+ d1: Optional[_Mapping[str, Any]],
38
+ d2: Optional[_Mapping[str, Any]],
39
+ stop_keys: list[AnyStr] = [],
40
+ allow_recursion: bool = True,
41
+ ) -> Optional[dict[str, Any]]:
42
+ "Get the recursive difference between two objects"
43
+ if d1 is None:
44
+ return cast(dict, d2)
45
+
46
+ if d2 is None:
47
+ return cast(dict, d1)
48
+
49
+ out = {}
50
+ for k1, v1 in d1.items():
51
+ if isinstance(v1, Mapping) and allow_recursion:
52
+ internal = get_recursive_delta(
53
+ v1,
54
+ d2.get(k1, {}),
55
+ stop_keys=stop_keys,
56
+ allow_recursion=k1 not in stop_keys,
57
+ )
58
+ if internal:
59
+ out[k1] = internal
60
+ else:
61
+ if k1 in d2:
62
+ v2 = d2[k1]
63
+ if v1 != v2:
64
+ out[k1] = v2
65
+
66
+ for k2, v2 in d2.items():
67
+ if k2 not in d1:
68
+ out[k2] = v2
69
+
70
+ return out
71
+
72
+
73
+ def flatten(data: _Mapping, parent_key: Optional[str] = None) -> dict[str, Any]:
74
+ "Flatten a nested dict"
75
+ items: list[tuple[str, Any]] = []
76
+ for k, v in data.items():
77
+ cur_key = f"{parent_key}.{k}" if parent_key is not None else k
78
+ if isinstance(v, dict):
79
+ items.extend(flatten(v, cur_key).items())
80
+ else:
81
+ items.append((cur_key, v))
82
+
83
+ return dict(items)
84
+
85
+
86
+ def unflatten(data: _Mapping) -> _Mapping:
87
+ "Unflatted a nested dict"
88
+ out: dict[str, Any] = dict()
89
+ for k, v in data.items():
90
+ parts = k.split(".")
91
+ d = out
92
+ for p in parts[:-1]:
93
+ if p not in d:
94
+ d[p] = dict()
95
+ d = d[p]
96
+ d[parts[-1]] = v
97
+ return out
98
+
99
+
100
+ def prune(data: _Mapping, keys: list[str], parent_key: Optional[str] = None) -> dict[str, Any]:
101
+ "Remove all keys in the given list from the dict if they exist"
102
+ pruned_items: list[tuple[str, Any]] = []
103
+
104
+ for key, val in data.items():
105
+ cur_key = f"{parent_key}.{key}" if parent_key else key
106
+
107
+ if isinstance(val, dict):
108
+ child_keys = [_key for _key in keys if _key.startswith(cur_key)]
109
+
110
+ if len(child_keys) > 0:
111
+ pruned_items.append((key, prune(val, child_keys, cur_key)))
112
+ elif isinstance(val, list):
113
+ if cur_key not in keys and not any(_key.startswith(cur_key) for _key in keys):
114
+ continue
115
+
116
+ list_result = []
117
+ for entry in val:
118
+ if isinstance(val, dict):
119
+ child_keys = [_key for _key in keys if _key.startswith(cur_key)]
120
+
121
+ if len(child_keys) > 0:
122
+ pruned_items.append((key, prune(val, child_keys, cur_key)))
123
+ else:
124
+ list_result.append(entry)
125
+
126
+ pruned_items.append((key, list_result))
127
+ elif cur_key in keys:
128
+ pruned_items.append((key, val))
129
+
130
+ return {k: v for k, v in pruned_items}
@@ -0,0 +1,199 @@
1
+ from inspect import getmembers, isfunction
2
+ from sys import exc_info
3
+ from traceback import format_tb
4
+ from typing import Optional, Self
5
+
6
+
7
+ class ClueException(Exception):
8
+ """Wrapper for all exceptions thrown in Clue' code"""
9
+
10
+ message: str
11
+ cause: Exception | None
12
+ status_code: int | None
13
+
14
+ def __init__(
15
+ self: Self,
16
+ message: str = "Something went wrong",
17
+ cause: Exception | None = None,
18
+ status_code: int | None = None,
19
+ ) -> None:
20
+ super().__init__(message)
21
+ self.message = message
22
+ self.cause = cause
23
+ self.status_code = status_code
24
+
25
+ def __repr__(self: Self) -> str:
26
+ """String reproduction of the Clue exception. Pass the message on"""
27
+ return self.message
28
+
29
+
30
+ class InvalidClassification(ClueException):
31
+ """Exception for Invalid Classification"""
32
+
33
+
34
+ class InvalidDefinition(ClueException):
35
+ """Exception for Invalid Definition"""
36
+
37
+
38
+ class InvalidRangeException(ClueException):
39
+ """Exception for Invalid Range"""
40
+
41
+
42
+ class NonRecoverableError(ClueException):
43
+ """Exception for an unrecoverable error"""
44
+
45
+
46
+ class RecoverableError(ClueException):
47
+ """Exception for a recoverable error"""
48
+
49
+
50
+ class ConfigException(ClueException):
51
+ """Exception thrown due to invalid configuration"""
52
+
53
+
54
+ class ResourceExists(ClueException):
55
+ """Exception thrown due to a pre-existing resource"""
56
+
57
+
58
+ class VersionConflict(ClueException):
59
+ """Exception thrown due to a version conflict"""
60
+
61
+ def __init__(self: Self, message: str = "Something went wrong", cause: Optional[Exception] = None) -> None:
62
+ ClueException.__init__(self, message, cause)
63
+
64
+
65
+ class ClueTypeError(ClueException, TypeError):
66
+ """TypeError child specifically for exceptions thrown by us"""
67
+
68
+ def __init__(
69
+ self: Self,
70
+ message: str = "Something went wrong",
71
+ cause: Optional[Exception] = None,
72
+ status_code: int | None = None,
73
+ ) -> None:
74
+ ClueException.__init__(
75
+ self, message, cause if cause is not None else TypeError(message), status_code=status_code
76
+ )
77
+
78
+
79
+ class ClueAttributeError(ClueException, AttributeError):
80
+ """AttributeError child specifically for exceptions thrown by us"""
81
+
82
+ def __init__(
83
+ self: Self,
84
+ message: str = "Something went wrong",
85
+ cause: Optional[Exception] = None,
86
+ status_code: int | None = None,
87
+ ) -> None:
88
+ ClueException.__init__(
89
+ self, message, cause if cause is not None else AttributeError(message), status_code=status_code
90
+ )
91
+
92
+
93
+ class ClueValueError(ClueException, ValueError):
94
+ """ValueError child specifically for exceptions thrown by us"""
95
+
96
+ def __init__(
97
+ self: Self,
98
+ message: str = "Something went wrong",
99
+ cause: Optional[Exception] = None,
100
+ status_code: int | None = None,
101
+ ) -> None:
102
+ ClueException.__init__(
103
+ self, message, cause if cause is not None else ValueError(message), status_code=status_code
104
+ )
105
+
106
+
107
+ class ClueNotImplementedError(ClueException, NotImplementedError):
108
+ """NotImplementedError child specifically for exceptions thrown by us"""
109
+
110
+ def __init__(self: Self, message: str = "Something went wrong", cause: Optional[Exception] = None) -> None:
111
+ ClueException.__init__(self, message, cause if cause is not None else NotImplementedError(message))
112
+
113
+
114
+ class ClueKeyError(ClueException, KeyError):
115
+ """KeyError child specifically for exceptions thrown by us"""
116
+
117
+ def __init__(self: Self, message: str = "Something went wrong", cause: Optional[Exception] = None) -> None:
118
+ ClueException.__init__(self, message, cause if cause is not None else KeyError(message))
119
+
120
+
121
+ class ClueRuntimeError(ClueException, RuntimeError):
122
+ """RuntimeError child specifically for exceptions thrown by us"""
123
+
124
+ def __init__(self: Self, message: str = "Something went wrong", cause: Optional[Exception] = None) -> None:
125
+ ClueException.__init__(self, message, cause if cause is not None else RuntimeError(message))
126
+
127
+
128
+ class NotFoundException(ClueException):
129
+ """Exception thrown when a resource cannot be found"""
130
+
131
+
132
+ class AccessDeniedException(ClueException):
133
+ """Exception thrown when a resource cannot be accessed by a user"""
134
+
135
+
136
+ class InvalidDataException(ClueException):
137
+ """Exception thrown when user-provided data is invalid"""
138
+
139
+
140
+ class AuthenticationException(ClueException):
141
+ """Exception thrown when a user cannot be authenticated"""
142
+
143
+
144
+ class TimeoutException(ClueException):
145
+ """Exception for Timeout"""
146
+
147
+
148
+ class UnprocessableException(ClueException):
149
+ """Exception for Unprocessable"""
150
+
151
+
152
+ class Chain(object):
153
+ """This class can be used as a decorator to override the type of exceptions returned by a function"""
154
+
155
+ def __init__(self: Self, exception: type[Exception]):
156
+ self.exception = exception
157
+
158
+ def __call__(self, original):
159
+ """Execute a function and wrap any resulting exceptions"""
160
+
161
+ def wrapper(*args, **kwargs):
162
+ try:
163
+ return original(*args, **kwargs)
164
+ except Exception as e:
165
+ wrapped = self.exception(str(e), e)
166
+ raise wrapped.with_traceback(exc_info()[2])
167
+
168
+ wrapper.__name__ = original.__name__
169
+ wrapper.__doc__ = original.__doc__
170
+ wrapper.__dict__.update(original.__dict__)
171
+
172
+ return wrapper
173
+
174
+ def execute(self, func, *args, **kwargs):
175
+ """Execute a function and wrap any resulting exceptions"""
176
+ try:
177
+ return func(*args, **kwargs)
178
+ except Exception as e:
179
+ wrapped = self.exception(str(e), e)
180
+ raise wrapped.with_traceback(exc_info()[2])
181
+
182
+
183
+ class ChainAll:
184
+ """This class can be used as a decorator to override the type of exceptions returned by every method of a class"""
185
+
186
+ def __init__(self: Self, exception: type[Exception]):
187
+ self.exception = Chain(exception)
188
+
189
+ def __call__(self, cls):
190
+ """We can use an instance of this class as a decorator."""
191
+ for method in getmembers(cls, predicate=isfunction):
192
+ setattr(cls, method[0], self.exception(method[1]))
193
+
194
+ return cls
195
+
196
+
197
+ def get_stacktrace_info(ex: Exception) -> str:
198
+ """Get and format traceback information from a given exception"""
199
+ return "".join(format_tb(exc_info()[2]) + [": ".join((ex.__class__.__name__, str(ex)))])
clue/common/forge.py ADDED
@@ -0,0 +1,152 @@
1
+ # This file contains the loaders for the different components of the system
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ import os
6
+ from pathlib import Path
7
+ from string import Template
8
+ from typing import TYPE_CHECKING
9
+
10
+ from flask_caching import Cache
11
+
12
+ from clue.common.dict_utils import recursive_update
13
+ from clue.common.logging.format import BRL_DATE_FORMAT, BRL_LOG_FORMAT
14
+ from clue.common.str_utils import default_string_value
15
+
16
+ APP_NAME: str = default_string_value(env_name="APP_NAME", default="clue") # type: ignore[assignment]
17
+ APP_PREFIX = os.environ.get("APP_PREFIX", "brl")
18
+
19
+ if TYPE_CHECKING:
20
+ from clue.common.classification import Classification
21
+
22
+ cache = Cache(config={"CACHE_TYPE": "SimpleCache"})
23
+
24
+ classification_engines: dict[Path, Classification] = {}
25
+
26
+ logger = logging.getLogger(f"{APP_NAME}.common.forge")
27
+ logger.setLevel(logging.INFO)
28
+ console = logging.StreamHandler()
29
+ console.setLevel(logging.INFO)
30
+ console.setFormatter(logging.Formatter(BRL_LOG_FORMAT, BRL_DATE_FORMAT))
31
+ logger.addHandler(console)
32
+
33
+
34
+ def __get_yml_path(yml_config: str | None = None) -> Path | None: # noqa: C901
35
+ if yml_config is not None:
36
+ return Path(yml_config)
37
+
38
+ if (_yml_path := Path(f"/etc/{APP_NAME}/classification.yml")).exists():
39
+ return _yml_path
40
+
41
+ if (_yml_path := Path(f"/etc/{APP_NAME}/conf/classification.yml")).exists():
42
+ return _yml_path
43
+
44
+ if os.getenv("AZURE_TEST_CONFIG", None) is not None:
45
+ import re
46
+
47
+ logger.info("Azure build environment detected, checking additional classification path")
48
+
49
+ work_dir_parent = Path("/__w")
50
+ work_dir: Path | None = None
51
+ for sub_path in work_dir_parent.iterdir():
52
+ if not sub_path.is_dir():
53
+ continue
54
+
55
+ logger.info("Testing sub path %s", sub_path)
56
+
57
+ if re.match(r"\d+", sub_path.name):
58
+ work_dir = work_dir_parent / sub_path
59
+
60
+ if work_dir is not None:
61
+ logger.info("Subpath %s exists, checking for test path", work_dir)
62
+ test_classification_path = work_dir / "s" / "test" / "config" / "classification.yml"
63
+
64
+ if test_classification_path.exists():
65
+ logger.info("Path %s detected", test_classification_path)
66
+ return test_classification_path
67
+
68
+ logger.error("No classification path found at path %s", test_classification_path)
69
+ logger.info(
70
+ "Available files:\n%s", "\n".join(sorted(str(path) for path in (work_dir / "s").glob("**/*")))
71
+ )
72
+ work_dir = None
73
+
74
+ custom_path = os.environ.get("CLUE_CONF_FOLDER", None)
75
+ if custom_path is None:
76
+ return None
77
+
78
+ if (_yml_path := (Path(custom_path) / "classification.yml")).exists():
79
+ return _yml_path
80
+
81
+ return None
82
+
83
+
84
+ def get_classification(yml_config: str | None = None): # noqa: C901
85
+ """Creates and registers a Classification engine.
86
+
87
+ If a yaml config is not provided, it will search in /etc/clue and /etc/clue/conf for a classification.yml
88
+ file instead.
89
+
90
+ Arguments:
91
+ yml_config: An optional yaml config to load.
92
+
93
+ Returns:
94
+ The created Classification engine.
95
+ """
96
+ import yaml
97
+
98
+ from clue.common.classification import Classification, InvalidDefinition
99
+
100
+ _yml_path = __get_yml_path(yml_config)
101
+
102
+ if _yml_path:
103
+ logger.debug("Classification file found at %s", _yml_path)
104
+ else:
105
+ logger.warning("Missing classification.yml file!")
106
+
107
+ if _yml_path in classification_engines:
108
+ return classification_engines[_yml_path]
109
+
110
+ classification_definition = {}
111
+ default_file = Path(__file__).parent / "classification.yml"
112
+ if default_file.exists():
113
+ with default_file.open() as default_fh:
114
+ default_yml_data = yaml.safe_load(default_fh.read())
115
+ if default_yml_data:
116
+ classification_definition.update(default_yml_data)
117
+
118
+ # Load modifiers from the yaml config
119
+ if _yml_path is not None and _yml_path.exists():
120
+ with _yml_path.open() as yml_fh:
121
+ yml_data = yaml.safe_load(yml_fh.read())
122
+ if yml_data:
123
+ classification_definition = recursive_update(classification_definition, yml_data)
124
+
125
+ if not classification_definition:
126
+ raise InvalidDefinition("Could not find any classification definition to load.")
127
+
128
+ classification_engine = Classification(classification_definition)
129
+
130
+ if _yml_path:
131
+ classification_engines[_yml_path] = classification_engine
132
+
133
+ return classification_engine
134
+
135
+
136
+ def env_substitute(buffer):
137
+ """Replace environment variables in the buffer with their value.
138
+
139
+ Use the built in template expansion tool that expands environment variable style strings ${}
140
+ We set the idpattern to none so that $abc doesn't get replaced but ${abc} does.
141
+
142
+ Case insensitive.
143
+ Variables that are found in the buffer, but are not defined as environment variables are ignored.
144
+ """
145
+ return Template(buffer).safe_substitute(os.environ, idpattern=None, bracedidpattern="(?a:[_a-z][_a-z0-9]*)")
146
+
147
+
148
+ def get_metrics_sink(redis=None):
149
+ """Creates a clue_metrics CommsQueue on redis for metrics."""
150
+ from clue.remote.datatypes.queues.comms import CommsQueue
151
+
152
+ return CommsQueue("clue_metrics", host=redis)
@@ -0,0 +1,10 @@
1
+ import json
2
+ from typing import Any
3
+
4
+
5
+ def try_parse_json(json_str: str, return_raw: bool = False) -> dict[str, Any] | str | None:
6
+ "Try and parse JSON, optionally returning the raw string if json loading fails."
7
+ try:
8
+ return json.loads(json_str)
9
+ except json.JSONDecodeError:
10
+ return json_str if return_raw else None
@@ -0,0 +1,11 @@
1
+ from typing import Any
2
+
3
+
4
+ def flatten_list(_list: list[list[Any]]):
5
+ "Flatten a nested list"
6
+ flat_list = []
7
+ for sublist in _list:
8
+ for item in sublist:
9
+ flat_list.append(item)
10
+
11
+ return flat_list