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.
- deltafi/__init__.py +3 -1
- deltafi/action.py +262 -102
- deltafi/actioneventqueue.py +29 -4
- deltafi/actiontype.py +7 -11
- deltafi/domain.py +241 -88
- deltafi/exception.py +1 -11
- deltafi/genericmodel.py +38 -0
- deltafi/input.py +6 -163
- deltafi/logger.py +16 -4
- deltafi/lookuptable.py +292 -0
- deltafi/metric.py +2 -2
- deltafi/plugin.py +374 -87
- deltafi/result.py +174 -172
- deltafi/resultmessage.py +56 -0
- deltafi/storage.py +20 -90
- deltafi/test_kit/__init__.py +19 -0
- deltafi/test_kit/assertions.py +56 -0
- deltafi/test_kit/compare_helpers.py +293 -0
- deltafi/test_kit/constants.py +23 -0
- deltafi/test_kit/egress.py +54 -0
- deltafi/test_kit/framework.py +390 -0
- deltafi/test_kit/timed_ingress.py +104 -0
- deltafi/test_kit/transform.py +103 -0
- deltafi/types.py +31 -0
- deltafi-2.40.0.dist-info/METADATA +82 -0
- deltafi-2.40.0.dist-info/RECORD +27 -0
- {deltafi-0.109.0.dist-info → deltafi-2.40.0.dist-info}/WHEEL +1 -1
- deltafi-0.109.0.dist-info/METADATA +0 -41
- deltafi-0.109.0.dist-info/RECORD +0 -15
|
@@ -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
|
+
}
|