deltafi 0.109.0__py3-none-any.whl → 2.40.0__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.
@@ -0,0 +1,390 @@
1
+ #
2
+ # DeltaFi - Data transformation and enrichment platform
3
+ #
4
+ # Copyright 2021-2025 DeltaFi Contributors <deltafi@deltafi.org>
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ import re
20
+ import uuid
21
+ from abc import ABC
22
+ from importlib.resources import files
23
+ from typing import List
24
+
25
+ from deltafi.domain import DeltaFileMessage, Event, Content, Context
26
+ from deltafi.logger import get_logger
27
+ from deltafi.metric import Metric
28
+ from deltafi.result import ErrorResult, FilterResult
29
+ from deltafi.resultmessage import LogMessage
30
+ from deltafi.storage import Segment
31
+
32
+ from .assertions import *
33
+ from .compare_helpers import GenericCompareHelper, CompareHelper
34
+ from .constants import *
35
+
36
+
37
+ class IOContent:
38
+ """
39
+ The IOContent class holds the details for loading input or output
40
+ content into the test framework.
41
+ Attributes:
42
+ file_name (str) : The name of file in test/data.
43
+ content_name (str) : The name of the content.
44
+ content_type (str) : The media type of the content
45
+ offset (int) : Offset to use in Segment
46
+ content_bytes (str): Optional. If set to a String of length greater than zero, indicates to consumers of this
47
+ IOContent that they should bypass file read and use these bytes for content.
48
+ no_content (bool) : Optional. If 'True', then consumers should not attempt to interpret content but should
49
+ apply other aspects of this IOContent. When 'True', 'content_bytes' should be ignored and
50
+ loaded content, if any, should be interpreted as empty String or otherwise as documented by
51
+ the consumer.
52
+ """
53
+
54
+ def __init__(self, file_name: str, content_name: str = None, content_type: str = None, offset: int = 0,
55
+ content_bytes: str = "", no_content: bool = False):
56
+ self.file_name = file_name
57
+ if content_name is None:
58
+ self.content_name = file_name
59
+ else:
60
+ self.content_name = content_name
61
+ if content_type is None:
62
+ self.content_type = IOContent.file_type(file_name)
63
+ else:
64
+ self.content_type = content_type
65
+ self.offset = offset
66
+ self.no_content = no_content
67
+ if no_content:
68
+ self.content_bytes = None
69
+ else:
70
+ self.content_bytes = content_bytes
71
+ self.segment_uuid = uuid.uuid4()
72
+
73
+ @classmethod
74
+ def file_type(cls, name: str):
75
+ if name.endswith(".json"):
76
+ return "application/json"
77
+ elif name.endswith(".xml"):
78
+ return "application/xnl"
79
+ elif name.endswith(".txt"):
80
+ return "text/plain"
81
+ else:
82
+ return "application/octet-stream"
83
+
84
+
85
+ class LoadedContent:
86
+ def __init__(self, did: str, ioc: IOContent, data: str):
87
+ self.name = ioc.content_name
88
+ self.content_type = ioc.content_type
89
+ self.offset = ioc.offset
90
+ if data is not None:
91
+ self.data = data
92
+ else:
93
+ if ioc.no_content:
94
+ self.data = ""
95
+ else:
96
+ self.data = ioc.content_bytes
97
+ self.segment = Segment.from_dict(
98
+ {"uuid": str(ioc.segment_uuid), "offset": self.offset, "size": len(self.data), "did": did})
99
+
100
+
101
+ class InternalContentService:
102
+ def __init__(self):
103
+ self.loaded_content = {}
104
+ self.outputs = {}
105
+
106
+ def load(self, content_list: List[LoadedContent]):
107
+ for c in content_list:
108
+ self.loaded_content[c.segment.uuid] = c
109
+
110
+ def put_str(self, did: str, string_data: str):
111
+ segment = Segment(uuid=str(uuid.uuid4()), offset=0, size=len(string_data), did=did)
112
+ self.outputs[segment.uuid] = string_data
113
+ return segment
114
+
115
+ def get_str(self, segments: List[Segment]):
116
+ # TODO: String multiple segment ids together
117
+ seg_id = segments[0].uuid
118
+ return self.loaded_content[seg_id].data
119
+
120
+ def get_bytes(self, segments: List[Segment]):
121
+ seg_id = segments[0].uuid
122
+ return self.loaded_content[seg_id].data.encode('utf-8')
123
+
124
+ def get_output(self, seg_id: str):
125
+ if seg_id in self.outputs:
126
+ return self.outputs[seg_id]
127
+ elif seg_id in self.loaded_content:
128
+ return self.loaded_content[seg_id].data
129
+ else:
130
+ return None
131
+
132
+
133
+ class TestCaseBase(ABC):
134
+ def __init__(self, data: Dict):
135
+ """
136
+ A test case for DeltaFi python actions
137
+ :param data: Dict of test case fields
138
+ - action: instance of the action being tested
139
+ - data_dir: str: subdirectory name (e.g., test name) for locating test data files, i.e., test/data/{data_dir)
140
+ - compare_tool: (optional) CompareHelper instanced for comparing output content
141
+ - inputs: (optional) List[IOContent]: input content to action
142
+ - parameters: (optional) Dict: map of action input parameters
143
+ - in_memo: (optional) str: Input 'memo' value for a TimedIngress context
144
+ - in_meta: (optional) Dict: map of metadata as input to action
145
+ - join_meta: (optional): List[Dict]: When a List is provided, this enables the JOIN portion of an action.
146
+ When using JOIN, join_meta must match the size of inputs, though the Dict can be empty
147
+ - did: (optional): str: overrides random DID
148
+ """
149
+ if "action" in data:
150
+ self.action = data["action"]
151
+ else:
152
+ raise ValueError("action is required")
153
+
154
+ if "data_dir" in data:
155
+ self.data_dir = data["data_dir"]
156
+ else:
157
+ raise ValueError("data_dir is required")
158
+
159
+ if "compare_tool" in data:
160
+ self.compare_tool = data["compare_tool"]
161
+ else:
162
+ self.compare_tool = GenericCompareHelper()
163
+
164
+ self.inputs = data["inputs"] if "inputs" in data else []
165
+ self.file_name = data["file_name"] if "file_name" in data else "filename"
166
+ self.parameters = data["parameters"] if "parameters" in data else {}
167
+ self.in_meta = data["in_meta"] if "in_meta" in data else {}
168
+ self.in_memo = data["in_memo"] if "in_memo" in data else None
169
+ self.use_did = data["did"] if "did" in data else None
170
+ self.expected_result_type = None
171
+ self.err_or_filt_cause = None
172
+ self.err_or_filt_context = None
173
+ self.err_or_filt_annotations = None
174
+ self.join_meta = data["join_meta"] if "join_meta" in data else None
175
+ self.expected_metrics = []
176
+ self.expected_messages = []
177
+
178
+ def add_metric(self, metric: Metric):
179
+ self.expected_metrics.append(metric)
180
+
181
+ def add_message(self, message: LogMessage):
182
+ self.expected_messages.append(message)
183
+
184
+ def expect_error_result(self, cause: str, context: str, annotations: Dict = None):
185
+ """
186
+ A Sets the expected output of the action to an Error Result
187
+ :param cause: the expected error cause
188
+ :param context: the expected error context
189
+ :param annotations: Dict: (Optional) the expected annotations
190
+ """
191
+ self.expected_result_type = ErrorResult
192
+ self.err_or_filt_cause = cause
193
+ self.err_or_filt_context = context
194
+ self.err_or_filt_annotations = annotations
195
+
196
+ def expect_filter_result(self, cause: str, context: str = None, annotations: Dict = None):
197
+ """
198
+ A Sets the expected output of the action to a Filter Result
199
+ :param cause: the expected filter cause (message)
200
+ :param context: (Optional) the expected filter context
201
+ :param annotations: Dict: (Optional) the expected annotations
202
+ """
203
+ self.expected_result_type = FilterResult
204
+ self.err_or_filt_cause = cause
205
+ self.err_or_filt_context = context
206
+ self.err_or_filt_annotations = annotations
207
+
208
+
209
+ class ActionTest(ABC):
210
+ def __init__(self, package_name: str):
211
+ """
212
+ Provides structure for testing DeltaFi actions
213
+ Args:
214
+ package_name: name of the actions package for finding resources
215
+ """
216
+ self.content_service = InternalContentService()
217
+ self.did = ""
218
+ self.loaded_inputs = []
219
+ self.package_name = package_name
220
+ self.res_path = ""
221
+ self.context = None
222
+
223
+ def __reset__(self, did: str):
224
+ self.content_service = InternalContentService()
225
+ if did is None:
226
+ self.did = str(uuid.uuid4())
227
+ else:
228
+ self.did = did
229
+ self.loaded_inputs = []
230
+ self.res_path = ""
231
+ self.context = None
232
+
233
+ def load_file(self, ioc: IOContent):
234
+ file_res = self.res_path.joinpath(ioc.file_name)
235
+ with file_res.open("r") as f:
236
+ contents = f.read()
237
+ return contents
238
+
239
+ def get_contents(self, test_case: TestCaseBase):
240
+ pkg_path = files(self.package_name)
241
+ self.res_path = pkg_path.joinpath(f"test/data/{test_case.data_dir}/")
242
+
243
+ # Load inputs
244
+ for input_ioc in test_case.inputs:
245
+ if not input_ioc.no_content and len(input_ioc.content_bytes) == 0:
246
+ self.loaded_inputs.append(LoadedContent(self.did, input_ioc, self.load_file(input_ioc)))
247
+ else:
248
+ self.loaded_inputs.append(LoadedContent(self.did, input_ioc, None))
249
+
250
+ def make_content_list(self):
251
+ content_list = []
252
+ for loaded_input in self.loaded_inputs:
253
+ c = Content(name=loaded_input.name, segments=[loaded_input.segment], media_type=loaded_input.content_type,
254
+ content_service=self.content_service)
255
+ content_list.append(c)
256
+ loaded_input.content = c
257
+
258
+ return content_list
259
+
260
+ def make_df_msgs(self, test_case: TestCaseBase):
261
+ content_list = self.make_content_list()
262
+ self.content_service.load(self.loaded_inputs)
263
+
264
+ delta_file_messages = []
265
+
266
+ if test_case.join_meta is None:
267
+ delta_file_messages.append(DeltaFileMessage(metadata=test_case.in_meta, content_list=content_list))
268
+ else:
269
+ for index, content in enumerate(content_list):
270
+ delta_file_messages.append(DeltaFileMessage(
271
+ metadata=test_case.join_meta[index],
272
+ content_list=[content]))
273
+
274
+ return delta_file_messages
275
+
276
+ def make_context(self, test_case: TestCaseBase):
277
+ action_name = INGRESS_FLOW + "." + test_case.action.__class__.__name__
278
+ join = {} if test_case.join_meta else None
279
+ self.context = Context(
280
+ did=self.did,
281
+ delta_file_name=test_case.file_name,
282
+ data_source="DATASRC",
283
+ flow_name=INGRESS_FLOW,
284
+ flow_id="FLOWID",
285
+ action_name=action_name,
286
+ action_version="1.0",
287
+ hostname=HOSTNAME,
288
+ system_name=SYSTEM,
289
+ content_service=self.content_service,
290
+ saved_content=[],
291
+ join=join,
292
+ memo=test_case.in_memo,
293
+ logger=get_logger())
294
+ return self.context
295
+
296
+ def make_event(self, test_case: TestCaseBase):
297
+ return Event(delta_file_messages=self.make_df_msgs(test_case), context=self.make_context(test_case),
298
+ params=test_case.parameters, queue_name="", return_address="")
299
+
300
+ def call_action(self, test_case: TestCaseBase):
301
+ self.get_contents(test_case)
302
+ return test_case.action.execute_action(self.make_event(test_case))
303
+
304
+ def run_and_check_result_type(self, test_case: TestCaseBase, result_type):
305
+ self.__reset__(test_case.use_did)
306
+ result = self.call_action(test_case)
307
+
308
+ if not isinstance(result, result_type):
309
+ raise ValueError(f"Result type {result.__class__.__name__} does not match {result_type.__name__}")
310
+
311
+ return result
312
+
313
+ def execute_error(self, test_case: TestCaseBase):
314
+ result = self.run_and_check_result_type(test_case, ErrorResult)
315
+ resp = result.response()
316
+ assert_equal_with_label(test_case.err_or_filt_cause, resp['cause'], "error cause")
317
+ if test_case.err_or_filt_context is not None:
318
+ assert_equal_with_label(test_case.err_or_filt_context, resp['context'], "error context")
319
+ if test_case.err_or_filt_annotations is not None:
320
+ assert_keys_and_values(test_case.err_or_filt_annotations, result.annotations)
321
+
322
+ def execute_filter(self, test_case: TestCaseBase):
323
+ result = self.run_and_check_result_type(test_case, FilterResult)
324
+ resp = result.response()
325
+ assert_equal_with_label(test_case.err_or_filt_cause, resp['message'], "filter cause")
326
+ if test_case.err_or_filt_context is not None:
327
+ assert_equal_with_label(test_case.err_or_filt_context, resp['context'], "filter context")
328
+ if test_case.err_or_filt_annotations is not None:
329
+ assert_keys_and_values(test_case.err_or_filt_annotations, result.annotations)
330
+
331
+ def execute(self, test_case: TestCaseBase):
332
+ if test_case.expected_result_type == ErrorResult:
333
+ self.execute_error(test_case)
334
+ elif test_case.expected_result_type == FilterResult:
335
+ self.execute_filter(test_case)
336
+ else:
337
+ raise ValueError(f"unknown type: {test_case.expected_result_type}")
338
+
339
+ @staticmethod
340
+ def compare_content_details(expected: LoadedContent, actual: Content):
341
+ assert_equal(expected.content_type, actual.media_type)
342
+ assert_equal(expected.name, actual.name)
343
+
344
+ def compare_one_content(self, comparator: CompareHelper, expected: LoadedContent, actual, index):
345
+ self.compare_content_details(expected, actual)
346
+ seg_id = actual.segments[0].uuid
347
+ comparator.compare(expected.data, self.content_service.get_output(seg_id), f"Content[{index}]")
348
+
349
+ def compare_content_list(self, comparator: CompareHelper, expected_outputs: List[IOContent], content: List):
350
+ assert_equal_len(expected_outputs, content)
351
+ for index, expected_ioc in enumerate(expected_outputs):
352
+ if not expected_ioc.no_content and len(expected_ioc.content_bytes) == 0:
353
+ expected = LoadedContent(self.did, expected_ioc, self.load_file(expected_ioc))
354
+ else:
355
+ expected = LoadedContent(self.did, expected_ioc, None)
356
+ self.compare_one_content(comparator, expected, content[index], index)
357
+
358
+ @staticmethod
359
+ def compare_one_metric(expected: Metric, result: Metric):
360
+ assert_equal_short(expected.name, result.name, "invalid metric name")
361
+ assert_equal_short(expected.value, result.value, f"invalid metric value for {expected.name}")
362
+ assert_keys_and_values(expected.tags, result.tags)
363
+
364
+ def compare_metrics(self, expected_metrics: List[Metric], results: List[Metric]):
365
+ if len(expected_metrics) > 0:
366
+ assert_equal_len_with_label(expected_metrics, results, "invalid metrics count")
367
+ for index, expected in enumerate(expected_metrics):
368
+ self.compare_one_metric(expected, results[index])
369
+
370
+ @staticmethod
371
+ def compare_one_message(expected: LogMessage, result: LogMessage):
372
+ assert_equal_short(expected.severity, result.severity, "message severity does not match")
373
+ # Look for regex characters:
374
+ if '*' in expected.message or '{' in expected.message or '^' in expected.message \
375
+ or '$' in expected.message:
376
+ match = re.search(expected.message, result.message)
377
+ assert match is not None, "message does not match regex"
378
+
379
+ else:
380
+ assert_equal_short(expected.message, result.message, "message value does not match")
381
+
382
+ def compare_log_messages(self, expected_messages: List[LogMessage], results: List[LogMessage]):
383
+ if len(expected_messages) > 0:
384
+ assert_equal_len_with_label(expected_messages, results, "invalid messages count")
385
+ for index, expected in enumerate(expected_messages):
386
+ self.compare_one_message(expected, results[index])
387
+
388
+ def has_saved_content__size(self, count: int):
389
+ assert_equal_with_label(
390
+ count, len(self.context.saved_content), "savedContent")
@@ -0,0 +1,104 @@
1
+ #
2
+ # DeltaFi - Data transformation and enrichment platform
3
+ #
4
+ # Copyright 2021-2025 DeltaFi Contributors <deltafi@deltafi.org>
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ from typing import List
20
+
21
+ from deltafi.result import IngressResult, IngressResultItem, IngressStatusEnum
22
+
23
+ from .assertions import *
24
+ from .framework import TestCaseBase, ActionTest, IOContent
25
+
26
+
27
+ class TimedIngressTestCase(TestCaseBase):
28
+ def __init__(self, fields: Dict):
29
+ super().__init__(fields)
30
+ self.memo = None
31
+ self.results = []
32
+ self.execute_immediate = False
33
+ self.status = IngressStatusEnum.HEALTHY
34
+ self.status_message = None
35
+
36
+ def expect_ingress_result(self,
37
+ memo: str = None,
38
+ exec_immed: bool = False,
39
+ status: IngressStatusEnum = IngressStatusEnum.HEALTHY,
40
+ status_message: str = None):
41
+ self.expected_result_type = IngressResult
42
+ self.memo = memo
43
+ self.execute_immediate = exec_immed
44
+ self.status = status
45
+ self.status_message = status_message
46
+
47
+ def add_ingress_result_item(self, content: List[IOContent], metadata: Dict, name: str = None,
48
+ annotations: Dict = None):
49
+ if annotations is None:
50
+ annotations = {}
51
+ self.results.append(
52
+ {
53
+ 'content': content,
54
+ 'metadata': metadata,
55
+ 'name': name,
56
+ 'annotations': annotations
57
+ }
58
+ )
59
+
60
+
61
+ class TimedIngressActionTest(ActionTest):
62
+ def __init__(self, package_name: str):
63
+ """
64
+ Provides structure for testing DeltaFi TimedIngress action
65
+ Args:
66
+ package_name: name of the actions package for finding resources
67
+ """
68
+ super().__init__(package_name)
69
+
70
+ def ingress(self, test_case: TimedIngressTestCase):
71
+ if test_case.expected_result_type == IngressResult:
72
+ self.expect_ingress_result(test_case)
73
+ else:
74
+ super().execute(test_case)
75
+
76
+ def expect_ingress_result(self, test_case: TimedIngressTestCase):
77
+ result = super().run_and_check_result_type(test_case, IngressResult)
78
+ self.assert_ingress_result(test_case, result)
79
+
80
+ def assert_ingress_result(self, test_case: TimedIngressTestCase, result: IngressResult):
81
+ assert_equal_short(test_case.memo, result.memo, "invalid memo")
82
+ assert_equal_short(test_case.execute_immediate, result.execute_immediate, "invalid execute_immediate")
83
+ assert_equal_short(test_case.status, result.status, "invalid status")
84
+ assert_equal_with_label(test_case.status_message, result.status_message, "invalid status_message")
85
+ self.compare_log_messages(test_case.expected_messages, result.messages)
86
+
87
+ assert_equal_len_with_label(test_case.results, result.ingress_result_items, "item count mismatch")
88
+ for index, ingress_item in enumerate(result.ingress_result_items):
89
+ self.compare_one_ingress_item(test_case, ingress_item, index)
90
+ expected = test_case.results[index]
91
+ if 'name' in expected:
92
+ assert_equal_with_label(expected["name"], ingress_item.delta_file_name, f"name[{index}]")
93
+
94
+ def compare_one_ingress_item(self, test_case: TimedIngressTestCase, result: IngressResultItem, index: int):
95
+ expected = test_case.results[index]
96
+
97
+ # Check output
98
+ self.compare_content_list(test_case.compare_tool, expected['content'], result.content)
99
+
100
+ # Check metadata
101
+ assert_keys_and_values(expected['metadata'], result.metadata)
102
+
103
+ # Check annotations
104
+ assert_keys_and_values(expected['annotations'], result.annotations)
@@ -0,0 +1,103 @@
1
+ #
2
+ # DeltaFi - Data transformation and enrichment platform
3
+ #
4
+ # Copyright 2021-2025 DeltaFi Contributors <deltafi@deltafi.org>
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ from typing import List
20
+
21
+ from deltafi.result import TransformResult, TransformResults
22
+
23
+ from .assertions import *
24
+ from .framework import TestCaseBase, ActionTest, IOContent
25
+
26
+
27
+ class TransformTestCase(TestCaseBase):
28
+ def __init__(self, fields: Dict):
29
+ super().__init__(fields)
30
+ self.results = []
31
+
32
+ def expect_transform_result(self):
33
+ self.expected_result_type = TransformResult
34
+
35
+ def expect_transform_results(self):
36
+ self.expected_result_type = TransformResults
37
+
38
+ def add_transform_result(self, content: List[IOContent], metadata: Dict, delete_metadata_keys: List[str],
39
+ annotations: Dict, name: str = None):
40
+ self.results.append(
41
+ {
42
+ 'content': content,
43
+ 'metadata': metadata,
44
+ 'delete_metadata_keys': delete_metadata_keys,
45
+ 'annotations': annotations,
46
+ 'name': name
47
+ }
48
+ )
49
+
50
+
51
+ class TransformActionTest(ActionTest):
52
+ def __init__(self, package_name: str):
53
+ """
54
+ Provides structure for testing DeltaFi Transform action
55
+ Args:
56
+ package_name: name of the actions package for finding resources
57
+ """
58
+ super().__init__(package_name)
59
+
60
+ def transform(self, test_case: TransformTestCase):
61
+ if test_case.expected_result_type == TransformResult:
62
+ self.expect_transform_result(test_case)
63
+ elif test_case.expected_result_type == TransformResults:
64
+ self.expect_transform_results(test_case)
65
+ else:
66
+ super().execute(test_case)
67
+
68
+ def expect_transform_result(self, test_case: TransformTestCase):
69
+ result = super().run_and_check_result_type(test_case, TransformResult)
70
+ self.assert_transform_result(test_case, result)
71
+
72
+ def expect_transform_results(self, test_case: TransformTestCase):
73
+ result = super().run_and_check_result_type(test_case, TransformResults)
74
+ self.assert_transform_results(test_case, result)
75
+
76
+ def assert_transform_results(self, test_case: TransformTestCase, result: TransformResults):
77
+ assert_equal_len_with_label(test_case.results, result.child_results, "invalid child count")
78
+ for index, child_result in enumerate(result.child_results):
79
+ self.compare_one_transform_result(test_case, child_result, index)
80
+ expected = test_case.results[index]
81
+ if 'name' in expected:
82
+ assert_equal_with_label(expected["name"], child_result.delta_file_name, f"name[{index}]")
83
+
84
+ def assert_transform_result(self, test_case: TransformTestCase, result: TransformResult):
85
+ self.compare_metrics(test_case.expected_metrics, result.metrics)
86
+ self.compare_log_messages(test_case.expected_messages, result.messages)
87
+ self.compare_one_transform_result(test_case, result, 0)
88
+
89
+ def compare_one_transform_result(self, test_case: TransformTestCase, result: TransformResult, index: int):
90
+ expected = test_case.results[index]
91
+
92
+ # Check output
93
+ self.compare_content_list(test_case.compare_tool, expected['content'], result.content)
94
+
95
+ # Check metadata
96
+ assert_keys_and_values(expected['metadata'], result.metadata)
97
+
98
+ # Check deleted metadata
99
+ for key in expected['delete_metadata_keys']:
100
+ assert_key_in(key, result.delete_metadata_keys)
101
+
102
+ # Check annotations
103
+ assert_keys_and_values(expected['annotations'], result.annotations)
deltafi/types.py ADDED
@@ -0,0 +1,31 @@
1
+ #
2
+ # DeltaFi - Data transformation and enrichment platform
3
+ #
4
+ # Copyright 2021-2025 DeltaFi Contributors <deltafi@deltafi.org>
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+
20
+ class PluginCoordinates(object):
21
+ def __init__(self, group_id: str, artifact_id: str, version: str):
22
+ self.group_id = group_id
23
+ self.artifact_id = artifact_id
24
+ self.version = version
25
+
26
+ def json(self):
27
+ return {
28
+ "groupId": self.group_id,
29
+ "artifactId": self.artifact_id,
30
+ "version": self.version
31
+ }