deltafi 2.14.0__tar.gz → 2.15.1__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.
Files changed (25) hide show
  1. {deltafi-2.14.0 → deltafi-2.15.1}/PKG-INFO +1 -1
  2. deltafi-2.15.1/deltafi/action.py +321 -0
  3. {deltafi-2.14.0 → deltafi-2.15.1}/deltafi/plugin.py +4 -16
  4. {deltafi-2.14.0 → deltafi-2.15.1}/pyproject.toml +1 -1
  5. deltafi-2.14.0/deltafi/action.py +0 -147
  6. {deltafi-2.14.0 → deltafi-2.15.1}/README.md +0 -0
  7. {deltafi-2.14.0 → deltafi-2.15.1}/deltafi/__init__.py +0 -0
  8. {deltafi-2.14.0 → deltafi-2.15.1}/deltafi/actioneventqueue.py +0 -0
  9. {deltafi-2.14.0 → deltafi-2.15.1}/deltafi/actiontype.py +0 -0
  10. {deltafi-2.14.0 → deltafi-2.15.1}/deltafi/domain.py +0 -0
  11. {deltafi-2.14.0 → deltafi-2.15.1}/deltafi/exception.py +0 -0
  12. {deltafi-2.14.0 → deltafi-2.15.1}/deltafi/genericmodel.py +0 -0
  13. {deltafi-2.14.0 → deltafi-2.15.1}/deltafi/input.py +0 -0
  14. {deltafi-2.14.0 → deltafi-2.15.1}/deltafi/logger.py +0 -0
  15. {deltafi-2.14.0 → deltafi-2.15.1}/deltafi/metric.py +0 -0
  16. {deltafi-2.14.0 → deltafi-2.15.1}/deltafi/result.py +0 -0
  17. {deltafi-2.14.0 → deltafi-2.15.1}/deltafi/storage.py +0 -0
  18. {deltafi-2.14.0 → deltafi-2.15.1}/deltafi/test_kit/__init__.py +0 -0
  19. {deltafi-2.14.0 → deltafi-2.15.1}/deltafi/test_kit/assertions.py +0 -0
  20. {deltafi-2.14.0 → deltafi-2.15.1}/deltafi/test_kit/compare_helpers.py +0 -0
  21. {deltafi-2.14.0 → deltafi-2.15.1}/deltafi/test_kit/constants.py +0 -0
  22. {deltafi-2.14.0 → deltafi-2.15.1}/deltafi/test_kit/egress.py +0 -0
  23. {deltafi-2.14.0 → deltafi-2.15.1}/deltafi/test_kit/framework.py +0 -0
  24. {deltafi-2.14.0 → deltafi-2.15.1}/deltafi/test_kit/timed_ingress.py +0 -0
  25. {deltafi-2.14.0 → deltafi-2.15.1}/deltafi/test_kit/transform.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: deltafi
3
- Version: 2.14.0
3
+ Version: 2.15.1
4
4
  Summary: SDK for DeltaFi plugins and actions
5
5
  License: Apache License, Version 2.0
6
6
  Keywords: deltafi
@@ -0,0 +1,321 @@
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 abc import ABC, abstractmethod
20
+ from typing import Any, List
21
+
22
+ from pydantic import BaseModel
23
+
24
+ from deltafi.actiontype import ActionType
25
+ from deltafi.domain import DeltaFileMessage
26
+ from deltafi.genericmodel import GenericModel
27
+ from deltafi.input import EgressInput, TransformInput
28
+ from deltafi.result import *
29
+
30
+
31
+ class Join(ABC):
32
+ def join(self, transform_inputs: List[TransformInput]):
33
+ all_content = []
34
+ all_metadata = {}
35
+ for transform_input in transform_inputs:
36
+ all_content += transform_input.content
37
+ all_metadata.update(transform_input.metadata)
38
+ return TransformInput(content=all_content, metadata=all_metadata)
39
+
40
+
41
+ class ContentSpec:
42
+ name: str
43
+ media_type: str
44
+ description: str
45
+
46
+ def __init__(self, name: str = None, media_type: str = None, description: str = None):
47
+ self.name = name
48
+ self.media_type = media_type
49
+ self.description = description
50
+
51
+ def json(self):
52
+ json_dictionary = {}
53
+ if self.name is not None:
54
+ json_dictionary['name'] = self.name
55
+ if self.media_type is not None:
56
+ json_dictionary['mediaType'] = self.media_type
57
+ if self.description is not None:
58
+ json_dictionary['description'] = self.description
59
+ return json_dictionary
60
+
61
+
62
+ class KeyedDescription:
63
+ key: str
64
+ description: str
65
+
66
+ def __init__(self, key: str, description: str):
67
+ self.key = key
68
+ self.description = description
69
+
70
+ def json(self):
71
+ json_dictionary = {}
72
+ if self.key is not None:
73
+ json_dictionary['key'] = self.key
74
+ json_dictionary['description'] = self.description
75
+ return json_dictionary
76
+
77
+
78
+ class InputSpec:
79
+ content_summary: str
80
+ content_specs: List[ContentSpec]
81
+ metadata_summary: str
82
+ metadata_descriptions: List[KeyedDescription]
83
+
84
+ def __init__(self, content_summary: str = None, content_specs: List[ContentSpec] = None,
85
+ metadata_summary: str = None, metadata_descriptions: List[KeyedDescription] = None):
86
+ self.content_summary = content_summary
87
+ self.content_specs = content_specs
88
+ self.metadata_summary = metadata_summary
89
+ self.metadata_descriptions = metadata_descriptions
90
+
91
+ def json(self):
92
+ json_dictionary = {}
93
+ if self.content_summary is not None:
94
+ json_dictionary['contentSummary'] = self.content_summary
95
+ if self.content_specs is not None:
96
+ json_dictionary['contentSpecs'] = [cs.json() for cs in self.content_specs]
97
+ if self.metadata_summary is not None:
98
+ json_dictionary['metadataSummary'] = self.metadata_summary
99
+ if self.metadata_descriptions is not None:
100
+ json_dictionary['metadataDescriptions'] = [md.json() for md in self.metadata_descriptions]
101
+ return json_dictionary
102
+
103
+
104
+ class OutputSpec:
105
+ content_summary: str
106
+ content_specs: List[ContentSpec]
107
+ metadata_summary: str
108
+ metadata_descriptions: List[KeyedDescription]
109
+ passthrough: bool
110
+ annotations_summary: str
111
+ annotation_descriptions: List[KeyedDescription]
112
+
113
+ def __init__(self, content_summary: str = None, content_specs: List[ContentSpec] = None,
114
+ metadata_summary: str = None, metadata_descriptions: List[KeyedDescription] = None,
115
+ passthrough: bool = False, annotations_summary: str = None,
116
+ annotation_descriptions: List[KeyedDescription] = None):
117
+ self.content_summary = content_summary
118
+ self.content_specs = content_specs
119
+ self.metadata_summary = metadata_summary
120
+ self.metadata_descriptions = metadata_descriptions
121
+ self.passthrough = passthrough
122
+ self.annotations_summary = annotations_summary
123
+ self.annotation_descriptions = annotation_descriptions
124
+
125
+ def json(self):
126
+ json_dictionary = {}
127
+ if self.content_summary is not None:
128
+ json_dictionary['contentSummary'] = self.content_summary
129
+ if self.content_specs is not None:
130
+ json_dictionary['contentSpecs'] = [cs.json() for cs in self.content_specs]
131
+ if self.metadata_summary is not None:
132
+ json_dictionary['metadataSummary'] = self.metadata_summary
133
+ if self.metadata_descriptions is not None:
134
+ json_dictionary['metadataDescriptions'] = [md.json() for md in self.metadata_descriptions]
135
+ if self.passthrough is not None:
136
+ json_dictionary['passthrough'] = self.passthrough
137
+ if self.annotations_summary is not None:
138
+ json_dictionary['annotationsSummary'] = self.annotations_summary
139
+ if self.annotation_descriptions is not None:
140
+ json_dictionary['annotationDescriptions'] = [ad.json() for ad in self.annotation_descriptions]
141
+ return json_dictionary
142
+
143
+
144
+ class DescriptionWithConditions:
145
+ description: str
146
+ conditions: List[str]
147
+
148
+ def __init__(self, description: str = None, conditions: List[str] = None):
149
+ self.description = description
150
+ self.conditions = conditions
151
+
152
+ def json(self):
153
+ json_dictionary = {}
154
+ if self.description is not None:
155
+ json_dictionary['description'] = self.description
156
+ if self.conditions is not None:
157
+ json_dictionary['conditions'] = [c for c in self.conditions]
158
+ return json_dictionary
159
+
160
+
161
+ class ActionOptions:
162
+ description: str
163
+ input_spec: InputSpec
164
+ output_spec: OutputSpec
165
+ filters: List[DescriptionWithConditions] = None
166
+ errors: List[DescriptionWithConditions] = None
167
+ notes: List[str]
168
+ details: str
169
+
170
+ def __init__(self, description: str = None, input_spec: InputSpec = None, output_spec: OutputSpec = None,
171
+ filters: List = None, errors: List = None, notes: List[str] = None, details: str = None):
172
+ self.description = description
173
+ self.input_spec = input_spec
174
+ self.output_spec = output_spec
175
+ if filters is not None:
176
+ self.filters = []
177
+ for f in filters:
178
+ if isinstance(f, DescriptionWithConditions):
179
+ self.filters.append(f)
180
+ else:
181
+ self.filters.append(DescriptionWithConditions(description=f))
182
+ if errors is not None:
183
+ self.errors = []
184
+ for e in errors:
185
+ if isinstance(e, DescriptionWithConditions):
186
+ self.errors.append(e)
187
+ else:
188
+ self.errors.append(DescriptionWithConditions(description=e))
189
+ self.notes = notes
190
+ self.details = details
191
+
192
+ def json(self):
193
+ json_dictionary = {}
194
+ if self.description is not None:
195
+ json_dictionary['description'] = self.description
196
+ if self.input_spec is not None:
197
+ json_dictionary['inputSpec'] = self.input_spec.json()
198
+ if self.output_spec is not None:
199
+ json_dictionary['outputSpec'] = self.output_spec.json()
200
+ if self.filters is not None:
201
+ json_dictionary['filters'] = [f.json() for f in self.filters]
202
+ if self.errors is not None:
203
+ json_dictionary['errors'] = [e.json() for e in self.errors]
204
+ if self.notes is not None:
205
+ json_dictionary['notes'] = [n for n in self.notes]
206
+ if self.details is not None:
207
+ json_dictionary['details'] = self.details
208
+ return json_dictionary
209
+
210
+
211
+ class Action(ABC):
212
+ def __init__(self, action_type: ActionType, description: str, valid_result_types: tuple,
213
+ action_options: ActionOptions = None):
214
+ self.action_type = action_type
215
+ if action_options is None:
216
+ self.action_options = ActionOptions(description=description)
217
+ else:
218
+ self.action_options = action_options
219
+ self.valid_result_types = valid_result_types
220
+
221
+ @abstractmethod
222
+ def build_input(self, context: Context, delta_file_message: DeltaFileMessage):
223
+ pass
224
+
225
+ def execute_join_action(self, event):
226
+ raise RuntimeError(f"Join is not supported for {self.__class__.__name__}")
227
+
228
+ @abstractmethod
229
+ def execute(self, context: Context, action_input: Any, params: BaseModel):
230
+ pass
231
+
232
+ def execute_action(self, event):
233
+ if event.delta_file_messages is None or not len(event.delta_file_messages):
234
+ raise RuntimeError(f"Received event with no delta file messages for did {event.context.did}")
235
+ if event.context.join is not None:
236
+ result = self.execute_join_action(event)
237
+ else:
238
+ result = self.execute(
239
+ event.context,
240
+ self.build_input(event.context, event.delta_file_messages[0]),
241
+ self.param_class().model_validate(event.params))
242
+
243
+ self.validate_type(result)
244
+ return result
245
+
246
+ @staticmethod
247
+ def param_class():
248
+ """Factory method to create and return an empty GenericModel instance.
249
+
250
+ All action parameter classes must inherit pydantic.BaseModel.
251
+ Use of complex types in custom action parameter classes must specify
252
+ the internal types when defined. E.g., dict[str, str], or List[str]
253
+
254
+ Returns
255
+ -------
256
+ GenericModel
257
+ an empty GenericModel instance
258
+ """
259
+ return GenericModel
260
+
261
+ def validate_type(self, result):
262
+ if not isinstance(result, self.valid_result_types):
263
+ raise ValueError(f"{self.__class__.__name__} must return one of "
264
+ f"{[result_type.__name__ for result_type in self.valid_result_types]} "
265
+ f"but a {result.__class__.__name__} was returned")
266
+
267
+
268
+ class EgressAction(Action, ABC):
269
+ def __init__(self, description: str, action_options: ActionOptions = None):
270
+ super().__init__(ActionType.EGRESS, description, (EgressResult, ErrorResult, FilterResult), action_options)
271
+
272
+ def build_input(self, context: Context, delta_file_message: DeltaFileMessage):
273
+ return EgressInput(content=delta_file_message.content_list[0], metadata=delta_file_message.metadata)
274
+
275
+ @abstractmethod
276
+ def egress(self, context: Context, params: BaseModel, egress_input: EgressInput):
277
+ pass
278
+
279
+ def execute(self, context: Context, egress_input: EgressInput, params: BaseModel):
280
+ return self.egress(context, params, egress_input)
281
+
282
+
283
+ class TimedIngressAction(Action, ABC):
284
+ def __init__(self, description: str, action_options: ActionOptions = None):
285
+ super().__init__(ActionType.TIMED_INGRESS, description, (IngressResult, ErrorResult), action_options)
286
+
287
+ def build_input(self, context: Context, delta_file_message: DeltaFileMessage):
288
+ return None
289
+
290
+ @abstractmethod
291
+ def ingress(self, context: Context, params: BaseModel):
292
+ pass
293
+
294
+ def execute(self, context: Context, input_placeholder: Any, params: BaseModel):
295
+ return self.ingress(context, params)
296
+
297
+
298
+ class TransformAction(Action, ABC):
299
+ def __init__(self, description: str, action_options: ActionOptions = None):
300
+ super().__init__(ActionType.TRANSFORM, description,
301
+ (TransformResult, TransformResults, ErrorResult, FilterResult), action_options)
302
+
303
+ def build_input(self, context: Context, delta_file_message: DeltaFileMessage):
304
+ return TransformInput(content=delta_file_message.content_list, metadata=delta_file_message.metadata)
305
+
306
+ def execute_join_action(self, event):
307
+ if isinstance(self, Join):
308
+ return self.execute(
309
+ event.context,
310
+ self.join([self.build_input(event.context, delta_file_message)
311
+ for delta_file_message in event.delta_file_messages]),
312
+ self.param_class().model_validate(event.params))
313
+ else:
314
+ super().execute_join_action(event)
315
+
316
+ @abstractmethod
317
+ def transform(self, context: Context, params: BaseModel, transform_input: TransformInput):
318
+ pass
319
+
320
+ def execute(self, context: Context, transform_input: TransformInput, params: BaseModel):
321
+ return self.transform(context, params, transform_input)
@@ -29,7 +29,7 @@ from datetime import datetime, timezone, timedelta
29
29
  from importlib import metadata
30
30
  from os.path import isdir, isfile, join
31
31
  from pathlib import Path
32
- from typing import List
32
+ from typing import List, NamedTuple
33
33
 
34
34
  import requests
35
35
  import yaml
@@ -250,25 +250,13 @@ class Plugin(object):
250
250
  def action_name(self, action):
251
251
  return f"{self.coordinates.group_id}.{action.__class__.__name__}"
252
252
 
253
- def _load_action_docs(self, action):
254
- docs_path = str(Path(os.path.dirname(os.path.abspath(sys.argv[0]))) / 'docs')
255
- if not isdir(docs_path):
256
- return None
257
-
258
- action_docs_file = join(docs_path, action.__class__.__name__ + '.md')
259
- if not isfile(action_docs_file):
260
- return None
261
-
262
- return open(action_docs_file).read()
263
-
264
253
  def _action_json(self, action):
265
254
  return {
266
255
  'name': self.action_name(action),
267
- 'description': action.description,
268
256
  'type': action.action_type.name,
269
257
  'supportsJoin': isinstance(action, Join),
270
258
  'schema': action.param_class().model_json_schema(),
271
- 'docsMarkdown': self._load_action_docs(action)
259
+ 'actionOptions': action.action_options.json()
272
260
  }
273
261
 
274
262
  @staticmethod
@@ -337,10 +325,10 @@ class Plugin(object):
337
325
  self.logger.info("Plugin starting")
338
326
 
339
327
  for action in self.singleton_actions:
340
- num_threads = 1;
328
+ num_threads = 1
341
329
  if self.action_name(action) in self.thread_config:
342
330
  maybe_num_threads = self.thread_config[self.action_name(action)]
343
- if type(maybe_num_threads) == int and maybe_num_threads > 0:
331
+ if maybe_num_threads is int and maybe_num_threads > 0:
344
332
  num_threads = maybe_num_threads
345
333
  else:
346
334
  self.logger.error(f"Ignoring non-int or invalid thread value {maybe_num_threads}")
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "deltafi"
3
- version = "2.14.0"
3
+ version = "2.15.1"
4
4
  description = "SDK for DeltaFi plugins and actions"
5
5
  authors = ["DeltaFi <deltafi@systolic.com>"]
6
6
  license = "Apache License, Version 2.0"
@@ -1,147 +0,0 @@
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 abc import ABC, abstractmethod
20
- from typing import Any, List
21
-
22
- from pydantic import BaseModel
23
-
24
- from deltafi.actiontype import ActionType
25
- from deltafi.domain import DeltaFileMessage
26
- from deltafi.genericmodel import GenericModel
27
- from deltafi.input import EgressInput, TransformInput
28
- from deltafi.result import *
29
-
30
-
31
- class Join(ABC):
32
- def join(self, transform_inputs: List[TransformInput]):
33
- all_content = []
34
- all_metadata = {}
35
- for transform_input in transform_inputs:
36
- all_content += transform_input.content
37
- all_metadata.update(transform_input.metadata)
38
- return TransformInput(content=all_content, metadata=all_metadata)
39
-
40
-
41
- class Action(ABC):
42
- def __init__(self, action_type: ActionType, description: str, valid_result_types: tuple):
43
- self.action_type = action_type
44
- self.description = description
45
- self.valid_result_types = valid_result_types
46
-
47
- @abstractmethod
48
- def build_input(self, context: Context, delta_file_message: DeltaFileMessage):
49
- pass
50
-
51
- def execute_join_action(self, event):
52
- raise RuntimeError(f"Join is not supported for {self.__class__.__name__}")
53
-
54
- @abstractmethod
55
- def execute(self, context: Context, action_input: Any, params: BaseModel):
56
- pass
57
-
58
- def execute_action(self, event):
59
- if event.delta_file_messages is None or not len(event.delta_file_messages):
60
- raise RuntimeError(f"Received event with no delta file messages for did {event.context.did}")
61
- if event.context.join is not None:
62
- result = self.execute_join_action(event)
63
- else:
64
- result = self.execute(
65
- event.context,
66
- self.build_input(event.context, event.delta_file_messages[0]),
67
- self.param_class().model_validate(event.params))
68
-
69
- self.validate_type(result)
70
- return result
71
-
72
- @staticmethod
73
- def param_class():
74
- """Factory method to create and return an empty GenericModel instance.
75
-
76
- All action parameter classes must inherit pydantic.BaseModel.
77
- Use of complex types in custom action parameter classes must specify
78
- the internal types when defined. E.g., dict[str, str], or List[str]
79
-
80
- Returns
81
- -------
82
- GenericModel
83
- an empty GenericModel instance
84
- """
85
- return GenericModel
86
-
87
- def validate_type(self, result):
88
- if not isinstance(result, self.valid_result_types):
89
- raise ValueError(f"{self.__class__.__name__} must return one of "
90
- f"{[result_type.__name__ for result_type in self.valid_result_types]} "
91
- f"but a {result.__class__.__name__} was returned")
92
-
93
-
94
- class EgressAction(Action, ABC):
95
- def __init__(self, description: str):
96
- super().__init__(ActionType.EGRESS, description, (EgressResult, ErrorResult, FilterResult))
97
-
98
- def build_input(self, context: Context, delta_file_message: DeltaFileMessage):
99
- return EgressInput(content=delta_file_message.content_list[0], metadata=delta_file_message.metadata)
100
-
101
- @abstractmethod
102
- def egress(self, context: Context, params: BaseModel, egress_input: EgressInput):
103
- pass
104
-
105
- def execute(self, context: Context, egress_input: EgressInput, params: BaseModel):
106
- return self.egress(context, params, egress_input)
107
-
108
-
109
- class TimedIngressAction(Action, ABC):
110
- def __init__(self, description: str):
111
- super().__init__(ActionType.TIMED_INGRESS, description, (IngressResult, ErrorResult))
112
-
113
- def build_input(self, context: Context, delta_file_message: DeltaFileMessage):
114
- return None
115
-
116
- @abstractmethod
117
- def ingress(self, context: Context, params: BaseModel):
118
- pass
119
-
120
- def execute(self, context: Context, input_placeholder: Any, params: BaseModel):
121
- return self.ingress(context, params)
122
-
123
-
124
- class TransformAction(Action, ABC):
125
- def __init__(self, description: str):
126
- super().__init__(ActionType.TRANSFORM, description,
127
- (TransformResult, TransformResults, ErrorResult, FilterResult))
128
-
129
- def build_input(self, context: Context, delta_file_message: DeltaFileMessage):
130
- return TransformInput(content=delta_file_message.content_list, metadata=delta_file_message.metadata)
131
-
132
- def execute_join_action(self, event):
133
- if isinstance(self, Join):
134
- return self.execute(
135
- event.context,
136
- self.join([self.build_input(event.context, delta_file_message)
137
- for delta_file_message in event.delta_file_messages]),
138
- self.param_class().model_validate(event.params))
139
- else:
140
- super().execute_join_action(event)
141
-
142
- @abstractmethod
143
- def transform(self, context: Context, params: BaseModel, transform_input: TransformInput):
144
- pass
145
-
146
- def execute(self, context: Context, transform_input: TransformInput, params: BaseModel):
147
- return self.transform(context, params, transform_input)
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