deltafi 2.0rc1713535554033__tar.gz → 2.0rc1715691282958__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of deltafi might be problematic. Click here for more details.
- {deltafi-2.0rc1713535554033 → deltafi-2.0rc1715691282958}/PKG-INFO +5 -5
- {deltafi-2.0rc1713535554033 → deltafi-2.0rc1715691282958}/deltafi/action.py +13 -9
- {deltafi-2.0rc1713535554033 → deltafi-2.0rc1715691282958}/deltafi/actiontype.py +2 -0
- {deltafi-2.0rc1713535554033 → deltafi-2.0rc1715691282958}/deltafi/domain.py +52 -24
- {deltafi-2.0rc1713535554033 → deltafi-2.0rc1715691282958}/deltafi/plugin.py +7 -4
- {deltafi-2.0rc1713535554033 → deltafi-2.0rc1715691282958}/deltafi/result.py +4 -4
- deltafi-2.0rc1715691282958/deltafi/test_kit/compare_helpers.py +293 -0
- {deltafi-2.0rc1713535554033 → deltafi-2.0rc1715691282958}/deltafi/test_kit/framework.py +16 -52
- {deltafi-2.0rc1713535554033 → deltafi-2.0rc1715691282958}/pyproject.toml +8 -8
- deltafi-2.0rc1713535554033/deltafi/test_kit/compare_helpers.py +0 -50
- {deltafi-2.0rc1713535554033 → deltafi-2.0rc1715691282958}/README.md +0 -0
- {deltafi-2.0rc1713535554033 → deltafi-2.0rc1715691282958}/deltafi/__init__.py +0 -0
- {deltafi-2.0rc1713535554033 → deltafi-2.0rc1715691282958}/deltafi/actioneventqueue.py +0 -0
- {deltafi-2.0rc1713535554033 → deltafi-2.0rc1715691282958}/deltafi/exception.py +0 -0
- {deltafi-2.0rc1713535554033 → deltafi-2.0rc1715691282958}/deltafi/genericmodel.py +0 -0
- {deltafi-2.0rc1713535554033 → deltafi-2.0rc1715691282958}/deltafi/input.py +0 -0
- {deltafi-2.0rc1713535554033 → deltafi-2.0rc1715691282958}/deltafi/logger.py +0 -0
- {deltafi-2.0rc1713535554033 → deltafi-2.0rc1715691282958}/deltafi/metric.py +0 -0
- {deltafi-2.0rc1713535554033 → deltafi-2.0rc1715691282958}/deltafi/storage.py +0 -0
- {deltafi-2.0rc1713535554033 → deltafi-2.0rc1715691282958}/deltafi/test_kit/__init__.py +0 -0
- {deltafi-2.0rc1713535554033 → deltafi-2.0rc1715691282958}/deltafi/test_kit/assertions.py +0 -0
- {deltafi-2.0rc1713535554033 → deltafi-2.0rc1715691282958}/deltafi/test_kit/constants.py +0 -0
- {deltafi-2.0rc1713535554033 → deltafi-2.0rc1715691282958}/deltafi/test_kit/domain.py +0 -0
- {deltafi-2.0rc1713535554033 → deltafi-2.0rc1715691282958}/deltafi/test_kit/egress.py +0 -0
- {deltafi-2.0rc1713535554033 → deltafi-2.0rc1715691282958}/deltafi/test_kit/enrich.py +0 -0
- {deltafi-2.0rc1713535554033 → deltafi-2.0rc1715691282958}/deltafi/test_kit/transform.py +0 -0
- {deltafi-2.0rc1713535554033 → deltafi-2.0rc1715691282958}/deltafi/test_kit/validate.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: deltafi
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.0rc1715691282958
|
|
4
4
|
Summary: SDK for DeltaFi plugins and actions
|
|
5
5
|
License: Apache License, Version 2.0
|
|
6
6
|
Keywords: deltafi
|
|
@@ -20,11 +20,11 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
20
20
|
Classifier: Topic :: Software Development
|
|
21
21
|
Requires-Dist: deepdiff (>=6.7.1)
|
|
22
22
|
Requires-Dist: json-logging (>=1.3.0)
|
|
23
|
-
Requires-Dist: minio (>=7.2.
|
|
24
|
-
Requires-Dist: pydantic (>=2.
|
|
25
|
-
Requires-Dist: redis (>=5.0.
|
|
23
|
+
Requires-Dist: minio (>=7.2.5)
|
|
24
|
+
Requires-Dist: pydantic (>=2.7.1)
|
|
25
|
+
Requires-Dist: redis (>=5.0.4)
|
|
26
26
|
Requires-Dist: requests (>=2.31.0)
|
|
27
|
-
Requires-Dist: urllib3 (>=2.1
|
|
27
|
+
Requires-Dist: urllib3 (>=2.2.1)
|
|
28
28
|
Project-URL: Bug Reports, https://chat.deltafi.org/deltafi/channels/bug-reports
|
|
29
29
|
Project-URL: Documentation, https://docs.deltafi.org/#/
|
|
30
30
|
Project-URL: Source Code, https://gitlab.com/deltafi/deltafi
|
|
@@ -20,8 +20,8 @@ from abc import ABC, abstractmethod
|
|
|
20
20
|
from typing import Any, List
|
|
21
21
|
|
|
22
22
|
from deltafi.actiontype import ActionType
|
|
23
|
-
from deltafi.genericmodel import GenericModel
|
|
24
23
|
from deltafi.domain import Context, DeltaFileMessage
|
|
24
|
+
from deltafi.genericmodel import GenericModel
|
|
25
25
|
from deltafi.input import EgressInput, TransformInput
|
|
26
26
|
from deltafi.result import *
|
|
27
27
|
from pydantic import BaseModel
|
|
@@ -48,20 +48,23 @@ class Action(ABC):
|
|
|
48
48
|
def execute_action(self, event):
|
|
49
49
|
if event.delta_file_messages is None or not len(event.delta_file_messages):
|
|
50
50
|
raise RuntimeError(f"Received event with no delta file messages for did {event.context.did}")
|
|
51
|
-
|
|
52
51
|
if event.context.collect is not None:
|
|
53
|
-
result = self.execute(
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
result = self.execute(
|
|
53
|
+
event.context,
|
|
54
|
+
self.collect([self.build_input(event.context, delta_file_message)
|
|
55
|
+
for delta_file_message in event.delta_file_messages]),
|
|
56
|
+
self.param_class().model_validate(event.params))
|
|
56
57
|
else:
|
|
57
|
-
result = self.execute(
|
|
58
|
-
|
|
58
|
+
result = self.execute(
|
|
59
|
+
event.context,
|
|
60
|
+
self.build_input(event.context, event.delta_file_messages[0]),
|
|
61
|
+
self.param_class().model_validate(event.params))
|
|
59
62
|
|
|
60
63
|
self.validate_type(result)
|
|
61
64
|
return result
|
|
62
65
|
|
|
63
66
|
@staticmethod
|
|
64
|
-
def param_class(
|
|
67
|
+
def param_class():
|
|
65
68
|
"""Factory method to create and return an empty GenericModel instance.
|
|
66
69
|
|
|
67
70
|
Returns
|
|
@@ -110,7 +113,8 @@ class TimedIngressAction(Action, ABC):
|
|
|
110
113
|
|
|
111
114
|
class TransformAction(Action, ABC):
|
|
112
115
|
def __init__(self, description: str):
|
|
113
|
-
super().__init__(ActionType.TRANSFORM, description,
|
|
116
|
+
super().__init__(ActionType.TRANSFORM, description,
|
|
117
|
+
(TransformResult, TransformResults, ErrorResult, FilterResult))
|
|
114
118
|
|
|
115
119
|
def build_input(self, context: Context, delta_file_message: DeltaFileMessage):
|
|
116
120
|
return TransformInput(content=delta_file_message.content_list, metadata=delta_file_message.metadata)
|
|
@@ -40,13 +40,15 @@ class ActionExecution(NamedTuple):
|
|
|
40
40
|
|
|
41
41
|
class Context(NamedTuple):
|
|
42
42
|
did: str
|
|
43
|
-
|
|
43
|
+
delta_file_name: str
|
|
44
|
+
data_source: str
|
|
45
|
+
flow_name: str
|
|
46
|
+
flow_id: str
|
|
44
47
|
action_name: str
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
egress_flow: str
|
|
48
|
-
system: str
|
|
48
|
+
action_id: str
|
|
49
|
+
action_version: str
|
|
49
50
|
hostname: str
|
|
51
|
+
system_name: str
|
|
50
52
|
content_service: ContentService
|
|
51
53
|
collect: dict = None
|
|
52
54
|
collected_dids: List[str] = None
|
|
@@ -56,19 +58,42 @@ class Context(NamedTuple):
|
|
|
56
58
|
@classmethod
|
|
57
59
|
def create(cls, context: dict, hostname: str, content_service: ContentService, logger: Logger):
|
|
58
60
|
did = context['did']
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
action_name = action_name_parts[1]
|
|
62
|
-
if 'sourceFilename' in context:
|
|
63
|
-
source_filename = context['sourceFilename']
|
|
61
|
+
if 'deltaFileName' in context:
|
|
62
|
+
delta_file_name = context['deltaFileName']
|
|
64
63
|
else:
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
egress_flow = context['egressFlow']
|
|
64
|
+
delta_file_name = None
|
|
65
|
+
if 'dataSource' in context:
|
|
66
|
+
data_source = context['dataSource']
|
|
69
67
|
else:
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
data_source = None
|
|
69
|
+
if 'flowName' in context:
|
|
70
|
+
flow_name = context['flowName']
|
|
71
|
+
else:
|
|
72
|
+
flow_name = None
|
|
73
|
+
if 'flowId' in context:
|
|
74
|
+
flow_id = context['flowId']
|
|
75
|
+
else:
|
|
76
|
+
flow_id = None
|
|
77
|
+
if 'actionName' in context:
|
|
78
|
+
action_name = context['actionName']
|
|
79
|
+
else:
|
|
80
|
+
action_name = None
|
|
81
|
+
if 'actionId' in context:
|
|
82
|
+
action_id = context['actionId']
|
|
83
|
+
else:
|
|
84
|
+
action_id = None
|
|
85
|
+
if 'actionVersion' in context:
|
|
86
|
+
action_version = context['actionVersion']
|
|
87
|
+
else:
|
|
88
|
+
action_version = None
|
|
89
|
+
if 'hostname' in context:
|
|
90
|
+
hostname = context['hostname']
|
|
91
|
+
else:
|
|
92
|
+
hostname = None
|
|
93
|
+
if 'systemName' in context:
|
|
94
|
+
system_name = context['systemName']
|
|
95
|
+
else:
|
|
96
|
+
system_name = None
|
|
72
97
|
if 'collect' in context:
|
|
73
98
|
collect = context['collect']
|
|
74
99
|
else:
|
|
@@ -81,18 +106,21 @@ class Context(NamedTuple):
|
|
|
81
106
|
memo = context['memo']
|
|
82
107
|
else:
|
|
83
108
|
memo = None
|
|
109
|
+
|
|
84
110
|
return Context(did=did,
|
|
85
|
-
|
|
111
|
+
delta_file_name=delta_file_name,
|
|
112
|
+
data_source=data_source,
|
|
113
|
+
flow_name=flow_name,
|
|
114
|
+
flow_id=flow_id,
|
|
86
115
|
action_name=action_name,
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
egress_flow=egress_flow,
|
|
90
|
-
system=system,
|
|
116
|
+
action_id=action_id,
|
|
117
|
+
action_version=action_version,
|
|
91
118
|
hostname=hostname,
|
|
92
|
-
|
|
119
|
+
system_name=system_name,
|
|
93
120
|
collect=collect,
|
|
94
121
|
collected_dids=collected_dids,
|
|
95
122
|
memo=memo,
|
|
123
|
+
content_service=content_service,
|
|
96
124
|
logger=logger)
|
|
97
125
|
|
|
98
126
|
|
|
@@ -197,7 +225,6 @@ class Content:
|
|
|
197
225
|
|
|
198
226
|
return new_segments
|
|
199
227
|
|
|
200
|
-
|
|
201
228
|
def get_size(self):
|
|
202
229
|
"""
|
|
203
230
|
Returns the size of the content in bytes.
|
|
@@ -318,7 +345,8 @@ class Event(NamedTuple):
|
|
|
318
345
|
|
|
319
346
|
@classmethod
|
|
320
347
|
def create(cls, event: dict, hostname: str, content_service: ContentService, logger: Logger):
|
|
321
|
-
delta_file_messages = [DeltaFileMessage.from_dict(delta_file_message, content_service) for delta_file_message in
|
|
348
|
+
delta_file_messages = [DeltaFileMessage.from_dict(delta_file_message, content_service) for delta_file_message in
|
|
349
|
+
event['deltaFileMessages']]
|
|
322
350
|
context = Context.create(event['actionContext'], hostname, content_service, logger)
|
|
323
351
|
params = event['actionParams']
|
|
324
352
|
queue_name = None
|
|
@@ -46,9 +46,9 @@ def _coordinates():
|
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
def _setup_queue(max_connections):
|
|
49
|
-
|
|
50
|
-
password = os.getenv('
|
|
51
|
-
return ActionEventQueue(
|
|
49
|
+
url = os.getenv('VALKEY_URL', 'http://deltafi-valkey-master:6379')
|
|
50
|
+
password = os.getenv('VALKEY_PASSWORD')
|
|
51
|
+
return ActionEventQueue(url, max_connections, password)
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
def _setup_content_service():
|
|
@@ -286,7 +286,10 @@ class Plugin(object):
|
|
|
286
286
|
|
|
287
287
|
response = {
|
|
288
288
|
'did': event.context.did,
|
|
289
|
-
'
|
|
289
|
+
'flowName': event.context.flow_name,
|
|
290
|
+
'flowId': event.context.flow_id,
|
|
291
|
+
'actionName': event.context.action_name,
|
|
292
|
+
'actionId': event.context.action_id,
|
|
290
293
|
'start': start_time,
|
|
291
294
|
'stop': time.time(),
|
|
292
295
|
'type': result.result_type,
|
|
@@ -96,12 +96,12 @@ class FilterResult(Result):
|
|
|
96
96
|
|
|
97
97
|
|
|
98
98
|
class IngressResultItem:
|
|
99
|
-
def __init__(self, context: Context,
|
|
99
|
+
def __init__(self, context: Context, delta_file_name: str):
|
|
100
100
|
self.context = context
|
|
101
|
-
self.filename = filename
|
|
102
101
|
self._did = str(uuid.uuid4())
|
|
103
102
|
self.content = []
|
|
104
103
|
self.metadata = {}
|
|
104
|
+
self.delta_file_name = delta_file_name
|
|
105
105
|
|
|
106
106
|
@property
|
|
107
107
|
def did(self):
|
|
@@ -140,7 +140,7 @@ class IngressResultItem:
|
|
|
140
140
|
def response(self):
|
|
141
141
|
return {
|
|
142
142
|
'did': self._did,
|
|
143
|
-
'
|
|
143
|
+
'deltaFileName': self.delta_file_name,
|
|
144
144
|
'metadata': self.metadata,
|
|
145
145
|
'content': [content.json() for content in self.content]
|
|
146
146
|
}
|
|
@@ -156,8 +156,8 @@ class IngressResult(Result):
|
|
|
156
156
|
def __init__(self, context: Context):
|
|
157
157
|
super().__init__('ingress', 'INGRESS', context)
|
|
158
158
|
self.memo = None
|
|
159
|
-
self.execute_immediate = False
|
|
160
159
|
self.ingress_result_items = []
|
|
160
|
+
self.execute_immediate = False
|
|
161
161
|
self.status = IngressStatusEnum.HEALTHY
|
|
162
162
|
self.statusMessage = None
|
|
163
163
|
|
|
@@ -0,0 +1,293 @@
|
|
|
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
|
+
import json
|
|
20
|
+
import re
|
|
21
|
+
from abc import ABC
|
|
22
|
+
from abc import abstractmethod
|
|
23
|
+
from itertools import repeat
|
|
24
|
+
|
|
25
|
+
from deepdiff import DeepDiff, DeepSearch
|
|
26
|
+
|
|
27
|
+
from .assertions import *
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CompareHelper(ABC):
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def compare(self, expected: str, actual: str, label: str):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class GenericCompareHelper(CompareHelper):
|
|
37
|
+
def compare(self, expected: str, actual: str, label: str):
|
|
38
|
+
assert_equal_with_label(expected, actual, label)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class JsonCompareHelper(CompareHelper):
|
|
42
|
+
"""Provides helper functions for comparing JSON/dict objects.
|
|
43
|
+
|
|
44
|
+
Are these two JSON/dict objects equivalent?
|
|
45
|
+
- compare(...)
|
|
46
|
+
|
|
47
|
+
Are these two lists equivalent?
|
|
48
|
+
- compare_lists(...)
|
|
49
|
+
|
|
50
|
+
Is this list a subset/superset of that list?
|
|
51
|
+
- compare_lists_subset(...)
|
|
52
|
+
- compare_lists_superset(...)
|
|
53
|
+
|
|
54
|
+
Select a list of values from an existing list and put them into a new list to facilitate list comparisons:
|
|
55
|
+
- create_list_from_list_using_filter_regex(...)
|
|
56
|
+
|
|
57
|
+
Is this value found (or not) in this JSON/dict object?
|
|
58
|
+
- is_found(...)
|
|
59
|
+
- is_not_found(...)
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(self, regex_exclusion_list=None, ignore_order=True):
|
|
63
|
+
"""Creates and configures a JsonCompareHelper object. If the optional 'ignore_order' is true, then the order of
|
|
64
|
+
data is ignored when checking else order is enforced. The optional 'regex_exclusion_list' excludes
|
|
65
|
+
paths within the object from comparison; if empty or not provided, then no excludes are applied."""
|
|
66
|
+
if regex_exclusion_list is None:
|
|
67
|
+
regex_exclusion_list = []
|
|
68
|
+
self.excludes = regex_exclusion_list
|
|
69
|
+
self.ignore_order = ignore_order
|
|
70
|
+
|
|
71
|
+
def __perform_find(self, obj: object, item):
|
|
72
|
+
"""Returns a dict of matches of the 'item' in the object 'obj'. Both keys and values of dicts are included in
|
|
73
|
+
the search. The item may be compiled regex pattern or a string that compiles to a regex pattern. The
|
|
74
|
+
returned dict is empty if there are no matches. Excludes path(s) determined by the constructor."""
|
|
75
|
+
return DeepSearch(obj, item, verbose_level=2, exclude_regex_paths=self.excludes, use_regexp=True)
|
|
76
|
+
|
|
77
|
+
def is_not_found(self, obj: object, item):
|
|
78
|
+
"""Returns None if there are no occurrences of 'item' in object 'obj' else raises a ValueError. Both keys and
|
|
79
|
+
values of dicts are included in the search. If 'item' is a list, then all elements of item must not be
|
|
80
|
+
found in list, else a ValueError is raised. The argument 'item' may be a compiled regex pattern, a
|
|
81
|
+
string that compiles to a regex pattern, or a list of either or both. Excludes path(s) and failure on
|
|
82
|
+
ordering of elements are determined by the constructor."""
|
|
83
|
+
|
|
84
|
+
all_matches = []
|
|
85
|
+
|
|
86
|
+
if isinstance(item, list):
|
|
87
|
+
for value in item:
|
|
88
|
+
matches = self.__perform_find(obj, value)
|
|
89
|
+
if len(matches) > 0:
|
|
90
|
+
all_matches.append(matches)
|
|
91
|
+
else:
|
|
92
|
+
matches = self.__perform_find(obj, item)
|
|
93
|
+
if len(matches) > 0:
|
|
94
|
+
all_matches.append(matches)
|
|
95
|
+
|
|
96
|
+
if len(all_matches) > 0:
|
|
97
|
+
raise ValueError("Matches found for items '" + f"{all_matches}" + "'")
|
|
98
|
+
|
|
99
|
+
assert len(all_matches) == 0
|
|
100
|
+
|
|
101
|
+
def is_found(self, obj: object, item):
|
|
102
|
+
"""Returns None if 'item' occurs in object 'obj' else raises a ValueError. Both keys and values of dicts are
|
|
103
|
+
included in the search. If 'item' is a list, then all elements of item must occur in the object else a
|
|
104
|
+
ValueError is returned. The argument 'item' may be a compiled regex pattern, a string that compiles to
|
|
105
|
+
a regex pattern, or a list of either or both. Excludes path(s) and failure on ordering of elements are
|
|
106
|
+
determined by the constructor."""
|
|
107
|
+
|
|
108
|
+
not_found_items = []
|
|
109
|
+
|
|
110
|
+
if isinstance(item, list):
|
|
111
|
+
for value in item:
|
|
112
|
+
matches = self.__perform_find(obj, value)
|
|
113
|
+
if len(matches) == 0:
|
|
114
|
+
not_found_items.append(value)
|
|
115
|
+
else:
|
|
116
|
+
matches = self.__perform_find(obj, item)
|
|
117
|
+
if len(matches) == 0:
|
|
118
|
+
not_found_items.append(item)
|
|
119
|
+
|
|
120
|
+
if len(not_found_items) > 0:
|
|
121
|
+
raise ValueError("No matches found for items '" + f"{not_found_items}" + "'")
|
|
122
|
+
|
|
123
|
+
assert len(not_found_items) == 0
|
|
124
|
+
|
|
125
|
+
def __perform_diff(self, expected, actual):
|
|
126
|
+
"""Returns a dict with differences between 'expected' and 'actual'. The returned dict is empty if 'expected'
|
|
127
|
+
and 'actual' are equivalent. Both 'expected' and 'actual' must be dicts. Excludes path and failure on
|
|
128
|
+
ordering of elements are determined by the constructor. Elements must match number of repetitions."""
|
|
129
|
+
return DeepDiff(expected, actual, ignore_order=self.ignore_order, report_repetition=True,
|
|
130
|
+
exclude_regex_paths=self.excludes)
|
|
131
|
+
|
|
132
|
+
def __perform_diff_with_eval(self, expected, actual):
|
|
133
|
+
"""Returns None if 'expected' and 'actual' are equivalent else returns a ValueError. Both 'expected' and
|
|
134
|
+
'actual' must be dicts. Excludes path and failure on ordering of elements are determined by the
|
|
135
|
+
constructor. Elements must match number of repetitions."""
|
|
136
|
+
|
|
137
|
+
diffs = self.__perform_diff(expected, actual)
|
|
138
|
+
|
|
139
|
+
if len(diffs) > 0:
|
|
140
|
+
raise ValueError(f"{diffs}")
|
|
141
|
+
|
|
142
|
+
assert len(diffs) == 0
|
|
143
|
+
|
|
144
|
+
def compare(self, expected, actual, label: str):
|
|
145
|
+
"""Returns None if 'expected' and 'actual' are equivalent else returns a ValueError. Both 'expected' and
|
|
146
|
+
'actual' must be either dicts or strings that parse as JSON to dicts. Excludes path and failure on
|
|
147
|
+
ordering of elements are determined by the constructor. Elements must match number of repetitions."""
|
|
148
|
+
|
|
149
|
+
if isinstance(expected, str):
|
|
150
|
+
exp = json.loads(expected)
|
|
151
|
+
else:
|
|
152
|
+
exp = expected
|
|
153
|
+
|
|
154
|
+
if isinstance(actual, str):
|
|
155
|
+
act = json.loads(actual)
|
|
156
|
+
else:
|
|
157
|
+
act = actual
|
|
158
|
+
|
|
159
|
+
return self.__perform_diff_with_eval(exp, act)
|
|
160
|
+
|
|
161
|
+
def compare_lists(self, expected, actual: list, label: str):
|
|
162
|
+
"""Returns None if 'actual' is equivalent to 'expected' else returns a ValueError.
|
|
163
|
+
|
|
164
|
+
The 'actual' argument must be a list. The 'expected' argument may be a list or a dict. If a list, then
|
|
165
|
+
'expected' and 'actual' are compared against each other. If a dict, then 'actual' is equivalent if it
|
|
166
|
+
contains elements with the same repetitions as defined in the 'expected' dict with the key equal to the
|
|
167
|
+
element in 'actual' and the value equal to the number of repetitions.
|
|
168
|
+
|
|
169
|
+
Order of elements is ignored. Elements must match number of repetitions."""
|
|
170
|
+
|
|
171
|
+
expected_list = []
|
|
172
|
+
|
|
173
|
+
if isinstance(expected, dict):
|
|
174
|
+
for key, value in expected.items():
|
|
175
|
+
expected_list += list(repeat(key, value))
|
|
176
|
+
else:
|
|
177
|
+
expected_list = expected
|
|
178
|
+
|
|
179
|
+
return self.__perform_diff_with_eval({"json-compare-helper-internal-list": expected_list},
|
|
180
|
+
{"json-compare-helper-internal-list": actual})
|
|
181
|
+
|
|
182
|
+
@staticmethod
|
|
183
|
+
def compare_lists_subset(expected_subset, actual: list):
|
|
184
|
+
"""Returns None if the 'actual' list contains at least 'expected_subset' else returns a ValueError. The
|
|
185
|
+
'actual' list may contain 0 or more additional elements than defined in 'expected_subset'.
|
|
186
|
+
|
|
187
|
+
The 'actual' argument must be a list. The argument 'expected_subset' may be a list or a dict. In the
|
|
188
|
+
latter case, the key defines the item that must appear in 'actual' and the value defines the number of
|
|
189
|
+
repetitions.
|
|
190
|
+
|
|
191
|
+
Order of elements is ignored. Elements must match number of repetitions."""
|
|
192
|
+
|
|
193
|
+
expected_subset_map = {}
|
|
194
|
+
|
|
195
|
+
if isinstance(expected_subset, list):
|
|
196
|
+
for item in expected_subset:
|
|
197
|
+
value = expected_subset_map.get(item)
|
|
198
|
+
if value is None:
|
|
199
|
+
expected_subset_map[item] = 1
|
|
200
|
+
else:
|
|
201
|
+
expected_subset_map[item] = value + 1
|
|
202
|
+
else:
|
|
203
|
+
expected_subset_map = expected_subset
|
|
204
|
+
|
|
205
|
+
actual_map = {}
|
|
206
|
+
|
|
207
|
+
for item in actual:
|
|
208
|
+
value = actual_map.get(item)
|
|
209
|
+
if value is None:
|
|
210
|
+
actual_map[item] = 1
|
|
211
|
+
else:
|
|
212
|
+
actual_map[item] = value + 1
|
|
213
|
+
|
|
214
|
+
for key, value in expected_subset_map.items():
|
|
215
|
+
actual_value = actual_map.get(key)
|
|
216
|
+
if actual_value is None:
|
|
217
|
+
raise ValueError("Actual list did not contain element '" + str(key) + "'")
|
|
218
|
+
else:
|
|
219
|
+
if actual_value != value:
|
|
220
|
+
raise ValueError("Actual list had item '" + str(key) + "' with repetition " + str(actual_value)
|
|
221
|
+
+ " but required repetition " + str(value))
|
|
222
|
+
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
@staticmethod
|
|
226
|
+
def compare_lists_superset(expected_superset, actual: list):
|
|
227
|
+
"""Returns None if the 'actual' list contains only elements that appear in 'expected_superset' else returns a
|
|
228
|
+
ValueError. The 'actual' list cannot contain more elements than are defined in 'expected_superset'; the
|
|
229
|
+
'expected_superset' may define 0 or more values not contained in 'actual'.
|
|
230
|
+
|
|
231
|
+
The 'actual' argument must be a list. The argument 'expected_superset' may be a list or a dict. In the
|
|
232
|
+
latter case, the key defines the item that must appear in 'actual' and the value defines the number of
|
|
233
|
+
repetitions.
|
|
234
|
+
|
|
235
|
+
Order of elements is ignored. Elements must match number of repetitions."""
|
|
236
|
+
|
|
237
|
+
expected_superset_map = {}
|
|
238
|
+
|
|
239
|
+
if isinstance(expected_superset, list):
|
|
240
|
+
for item in expected_superset:
|
|
241
|
+
value = expected_superset_map.get(item)
|
|
242
|
+
if value is None:
|
|
243
|
+
expected_superset_map[item] = 1
|
|
244
|
+
else:
|
|
245
|
+
expected_superset_map[item] = value + 1
|
|
246
|
+
else:
|
|
247
|
+
expected_superset_map = expected_superset
|
|
248
|
+
|
|
249
|
+
actual_map = {}
|
|
250
|
+
|
|
251
|
+
for item in actual:
|
|
252
|
+
value = actual_map.get(item)
|
|
253
|
+
if value is None:
|
|
254
|
+
actual_map[item] = 1
|
|
255
|
+
else:
|
|
256
|
+
actual_map[item] = value + 1
|
|
257
|
+
|
|
258
|
+
for key, value in actual_map.items():
|
|
259
|
+
expected_value = expected_superset_map.get(key)
|
|
260
|
+
if expected_value is None:
|
|
261
|
+
raise ValueError("Actual list contained element '" + str(key)
|
|
262
|
+
+ "' that did not appear in the expected superset")
|
|
263
|
+
else:
|
|
264
|
+
if expected_value != value:
|
|
265
|
+
raise ValueError("Actual list had item '" + str(key) + "' with repetition " + str(value)
|
|
266
|
+
+ " but required repetition " + str(expected_value))
|
|
267
|
+
|
|
268
|
+
return None
|
|
269
|
+
|
|
270
|
+
@staticmethod
|
|
271
|
+
def create_list_from_list_using_filter_regex(regex, source_list: list):
|
|
272
|
+
"""Creates and returns a list of those items in the 'source_list' matching the regex in the 'regex'. The
|
|
273
|
+
argument 'regex' may be a string or a compiled regex object.
|
|
274
|
+
|
|
275
|
+
For the 'regex', consider using anchors to explicitly match from the start (or end) of a string. For
|
|
276
|
+
example, a string such as "^malware--" will explicitly match from the start of a string."""
|
|
277
|
+
|
|
278
|
+
def search_funct(element):
|
|
279
|
+
|
|
280
|
+
if isinstance(regex, str):
|
|
281
|
+
# 'regex' is a string
|
|
282
|
+
match = re.search(regex, element)
|
|
283
|
+
else:
|
|
284
|
+
# assume 'regex' is a compiled regex pattern object
|
|
285
|
+
match = regex.match(element)
|
|
286
|
+
|
|
287
|
+
if match is None:
|
|
288
|
+
return False
|
|
289
|
+
else:
|
|
290
|
+
return True
|
|
291
|
+
|
|
292
|
+
return list(filter(search_funct, source_list))
|
|
293
|
+
|
|
@@ -188,21 +188,10 @@ class ActionTest(ABC):
|
|
|
188
188
|
|
|
189
189
|
def __reset__(self, did: str):
|
|
190
190
|
self.content_service = InternalContentService()
|
|
191
|
-
<<<<<<< HEAD
|
|
192
191
|
if did is None:
|
|
193
192
|
self.did = str(uuid.uuid4())
|
|
194
193
|
else:
|
|
195
194
|
self.did = did
|
|
196
|
-
self.expected_outputs = []
|
|
197
|
-
||||||| parent of 831733c7 (2.0 Refactor)
|
|
198
|
-
self.did = str(uuid.uuid4())
|
|
199
|
-
self.expected_outputs = []
|
|
200
|
-
=======
|
|
201
|
-
if did is None:
|
|
202
|
-
self.did = str(uuid.uuid4())
|
|
203
|
-
else:
|
|
204
|
-
self.did = did
|
|
205
|
-
>>>>>>> 831733c7 (2.0 Refactor)
|
|
206
195
|
self.loaded_inputs = []
|
|
207
196
|
self.res_path = ""
|
|
208
197
|
|
|
@@ -237,36 +226,23 @@ class ActionTest(ABC):
|
|
|
237
226
|
content_list = self.make_content_list(test_case)
|
|
238
227
|
self.content_service.load(self.loaded_inputs)
|
|
239
228
|
|
|
240
|
-
<<<<<<< HEAD
|
|
241
|
-
return DeltaFileMessage(
|
|
242
|
-
metadata=test_case.in_meta,
|
|
243
|
-
content_list=content_list,
|
|
244
|
-
domains=test_case.in_domains,
|
|
245
|
-
enrichments=test_case.in_enrichments)
|
|
246
|
-
||||||| parent of 831733c7 (2.0 Refactor)
|
|
247
|
-
return DeltaFileMessage(metadata=test_case.in_meta,
|
|
248
|
-
content_list=content_list,
|
|
249
|
-
domains=test_case.in_domains,
|
|
250
|
-
enrichments=test_case.in_enrichments)
|
|
251
|
-
=======
|
|
252
229
|
return DeltaFileMessage(metadata=test_case.in_meta,
|
|
253
230
|
content_list=content_list)
|
|
254
|
-
>>>>>>> 831733c7 (2.0 Refactor)
|
|
255
231
|
|
|
256
232
|
def make_context(self, test_case: TestCaseBase):
|
|
257
233
|
action_name = INGRESS_FLOW + "." + test_case.action.__class__.__name__
|
|
258
234
|
return Context(
|
|
259
235
|
did=self.did,
|
|
260
|
-
|
|
236
|
+
delta_file_name=test_case.file_name,
|
|
237
|
+
data_source="DATASRC",
|
|
238
|
+
flow_name=INGRESS_FLOW,
|
|
239
|
+
flow_id="FLOWID",
|
|
261
240
|
action_name=action_name,
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
egress_flow=EGRESS_FLOW,
|
|
265
|
-
system=SYSTEM,
|
|
241
|
+
action_id="ACTIONID",
|
|
242
|
+
action_version="1.0",
|
|
266
243
|
hostname=HOSTNAME,
|
|
244
|
+
system_name=SYSTEM,
|
|
267
245
|
content_service=self.content_service,
|
|
268
|
-
collect=None,
|
|
269
|
-
collected_dids=None,
|
|
270
246
|
logger=get_logger())
|
|
271
247
|
|
|
272
248
|
def make_event(self, test_case: TestCaseBase):
|
|
@@ -312,39 +288,27 @@ class ActionTest(ABC):
|
|
|
312
288
|
else:
|
|
313
289
|
raise ValueError(f"unknown type: {test_case.expected_result_type}")
|
|
314
290
|
|
|
315
|
-
|
|
291
|
+
@staticmethod
|
|
292
|
+
def compare_content_details(expected: LoadedContent, actual: Content):
|
|
316
293
|
assert_equal(expected.content_type, actual.media_type)
|
|
317
294
|
assert_equal(expected.name, actual.name)
|
|
318
295
|
|
|
319
|
-
def compare_one_content(self,
|
|
296
|
+
def compare_one_content(self, comparator: CompareHelper, expected: LoadedContent, actual, index):
|
|
320
297
|
self.compare_content_details(expected, actual)
|
|
321
298
|
seg_id = actual.segments[0].uuid
|
|
322
|
-
|
|
299
|
+
comparator.compare(expected.data, self.content_service.get_output(seg_id), f"Content[{index}]")
|
|
323
300
|
|
|
324
|
-
def compare_content_list(self,
|
|
301
|
+
def compare_content_list(self, comparator: CompareHelper, expected_outputs: List[IOContent], content: List):
|
|
325
302
|
assert_equal_len(expected_outputs, content)
|
|
326
303
|
for index, expected_ioc in enumerate(expected_outputs):
|
|
327
304
|
if len(expected_ioc.content_bytes) == 0:
|
|
328
|
-
expected = LoadedContent(self.did, expected_ioc, self.load_file(
|
|
305
|
+
expected = LoadedContent(self.did, expected_ioc, self.load_file(expected_ioc))
|
|
329
306
|
else:
|
|
330
307
|
expected = LoadedContent(self.did, expected_ioc, None)
|
|
331
|
-
self.compare_one_content(
|
|
332
|
-
|
|
333
|
-
def compare_one_metric(self, expected: Metric, result: Metric):
|
|
334
|
-
assert expected.name == result.name
|
|
335
|
-
assert_equal_with_label(expected.value, result.value, expected.name)
|
|
336
|
-
assert_keys_and_values(expected.tags, result.tags)
|
|
337
|
-
|
|
338
|
-
expected_value = expected['value']
|
|
339
|
-
if type(expected_value) == str:
|
|
340
|
-
comparitor.compare(expected_value, actual['value'], f"Domain[{index}]")
|
|
341
|
-
elif type(expected_value) == IOContent:
|
|
342
|
-
expected_data = self.load_file(expected_value)
|
|
343
|
-
comparitor.compare(expected_data, actual['value'], f"Domain[{index}]")
|
|
344
|
-
else:
|
|
345
|
-
raise ValueError(f"unknown expected_value type: {type(expected_value)}")
|
|
308
|
+
self.compare_one_content(comparator, expected, content[index], index)
|
|
346
309
|
|
|
347
|
-
|
|
310
|
+
@staticmethod
|
|
311
|
+
def compare_one_metric(expected: Metric, result: Metric):
|
|
348
312
|
assert expected.name == result.name
|
|
349
313
|
assert_equal_with_label(expected.value, result.value, expected.name)
|
|
350
314
|
assert_keys_and_values(expected.tags, result.tags)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "deltafi"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.0rc1715691282958"
|
|
4
4
|
description = "SDK for DeltaFi plugins and actions"
|
|
5
5
|
authors = ["DeltaFi <deltafi@systolic.com>"]
|
|
6
6
|
license = "Apache License, Version 2.0"
|
|
@@ -23,19 +23,19 @@ classifiers = [
|
|
|
23
23
|
python = "^3.9"
|
|
24
24
|
deepdiff = ">=6.7.1"
|
|
25
25
|
json-logging = ">=1.3.0"
|
|
26
|
-
minio = ">=7.2.
|
|
27
|
-
pydantic = ">=2.
|
|
28
|
-
redis = ">=5.0.
|
|
26
|
+
minio = ">=7.2.5"
|
|
27
|
+
pydantic = ">=2.7.1"
|
|
28
|
+
redis = ">=5.0.4"
|
|
29
29
|
requests = ">=2.31.0"
|
|
30
|
-
urllib3 = ">=2.1
|
|
30
|
+
urllib3 = ">=2.2.1"
|
|
31
31
|
|
|
32
32
|
[tool.poetry.group.test]
|
|
33
33
|
optional = true
|
|
34
34
|
|
|
35
35
|
[tool.poetry.group.test.dependencies]
|
|
36
|
-
pytest = ">=
|
|
37
|
-
pytest-mock = ">=3.
|
|
38
|
-
mockito = ">=1.
|
|
36
|
+
pytest = ">=8.1.1"
|
|
37
|
+
pytest-mock = ">=3.14.0"
|
|
38
|
+
mockito = ">=1.5.0"
|
|
39
39
|
|
|
40
40
|
[tool.poetry.urls]
|
|
41
41
|
'Source Code' = "https://gitlab.com/deltafi/deltafi"
|
|
@@ -1,50 +0,0 @@
|
|
|
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
|
-
import json
|
|
20
|
-
from abc import ABC
|
|
21
|
-
from abc import abstractmethod
|
|
22
|
-
from typing import List
|
|
23
|
-
|
|
24
|
-
from deepdiff import DeepDiff
|
|
25
|
-
|
|
26
|
-
from .assertions import *
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class CompareHelper(ABC):
|
|
30
|
-
@abstractmethod
|
|
31
|
-
def compare(self, expected: str, actual: str, label: str):
|
|
32
|
-
pass
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class GenericCompareHelper(CompareHelper):
|
|
36
|
-
def compare(self, expected: str, actual: str, label: str):
|
|
37
|
-
assert_equal_with_label(expected, actual, label)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
class JsonCompareHelper(CompareHelper):
|
|
41
|
-
def __init__(self, regex_exclusion_list: List):
|
|
42
|
-
self.excludes = regex_exclusion_list
|
|
43
|
-
|
|
44
|
-
def compare(self, expected: str, actual: str, label: str):
|
|
45
|
-
exp = json.loads(expected)
|
|
46
|
-
act = json.loads(actual)
|
|
47
|
-
diffs = DeepDiff(exp, act, exclude_regex_paths=self.excludes)
|
|
48
|
-
if len(diffs) > 0:
|
|
49
|
-
raise ValueError(f"{diffs}")
|
|
50
|
-
assert len(diffs) == 0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|