deltafi 1.0.5__py3-none-any.whl → 1.0.7__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.

Potentially problematic release.


This version of deltafi might be problematic. Click here for more details.

deltafi/domain.py CHANGED
@@ -25,6 +25,7 @@ from deltafi.storage import ContentService, Segment
25
25
 
26
26
  class Context(NamedTuple):
27
27
  did: str
28
+ action_flow: str
28
29
  action_name: str
29
30
  source_filename: str
30
31
  ingress_flow: str
@@ -37,7 +38,9 @@ class Context(NamedTuple):
37
38
  @classmethod
38
39
  def create(cls, context: dict, hostname: str, content_service: ContentService, logger: Logger):
39
40
  did = context['did']
40
- action_name = context['name']
41
+ action_name_parts = context['name'].split(".")
42
+ action_flow = action_name_parts[0]
43
+ action_name = action_name_parts[1]
41
44
  if 'sourceFilename' in context:
42
45
  source_filename = context['sourceFilename']
43
46
  else:
@@ -49,6 +52,7 @@ class Context(NamedTuple):
49
52
  egress_flow = None
50
53
  system = context['systemName']
51
54
  return Context(did=did,
55
+ action_flow=action_flow,
52
56
  action_name=action_name,
53
57
  source_filename=source_filename,
54
58
  ingress_flow=ingress_flow,
deltafi/plugin.py CHANGED
@@ -270,7 +270,7 @@ class Plugin(object):
270
270
 
271
271
  response = {
272
272
  'did': event.context.did,
273
- 'action': event.context.action_name,
273
+ 'action': event.context.action_flow + "." + event.context.action_name,
274
274
  'start': start_time,
275
275
  'stop': time.time(),
276
276
  'type': result.result_type,
deltafi/result.py CHANGED
@@ -17,8 +17,8 @@
17
17
  #
18
18
 
19
19
  import abc
20
- from typing import Dict, List
21
20
  import uuid
21
+ from typing import Dict, List
22
22
 
23
23
  from deltafi.domain import Content, Context
24
24
  from deltafi.metric import Metric
@@ -123,40 +123,66 @@ class FormatResult(Result):
123
123
  def __init__(self, context: Context):
124
124
  super().__init__('format', 'FORMAT', context)
125
125
  self.content = None
126
+ self.delete_metadata_keys = []
126
127
  self.metadata = {}
127
128
 
128
129
  def add_metadata(self, key: str, value: str):
129
130
  self.metadata[key] = value
130
131
  return self
131
132
 
133
+ def delete_metadata_key(self, key: str):
134
+ self.delete_metadata_keys.append(key)
135
+ return self
136
+
132
137
  def set_content(self, content: Content):
133
138
  self.content = content
134
139
  return self
135
140
 
136
141
  def save_string_content(self, string_data: str, name: str, media_type: str):
137
142
  segment = self.context.content_service.put_str(self.context.did, string_data)
138
- self.content = Content(name=name, segments=[segment], media_type=media_type, content_service=self.context.content_service)
143
+ self.content = Content(name=name, segments=[segment], media_type=media_type,
144
+ content_service=self.context.content_service)
139
145
  return self
140
146
 
141
147
  def save_byte_content(self, byte_data: bytes, name: str, media_type: str):
142
148
  segment = self.context.content_service.put_bytes(self.context.did, byte_data)
143
- self.content = Content(name=name, segments=[segment], media_type=media_type, content_service=self.context.content_service)
149
+ self.content = Content(name=name, segments=[segment], media_type=media_type,
150
+ content_service=self.context.content_service)
144
151
  return self
145
152
 
146
153
  def response(self):
147
154
  return {
148
155
  'content': self.content.json(),
149
- 'metadata': self.metadata
156
+ 'metadata': self.metadata,
157
+ 'deleteMetadataKeys': self.delete_metadata_keys
150
158
  }
151
159
 
152
160
 
161
+ class ChildFormatResult:
162
+ def __init__(self, format_result: FormatResult = None):
163
+ self._did = str(uuid.uuid4())
164
+ self.format_result = format_result
165
+
166
+ @property
167
+ def did(self):
168
+ return self._did
169
+
170
+ def response(self):
171
+ res = self.format_result.response()
172
+ res["did"] = self._did
173
+ return res
174
+
175
+
153
176
  class FormatManyResult(Result):
154
177
  def __init__(self, context: Context):
155
178
  super().__init__('formatMany', 'FORMAT_MANY', context)
156
179
  self.format_results = []
157
180
 
158
- def add_format_result(self, format_result: FormatResult):
159
- self.format_results.append(format_result)
181
+ def add_format_result(self, format_result):
182
+ if isinstance(format_result, ChildFormatResult):
183
+ self.format_results.append(format_result)
184
+ else:
185
+ self.format_results.append(ChildFormatResult(format_result))
160
186
  return self
161
187
 
162
188
  def response(self):
@@ -184,12 +210,14 @@ class LoadResult(Result):
184
210
 
185
211
  def save_string_content(self, string_data: str, name: str, media_type: str):
186
212
  segment = self.context.content_service.put_str(self.context.did, string_data)
187
- self.content.append(Content(name=name, segments=[segment], media_type=media_type, content_service=self.context.content_service))
213
+ self.content.append(
214
+ Content(name=name, segments=[segment], media_type=media_type, content_service=self.context.content_service))
188
215
  return self
189
216
 
190
217
  def save_byte_content(self, byte_data: bytes, name: str, media_type: str):
191
218
  segment = self.context.content_service.put_bytes(self.context.did, byte_data)
192
- self.content.append(Content(name=name, segments=[segment], media_type=media_type, content_service=self.context.content_service))
219
+ self.content.append(
220
+ Content(name=name, segments=[segment], media_type=media_type, content_service=self.context.content_service))
193
221
  return self
194
222
 
195
223
  def add_metadata(self, key: str, value: str):
@@ -300,12 +328,14 @@ class TransformResult(Result):
300
328
 
301
329
  def save_string_content(self, string_data: str, name: str, media_type: str):
302
330
  segment = self.context.content_service.put_str(self.context.did, string_data)
303
- self.content.append(Content(name=name, segments=[segment], media_type=media_type, content_service=self.context.content_service))
331
+ self.content.append(
332
+ Content(name=name, segments=[segment], media_type=media_type, content_service=self.context.content_service))
304
333
  return self
305
334
 
306
335
  def save_byte_content(self, byte_data: bytes, name: str, media_type: str):
307
336
  segment = self.context.content_service.put_bytes(self.context.did, byte_data)
308
- self.content.append(Content(name=name, segments=[segment], media_type=media_type, content_service=self.context.content_service))
337
+ self.content.append(
338
+ Content(name=name, segments=[segment], media_type=media_type, content_service=self.context.content_service))
309
339
  return self
310
340
 
311
341
  def add_metadata(self, key: str, value: str):
@@ -0,0 +1,17 @@
1
+ #
2
+ # DeltaFi - Data transformation and enrichment platform
3
+ #
4
+ # Copyright 2021-2023 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
+ #
@@ -0,0 +1,47 @@
1
+ #
2
+ # DeltaFi - Data transformation and enrichment platform
3
+ #
4
+ # Copyright 2021-2023 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
+ from typing import Dict
19
+
20
+ from .constants import IGNORE_VALUE
21
+
22
+
23
+ def assert_equal(e, a):
24
+ assert e == a, f"{e} != {a}"
25
+
26
+
27
+ def assert_equal_with_label(e, a, l):
28
+ assert e == a, f"{l}. Expected:\n<<{e}>>\nBut was:\n<<{a}>>"
29
+
30
+
31
+ def assert_equal_len(e, a):
32
+ assert len(e) == len(a), f"{len(e)} != {len(a)}"
33
+
34
+
35
+ def assert_key_in(k, m):
36
+ assert k in m, f"{k} not found"
37
+
38
+
39
+ def assert_key_not_in(k, m):
40
+ assert k not in m, f"{k} found, but not expected"
41
+
42
+
43
+ def assert_keys_and_values(expected: Dict, actual: Dict):
44
+ for key in expected:
45
+ assert_key_in(key, actual)
46
+ if expected[key] != IGNORE_VALUE:
47
+ assert_equal(expected[key], actual[key])
@@ -0,0 +1,49 @@
1
+ #
2
+ # DeltaFi - Data transformation and enrichment platform
3
+ #
4
+ # Copyright 2021-2023 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
+ import json
19
+ from abc import ABC
20
+ from abc import abstractmethod
21
+ from typing import List
22
+
23
+ from deepdiff import DeepDiff
24
+
25
+ from .assertions import *
26
+
27
+
28
+ class CompareHelper(ABC):
29
+ @abstractmethod
30
+ def compare(self, expected: str, actual: str, label: str):
31
+ pass
32
+
33
+
34
+ class GenericCompareHelper(CompareHelper):
35
+ def compare(self, expected: str, actual: str, label: str):
36
+ assert_equal_with_label(expected, actual, label)
37
+
38
+
39
+ class JsonCompareHelper(CompareHelper):
40
+ def __init__(self, regex_exclusion_list: List):
41
+ self.excludes = regex_exclusion_list
42
+
43
+ def compare(self, expected: str, actual: str, label: str):
44
+ exp = json.loads(expected)
45
+ act = json.loads(actual)
46
+ diffs = DeepDiff(exp, act, exclude_regex_paths=self.excludes)
47
+ if len(diffs) > 0:
48
+ raise ValueError(f"{diffs}")
49
+ assert len(diffs) == 0
@@ -0,0 +1,23 @@
1
+ #
2
+ # DeltaFi - Data transformation and enrichment platform
3
+ #
4
+ # Copyright 2021-2023 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
+ INGRESS_FLOW = "ingress-flow"
20
+ EGRESS_FLOW = "egress-flow"
21
+ HOSTNAME = "HOSTNAME"
22
+ SYSTEM = "SYSTEM"
23
+ IGNORE_VALUE = "%%IGNORE%%"
@@ -0,0 +1,99 @@
1
+ #
2
+ # DeltaFi - Data transformation and enrichment platform
3
+ #
4
+ # Copyright 2021-2023 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
+ from typing import List
19
+
20
+ from deltafi.result import FormatResult, FormatManyResult
21
+
22
+ from .assertions import *
23
+ from .framework import TestCaseBase, ActionTest
24
+
25
+
26
+ class FormatTestCase(TestCaseBase):
27
+ def __init__(self, fields: Dict):
28
+ super().__init__(fields)
29
+ self.metadata = {}
30
+ self.delete_metadata_keys = []
31
+ self.expected_format_many_result = []
32
+
33
+ def expect_format_result(self, metadata: Dict, delete_metadata_keys: List[str]):
34
+ self.expected_result_type = FormatResult
35
+ self.metadata = metadata
36
+ self.delete_metadata_keys = delete_metadata_keys
37
+
38
+ def add_format_many_result(self, metadata: Dict, delete_metadata_keys: List):
39
+ self.expected_result_type = FormatManyResult
40
+ self.expected_format_many_result.append(
41
+ {
42
+ "metadata": metadata,
43
+ "delete_metadata_keys": delete_metadata_keys
44
+ }
45
+ )
46
+
47
+
48
+ class FormatActionTest(ActionTest):
49
+ def __init__(self, package_name: str):
50
+ """
51
+ Provides structure for testing DeltaFi Format action
52
+ Args:
53
+ package_name: name of the actions package for finding resources
54
+ """
55
+ super().__init__(package_name)
56
+
57
+ def format(self, test_case: FormatTestCase):
58
+ if test_case.expected_result_type == FormatManyResult:
59
+ self.expect_format_many_result(test_case)
60
+ elif test_case.expected_result_type == FormatResult:
61
+ self.expect_format_result(test_case)
62
+ else:
63
+ super().execute(test_case)
64
+
65
+ def expect_format_result(self, test_case: FormatTestCase):
66
+ result = super().run_and_check_result_type(test_case, FormatResult)
67
+ self.assert_format_result(test_case, result)
68
+
69
+ def expect_format_many_result(self, test_case: FormatTestCase):
70
+ result = super().run_and_check_result_type(test_case, FormatManyResult)
71
+ self.assert_format_many_result(test_case, result)
72
+
73
+ def assert_format_result(self, test_case: FormatTestCase, result: FormatResult):
74
+ # Check output
75
+ if result.content is None:
76
+ self.compare_all_output(test_case.compare_tool, [])
77
+ else:
78
+ self.compare_all_output(test_case.compare_tool, [result.content])
79
+
80
+ # Check metadata
81
+ assert_keys_and_values(test_case.metadata, result.metadata)
82
+
83
+ # Check deleted metadata
84
+ for key in test_case.delete_metadata_keys:
85
+ assert_key_in(key, result.delete_metadata_keys)
86
+
87
+ def assert_format_many_result(self, test_case: FormatTestCase, actual: FormatManyResult):
88
+ assert_equal_len(test_case.expected_format_many_result, actual.format_results)
89
+ for index, expected_child_result in enumerate(test_case.expected_format_many_result):
90
+ actual_child = actual.format_results[index]
91
+ self.compare_one_content(test_case.compare_tool,
92
+ self.expected_outputs[index],
93
+ actual_child.format_result.content,
94
+ index)
95
+
96
+ assert_keys_and_values(expected_child_result['metadata'],
97
+ actual_child.format_result.metadata)
98
+ for key in expected_child_result['delete_metadata_keys']:
99
+ assert_key_in(key, actual_child.format_result.delete_metadata_keys)
@@ -0,0 +1,326 @@
1
+ #
2
+ # DeltaFi - Data transformation and enrichment platform
3
+ #
4
+ # Copyright 2021-2023 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
+ import uuid
19
+ from abc import ABC
20
+ from importlib.resources import files
21
+ from typing import List
22
+
23
+ from deltafi.domain import DeltaFileMessage, Event, Content, Context
24
+ from deltafi.logger import get_logger
25
+ from deltafi.result import ErrorResult, FilterResult
26
+ from deltafi.storage import Segment
27
+
28
+ from .assertions import *
29
+ from .compare_helpers import GenericCompareHelper, CompareHelper
30
+ from .constants import *
31
+
32
+
33
+ class IOContent:
34
+ """
35
+ The IOContent class holds the details for loading input or output
36
+ content into the test framework.
37
+ Attributes:
38
+ file_name (str): The name of file in test/data.
39
+ content_name (str): The name of the content.
40
+ content_type (str): The media type of the content
41
+ offset (int): Offset to use in Segment
42
+ content_bytes (str): Bypass file read, and uses these bytes for content
43
+ """
44
+
45
+ def __init__(self, file_name: str, content_name: str = None,
46
+ content_type: str = None, offset: int = 0,
47
+ content_bytes: str = ""):
48
+ self.file_name = file_name
49
+ if content_name is None:
50
+ self.content_name = file_name
51
+ else:
52
+ self.content_name = content_name
53
+ if content_type is None:
54
+ self.content_type = IOContent.file_type(file_name)
55
+ else:
56
+ self.content_type = content_type
57
+ self.offset = offset
58
+ self.content_bytes = content_bytes
59
+
60
+ @classmethod
61
+ def file_type(cls, name: str):
62
+ if name.endswith(".json"):
63
+ return "application/json"
64
+ elif name.endswith(".xml"):
65
+ return "application/xnl"
66
+ elif name.endswith(".txt"):
67
+ return "text/plain"
68
+ else:
69
+ return "application/octet-stream"
70
+
71
+
72
+ class LoadedContent:
73
+ def __init__(self, did: str, ioc: IOContent, data: str):
74
+ self.name = ioc.content_name
75
+ self.content_type = ioc.content_type
76
+ self.offset = ioc.offset
77
+ if data is not None:
78
+ self.data = data
79
+ else:
80
+ self.data = ioc.content_bytes
81
+ self.segment = Segment.from_dict({
82
+ "uuid": str(uuid.uuid4()),
83
+ "offset": self.offset,
84
+ "size": len(self.data),
85
+ "did": did
86
+ })
87
+
88
+
89
+ class InternalContentService:
90
+ def __init__(self):
91
+ self.loaded_content = {}
92
+ self.outputs = {}
93
+
94
+ def load(self, content_list: List[LoadedContent]):
95
+ for c in content_list:
96
+ self.loaded_content[c.segment.uuid] = c
97
+
98
+ def put_str(self, did: str, string_data: str):
99
+ segment = Segment(uuid=str(uuid.uuid4()),
100
+ offset=0,
101
+ size=len(string_data),
102
+ did=did)
103
+ self.outputs[segment.uuid] = string_data
104
+ return segment
105
+
106
+ def get_str(self, segments: List[Segment]):
107
+ # TODO: String multiple segment ids together
108
+ seg_id = segments[0].uuid
109
+ return self.loaded_content[seg_id].data
110
+
111
+ def get_output(self, seg_id: str):
112
+ return self.outputs[seg_id]
113
+
114
+
115
+ class TestCaseBase(ABC):
116
+ def __init__(self, data: Dict):
117
+ """
118
+ A test case for DeltaFi python actions
119
+ :param data: Dict of test case fields
120
+ - action: instance of the action being tested
121
+ - data_dir: str: subdirectory name (e.g., test name) for locating test data files, i.e., test/data/{data_dir)
122
+ - compare_tool: (optional) CompareHelper instanced for comparing output content
123
+ - inputs: (optional) List[IOContent]: input content to action
124
+ - parameters: (optional) Dict: map of action input parameters
125
+ - in_meta: (optional) Dict: map of metadata as input to action
126
+ - in_domains: (optional) List[Domain]: list of domains as input to action
127
+ - in_enrichments: (optional) List[Domain]: list of enrichments as input to action
128
+ """
129
+ if "action" in data:
130
+ self.action = data["action"]
131
+ else:
132
+ raise ValueError("action is required")
133
+
134
+ if "data_dir" in data:
135
+ self.data_dir = data["data_dir"]
136
+ else:
137
+ raise ValueError("data_dir is required")
138
+
139
+ if "compare_tool" in data:
140
+ self.compare_tool = data["compare_tool"]
141
+ else:
142
+ self.compare_tool = GenericCompareHelper()
143
+
144
+ self.inputs = data["inputs"] if "inputs" in data else []
145
+ self.file_name = data["file_name"] if "file_name" in data else "filename"
146
+ self.outputs = data["outputs"] if "outputs" in data else []
147
+ self.parameters = data["parameters"] if "parameters" in data else {}
148
+ self.in_meta = data["in_meta"] if "in_meta" in data else {}
149
+ self.in_domains = data["in_domains"] if "in_domains" in data else []
150
+ self.in_enrichments = data["in_enrichments"] if "in_enrichments" in data else []
151
+ self.expected_result_type = None
152
+ self.cause_regex = None
153
+ self.context_regex = None
154
+
155
+ def expect_error_result(self, cause: str, context: str):
156
+ """
157
+ A Sets the expected output of the action to an Error Result
158
+ :param cause: the expected error cause
159
+ :param context: the expected error context
160
+ """
161
+ self.expected_result_type = ErrorResult
162
+ self.cause_regex = cause
163
+ self.context_regex = context
164
+
165
+ def expect_filter_result(self, cause: str):
166
+ """
167
+ A Sets the expected output of the action to a Filter Result
168
+ :param cause: the expected filter cause (message)
169
+ """
170
+ self.expected_result_type = FilterResult
171
+ self.cause_regex = cause
172
+
173
+
174
+ class ActionTest(ABC):
175
+ def __init__(self, package_name: str):
176
+ """
177
+ Provides structure for testing DeltaFi actions
178
+ Args:
179
+ package_name: name of the actions package for finding resources
180
+ """
181
+ self.content_service = InternalContentService()
182
+ self.did = ""
183
+ self.expected_outputs = []
184
+ self.loaded_inputs = []
185
+ self.package_name = package_name
186
+ self.res_path = ""
187
+
188
+ def __reset__(self):
189
+ self.content_service = InternalContentService()
190
+ self.did = str(uuid.uuid4())
191
+ self.expected_outputs = []
192
+ self.loaded_inputs = []
193
+ self.res_path = ""
194
+
195
+ def load_file(self, ioc: IOContent):
196
+ file_res = self.res_path.joinpath(ioc.file_name)
197
+ with file_res.open("r") as f:
198
+ contents = f.read()
199
+ return contents
200
+
201
+ def get_contents(self, test_case: TestCaseBase):
202
+ pkg_path = files(self.package_name)
203
+ self.res_path = pkg_path.joinpath(f"test/data/{test_case.data_dir}/")
204
+
205
+ # Load inputs
206
+ for input_ioc in test_case.inputs:
207
+ if len(input_ioc.content_bytes) == 0:
208
+ self.loaded_inputs.append(LoadedContent(self.did, input_ioc, self.load_file(input_ioc)))
209
+ else:
210
+ self.loaded_inputs.append(LoadedContent(self.did, input_ioc, None))
211
+
212
+ # Load expected outputs
213
+ for output_ioc in test_case.outputs:
214
+ if len(output_ioc.content_bytes) == 0:
215
+ self.expected_outputs.append(LoadedContent(self.did, output_ioc, self.load_file(output_ioc)))
216
+ else:
217
+ self.expected_outputs.append(LoadedContent(self.did, output_ioc, None))
218
+
219
+ def make_content_list(self, test_case: TestCaseBase):
220
+ content_list = []
221
+ for loaded_input in self.loaded_inputs:
222
+ c = Content(name=loaded_input.name,
223
+ segments=[loaded_input.segment],
224
+ media_type=loaded_input.content_type,
225
+ content_service=self.content_service)
226
+ content_list.append(c)
227
+ loaded_input.content = c
228
+
229
+ return content_list
230
+
231
+ def make_df_msg(self, test_case: TestCaseBase):
232
+ content_list = self.make_content_list(test_case)
233
+ self.content_service.load(self.loaded_inputs)
234
+
235
+ return DeltaFileMessage(metadata=test_case.in_meta,
236
+ content_list=content_list,
237
+ domains=test_case.in_domains,
238
+ enrichments=test_case.in_enrichments)
239
+
240
+ def make_context(self, test_case: TestCaseBase):
241
+ action_name = INGRESS_FLOW + "." + test_case.action.__class__.__name__
242
+ return Context(did=self.did,
243
+ action_flow=INGRESS_FLOW,
244
+ action_name=action_name,
245
+ source_filename=test_case.file_name,
246
+ ingress_flow=INGRESS_FLOW,
247
+ egress_flow=EGRESS_FLOW,
248
+ system=SYSTEM,
249
+ hostname=HOSTNAME,
250
+ content_service=self.content_service,
251
+ logger=get_logger())
252
+
253
+ def make_event(self, test_case: TestCaseBase):
254
+ return Event(
255
+ delta_file_messages=[self.make_df_msg(test_case)],
256
+ context=self.make_context(test_case),
257
+ params=test_case.parameters,
258
+ queue_name="",
259
+ return_address="")
260
+
261
+ def call_action(self, test_case: TestCaseBase):
262
+ self.get_contents(test_case)
263
+ return test_case.action.execute(self.make_event(test_case))
264
+
265
+ def run_and_check_result_type(self, test_case: TestCaseBase, result_type):
266
+ self.__reset__()
267
+ result = self.call_action(test_case)
268
+
269
+ if not isinstance(result, result_type):
270
+ raise ValueError(f"Result type {result.__class__.__name__} does not match {result_type.__name__}")
271
+
272
+ return result
273
+
274
+ def execute_error(self, test_case: TestCaseBase):
275
+ result = self.run_and_check_result_type(test_case, ErrorResult)
276
+ resp = result.response()
277
+ assert resp['cause'] == test_case.cause_regex
278
+ assert resp['context'] == test_case.context_regex
279
+
280
+ def execute_filter(self, test_case):
281
+ result = self.run_and_check_result_type(test_case, FilterResult)
282
+ resp = result.response()
283
+ assert resp['message'] == test_case.cause_regex
284
+
285
+ def execute(self, test_case: TestCaseBase):
286
+ if isinstance(test_case.expected_result_type, ErrorResult.__class__):
287
+ self.execute_error(test_case)
288
+ elif isinstance(test_case.expected_result_type, FilterResult.__class__):
289
+ self.execute_filter(test_case)
290
+ else:
291
+ raise ValueError(f"unknown type: {test_case.expected_result_type}")
292
+
293
+ def compare_content_details(self, expected: LoadedContent, actual: Content):
294
+ assert_equal(expected.content_type, actual.media_type)
295
+ assert_equal(expected.name, actual.name)
296
+
297
+ def compare_one_content(self, comparitor: CompareHelper, expected: LoadedContent, actual, index):
298
+ self.compare_content_details(expected, actual)
299
+ seg_id = actual.segments[0].uuid
300
+ comparitor.compare(
301
+ expected.data,
302
+ self.content_service.get_output(seg_id),
303
+ f"Content[{index}]"
304
+ )
305
+
306
+ def compare_all_output(self, comparitor: CompareHelper, content: List):
307
+ assert_equal_len(self.expected_outputs, content)
308
+ for index, expected in enumerate(self.expected_outputs):
309
+ self.compare_one_content(comparitor, expected, content[index], index)
310
+
311
+ def compare_domains(self, comparitor: CompareHelper, expected_items: List[Dict], results: List[Dict]):
312
+ assert_equal_len(expected_items, results)
313
+ for index, expected in enumerate(expected_items):
314
+ actual = results[index]
315
+ assert_equal(expected['name'], actual['name'])
316
+ assert_equal(expected['mediaType'], actual['mediaType'])
317
+
318
+ expected_value = expected['value']
319
+ if type(expected_value) == str:
320
+ comparitor.compare(expected_value, actual['value'], f"Domain[{index}]")
321
+ elif type(expected_value) == IOContent:
322
+ expected_data = self.load_file(expected_value)
323
+ comparitor.compare(expected_data, actual['value'], f"Domain[{index}]")
324
+ else:
325
+ raise ValueError(
326
+ f"unknown expected_value type: {type(expected_value)}")
@@ -0,0 +1,121 @@
1
+ #
2
+ # DeltaFi - Data transformation and enrichment platform
3
+ #
4
+ # Copyright 2021-2023 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
+ from typing import List
19
+
20
+ from deltafi.result import LoadResult, ReinjectResult
21
+
22
+ from .assertions import *
23
+ from .compare_helpers import GenericCompareHelper, CompareHelper
24
+ from .framework import TestCaseBase, ActionTest, IOContent
25
+
26
+
27
+ class LoadTestCase(TestCaseBase):
28
+ def __init__(self, fields: Dict):
29
+ super().__init__(fields)
30
+ self.metadata = {}
31
+ self.delete_metadata_keys = []
32
+ self.annotations = {}
33
+ self.domains = []
34
+ self.domain_cmp_tool = GenericCompareHelper()
35
+ self.children = []
36
+
37
+ def set_domain_compare_tool(self, helper: CompareHelper):
38
+ self.domain_cmp_tool = helper
39
+
40
+ def expect_load_result(self, metadata: Dict, delete_metadata_keys: List[str], annotations: Dict, domains: List):
41
+ self.expected_result_type = LoadResult
42
+ self.metadata = metadata
43
+ self.delete_metadata_keys = delete_metadata_keys
44
+ self.annotations = annotations
45
+ self.domains = domains
46
+
47
+ def add_reinject_child(self, filename: str, flow: str, content: IOContent, metadata: Dict):
48
+ self.expected_result_type = ReinjectResult
49
+ self.children.append(
50
+ {
51
+ "filename": filename,
52
+ "flow": flow,
53
+ "content": content,
54
+ "metadata": metadata
55
+ }
56
+ )
57
+
58
+
59
+ class LoadActionTest(ActionTest):
60
+ def __init__(self, package_name: str):
61
+ """
62
+ Provides structure for testing DeltaFi Load action
63
+ Args:
64
+ package_name: name of the actions package for finding resources
65
+ """
66
+ super().__init__(package_name)
67
+
68
+ def load(self, test_case: LoadTestCase):
69
+ if test_case.expected_result_type == ReinjectResult:
70
+ self.expect_reinject_result(test_case)
71
+ elif test_case.expected_result_type == LoadResult:
72
+ self.expect_load_result(test_case)
73
+ else:
74
+ super().execute(test_case)
75
+
76
+ def expect_load_result(self, test_case: LoadTestCase):
77
+ result = super().run_and_check_result_type(test_case, LoadResult)
78
+ self.assert_load_result(test_case, result)
79
+
80
+ def expect_reinject_result(self, test_case: LoadTestCase):
81
+ result = super().run_and_check_result_type(test_case, ReinjectResult)
82
+ self.assert_reinject_result(test_case, result)
83
+
84
+ def assert_load_result(self, test_case: LoadTestCase, result: LoadResult):
85
+ # Check output
86
+ self.compare_all_output(test_case.compare_tool, result.content)
87
+
88
+ # Check metadata
89
+ assert_keys_and_values(test_case.metadata, result.metadata)
90
+
91
+ # Check deleted metadata
92
+ for key in test_case.delete_metadata_keys:
93
+ assert_key_in(key, result.delete_metadata_keys)
94
+
95
+ # Check annotations
96
+ assert_keys_and_values(test_case.annotations, result.annotations)
97
+
98
+ # Check domains
99
+ self.compare_domains(test_case.domain_cmp_tool, test_case.domains, result.domains)
100
+
101
+ def assert_reinject_result(self, test_case: LoadTestCase, actual: ReinjectResult):
102
+ assert_equal_len(test_case.children, actual.children)
103
+ for index, expected in enumerate(test_case.children):
104
+ reinject_child = actual.children[index]
105
+ assert_equal(expected['filename'], reinject_child.filename)
106
+ assert_equal(expected['flow'], reinject_child.flow)
107
+ assert_keys_and_values(expected['metadata'], reinject_child.metadata)
108
+
109
+ expected_value = expected['content']
110
+ child_content = reinject_child.content[0]
111
+ seg_id = child_content.segments[0].uuid
112
+ actual_content = self.content_service.get_output(seg_id)
113
+
114
+ if type(expected_value) == str:
115
+ test_case.domain_cmp_tool.compare(expected_value, actual_content, f"RI_child[{index}]")
116
+ elif type(expected_value) == IOContent:
117
+ expected_data = self.load_file(expected_value)
118
+ test_case.domain_cmp_tool.compare(expected_data, actual_content, f"RI_child[{index}]")
119
+ else:
120
+ raise ValueError(
121
+ f"unknown expected_value type: {type(expected_value)}")
@@ -0,0 +1,71 @@
1
+ #
2
+ # DeltaFi - Data transformation and enrichment platform
3
+ #
4
+ # Copyright 2021-2023 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
+ from typing import List
19
+
20
+ from deltafi.result import TransformResult
21
+
22
+ from .assertions import *
23
+ from .framework import TestCaseBase, ActionTest
24
+
25
+
26
+ class TransformTestCase(TestCaseBase):
27
+ def __init__(self, fields: Dict):
28
+ super().__init__(fields)
29
+ self.metadata = {}
30
+ self.delete_metadata_keys = []
31
+ self.annotations = {}
32
+
33
+ def expect_transform_result(self, metadata: Dict, delete_metadata_keys: List[str], annotations: Dict):
34
+ self.expected_result_type = TransformResult
35
+ self.metadata = metadata
36
+ self.delete_metadata_keys = delete_metadata_keys
37
+ self.annotations = annotations
38
+
39
+
40
+ class TransformActionTest(ActionTest):
41
+ def __init__(self, package_name: str):
42
+ """
43
+ Provides structure for testing DeltaFi Transform action
44
+ Args:
45
+ package_name: name of the actions package for finding resources
46
+ """
47
+ super().__init__(package_name)
48
+
49
+ def transform(self, test_case: TransformTestCase):
50
+ if test_case.expected_result_type == TransformResult:
51
+ self.expect_transform_result(test_case)
52
+ else:
53
+ super().execute(test_case)
54
+
55
+ def expect_transform_result(self, test_case: TransformTestCase):
56
+ result = super().run_and_check_result_type(test_case, TransformResult)
57
+ self.assert_transform_result(test_case, result)
58
+
59
+ def assert_transform_result(self, test_case: TransformTestCase, result: TransformResult):
60
+ # Check output
61
+ self.compare_all_output(test_case.compare_tool, result.content)
62
+
63
+ # Check metadata
64
+ assert_keys_and_values(test_case.metadata, result.metadata)
65
+
66
+ # Check deleted metadata
67
+ for key in test_case.delete_metadata_keys:
68
+ assert_key_in(key, result.delete_metadata_keys)
69
+
70
+ # Check annotations
71
+ assert_keys_and_values(test_case.annotations, result.annotations)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: deltafi
3
- Version: 1.0.5
3
+ Version: 1.0.7
4
4
  Summary: SDK for DeltaFi plugins and actions
5
5
  License: Apache License, Version 2.0
6
6
  Keywords: deltafi
@@ -24,6 +24,7 @@ Classifier: Programming Language :: Python :: 3.7
24
24
  Classifier: Programming Language :: Python :: 3.8
25
25
  Classifier: Programming Language :: Python :: 3.9
26
26
  Classifier: Topic :: Software Development
27
+ Requires-Dist: deepdiff (>=6.3.1)
27
28
  Requires-Dist: json-logging (>=1.3.0)
28
29
  Requires-Dist: minio (>=7.1.15)
29
30
  Requires-Dist: pydantic (>=1.10.9,<2.0.0)
@@ -0,0 +1,23 @@
1
+ deltafi/__init__.py,sha256=PNMUt1SbY5DhoTxle11dPv21Pr_bo8paVc7TVu4Cs7E,706
2
+ deltafi/action.py,sha256=ssbSGP-Nn15mD2UdHXmbBuNnVw5vPZY88RZRCV9xqMw,7330
3
+ deltafi/actioneventqueue.py,sha256=HR1n-Co5Nq8IarYKR7Uzydi8mkCdOZbC-yJWHuA6KwA,1981
4
+ deltafi/actiontype.py,sha256=gt8j4PgYuxoRbif5-FJi1fu4eLSy2J4oVbQ2qceRgqM,985
5
+ deltafi/domain.py,sha256=-0ad8dGq-JcLyTnrIXHSMr7ZgBnailUJSZn1yPwEHI8,11197
6
+ deltafi/exception.py,sha256=qQ2TY2QPtMU9t5RH8jmFo_cX98AJ-zOFWP_Wfm5U6qQ,1149
7
+ deltafi/input.py,sha256=DuY280ZglZxEUkhjTtyr0g9Ohu2drn16UVhASH244nA,6290
8
+ deltafi/logger.py,sha256=q76R_Gn8BfcASH3Zy0V82m5ot34bjTnSELyKbjhvx3E,2136
9
+ deltafi/metric.py,sha256=eIDjZQVO53KuAFoEtjLNFwqMrp_7BC0Td9GLD1tb6sE,967
10
+ deltafi/plugin.py,sha256=dIBbhwRLbBlF2NyuRz09-Jj8BSIkvJa74JkAzsfiYBc,11700
11
+ deltafi/result.py,sha256=fRo83BZNcTfIDYDePGBJuB84FOnpgBiR23zbedJ2mD8,11512
12
+ deltafi/storage.py,sha256=toq58EPZgzj2TfDF-YpFUmRnsWSjACA0KQAZzkM04xU,2740
13
+ deltafi/test_kit/__init__.py,sha256=PNMUt1SbY5DhoTxle11dPv21Pr_bo8paVc7TVu4Cs7E,706
14
+ deltafi/test_kit/assertions.py,sha256=2yJKCLTenJW44CprFLGorqfjVC6_6z-oAfPY1Q7J__0,1377
15
+ deltafi/test_kit/compare_helpers.py,sha256=xeP6ToXvlCxVK3LX4glf9VsDfAmOoexYjkQpltfZ2T8,1580
16
+ deltafi/test_kit/constants.py,sha256=epz0OS-qILcc8t7iIs5B3m2-wvZx8FpYpyb19ZsImbI,833
17
+ deltafi/test_kit/format.py,sha256=NTG7UnwvXQlzn6WqKjMG9EZg0tKTu1g3CcNDD_6AQwk,4070
18
+ deltafi/test_kit/framework.py,sha256=fEdllkV8luBwdZYkwMZo7-BzgRO_GIcg5e6lMyTD8fI,12855
19
+ deltafi/test_kit/load.py,sha256=0K5vCQXbXl5pos9Yg_4qcZ9bJYfdQUGUosTxncv0xmw,4854
20
+ deltafi/test_kit/transform.py,sha256=Eeqxev_dxoiFw72eAlYJ-3MDQH-kJtuHRGSIMbaatrg,2607
21
+ deltafi-1.0.7.dist-info/METADATA,sha256=x24guqKi-7KbkMFCqJd-XGqgbeNGf7zhldbX34xfouQ,1749
22
+ deltafi-1.0.7.dist-info/WHEEL,sha256=7Z8_27uaHI_UZAc4Uox4PpBhQ9Y5_modZXWMxtUi4NU,88
23
+ deltafi-1.0.7.dist-info/RECORD,,
@@ -1,15 +0,0 @@
1
- deltafi/__init__.py,sha256=PNMUt1SbY5DhoTxle11dPv21Pr_bo8paVc7TVu4Cs7E,706
2
- deltafi/action.py,sha256=ssbSGP-Nn15mD2UdHXmbBuNnVw5vPZY88RZRCV9xqMw,7330
3
- deltafi/actioneventqueue.py,sha256=HR1n-Co5Nq8IarYKR7Uzydi8mkCdOZbC-yJWHuA6KwA,1981
4
- deltafi/actiontype.py,sha256=gt8j4PgYuxoRbif5-FJi1fu4eLSy2J4oVbQ2qceRgqM,985
5
- deltafi/domain.py,sha256=x0tS5DKv9tXYJK7eda1SVkkbOhRk_4scpXMfU-FZSJM,11025
6
- deltafi/exception.py,sha256=qQ2TY2QPtMU9t5RH8jmFo_cX98AJ-zOFWP_Wfm5U6qQ,1149
7
- deltafi/input.py,sha256=DuY280ZglZxEUkhjTtyr0g9Ohu2drn16UVhASH244nA,6290
8
- deltafi/logger.py,sha256=q76R_Gn8BfcASH3Zy0V82m5ot34bjTnSELyKbjhvx3E,2136
9
- deltafi/metric.py,sha256=eIDjZQVO53KuAFoEtjLNFwqMrp_7BC0Td9GLD1tb6sE,967
10
- deltafi/plugin.py,sha256=7eHU-P8GIqmK8fz9Re7sOWafZ9wxOayifWmmnyKXRrw,11666
11
- deltafi/result.py,sha256=aIFyytP5fOQBXmV-8IQleR5cQQrKVW36DEuOJSAGCgA,10706
12
- deltafi/storage.py,sha256=toq58EPZgzj2TfDF-YpFUmRnsWSjACA0KQAZzkM04xU,2740
13
- deltafi-1.0.5.dist-info/METADATA,sha256=ihKnu4QL1beQdQr8Js6c0cWwOBU0VIth8KEzyNW2P3w,1715
14
- deltafi-1.0.5.dist-info/WHEEL,sha256=7Z8_27uaHI_UZAc4Uox4PpBhQ9Y5_modZXWMxtUi4NU,88
15
- deltafi-1.0.5.dist-info/RECORD,,