dtlpy 1.115.44__py3-none-any.whl → 1.116.6__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.
Files changed (238) hide show
  1. dtlpy/__init__.py +491 -491
  2. dtlpy/__version__.py +1 -1
  3. dtlpy/assets/__init__.py +26 -26
  4. dtlpy/assets/code_server/config.yaml +2 -2
  5. dtlpy/assets/code_server/installation.sh +24 -24
  6. dtlpy/assets/code_server/launch.json +13 -13
  7. dtlpy/assets/code_server/settings.json +2 -2
  8. dtlpy/assets/main.py +53 -53
  9. dtlpy/assets/main_partial.py +18 -18
  10. dtlpy/assets/mock.json +11 -11
  11. dtlpy/assets/model_adapter.py +83 -83
  12. dtlpy/assets/package.json +61 -61
  13. dtlpy/assets/package_catalog.json +29 -29
  14. dtlpy/assets/package_gitignore +307 -307
  15. dtlpy/assets/service_runners/__init__.py +33 -33
  16. dtlpy/assets/service_runners/converter.py +96 -96
  17. dtlpy/assets/service_runners/multi_method.py +49 -49
  18. dtlpy/assets/service_runners/multi_method_annotation.py +54 -54
  19. dtlpy/assets/service_runners/multi_method_dataset.py +55 -55
  20. dtlpy/assets/service_runners/multi_method_item.py +52 -52
  21. dtlpy/assets/service_runners/multi_method_json.py +52 -52
  22. dtlpy/assets/service_runners/single_method.py +37 -37
  23. dtlpy/assets/service_runners/single_method_annotation.py +43 -43
  24. dtlpy/assets/service_runners/single_method_dataset.py +43 -43
  25. dtlpy/assets/service_runners/single_method_item.py +41 -41
  26. dtlpy/assets/service_runners/single_method_json.py +42 -42
  27. dtlpy/assets/service_runners/single_method_multi_input.py +45 -45
  28. dtlpy/assets/voc_annotation_template.xml +23 -23
  29. dtlpy/caches/base_cache.py +32 -32
  30. dtlpy/caches/cache.py +473 -473
  31. dtlpy/caches/dl_cache.py +201 -201
  32. dtlpy/caches/filesystem_cache.py +89 -89
  33. dtlpy/caches/redis_cache.py +84 -84
  34. dtlpy/dlp/__init__.py +20 -20
  35. dtlpy/dlp/cli_utilities.py +367 -367
  36. dtlpy/dlp/command_executor.py +764 -764
  37. dtlpy/dlp/dlp +1 -1
  38. dtlpy/dlp/dlp.bat +1 -1
  39. dtlpy/dlp/dlp.py +128 -128
  40. dtlpy/dlp/parser.py +651 -651
  41. dtlpy/entities/__init__.py +83 -83
  42. dtlpy/entities/analytic.py +347 -347
  43. dtlpy/entities/annotation.py +1879 -1879
  44. dtlpy/entities/annotation_collection.py +699 -699
  45. dtlpy/entities/annotation_definitions/__init__.py +20 -20
  46. dtlpy/entities/annotation_definitions/base_annotation_definition.py +100 -100
  47. dtlpy/entities/annotation_definitions/box.py +195 -195
  48. dtlpy/entities/annotation_definitions/classification.py +67 -67
  49. dtlpy/entities/annotation_definitions/comparison.py +72 -72
  50. dtlpy/entities/annotation_definitions/cube.py +204 -204
  51. dtlpy/entities/annotation_definitions/cube_3d.py +149 -149
  52. dtlpy/entities/annotation_definitions/description.py +32 -32
  53. dtlpy/entities/annotation_definitions/ellipse.py +124 -124
  54. dtlpy/entities/annotation_definitions/free_text.py +62 -62
  55. dtlpy/entities/annotation_definitions/gis.py +69 -69
  56. dtlpy/entities/annotation_definitions/note.py +139 -139
  57. dtlpy/entities/annotation_definitions/point.py +117 -117
  58. dtlpy/entities/annotation_definitions/polygon.py +182 -182
  59. dtlpy/entities/annotation_definitions/polyline.py +111 -111
  60. dtlpy/entities/annotation_definitions/pose.py +92 -92
  61. dtlpy/entities/annotation_definitions/ref_image.py +86 -86
  62. dtlpy/entities/annotation_definitions/segmentation.py +240 -240
  63. dtlpy/entities/annotation_definitions/subtitle.py +34 -34
  64. dtlpy/entities/annotation_definitions/text.py +85 -85
  65. dtlpy/entities/annotation_definitions/undefined_annotation.py +74 -74
  66. dtlpy/entities/app.py +220 -220
  67. dtlpy/entities/app_module.py +107 -107
  68. dtlpy/entities/artifact.py +174 -174
  69. dtlpy/entities/assignment.py +399 -399
  70. dtlpy/entities/base_entity.py +214 -214
  71. dtlpy/entities/bot.py +113 -113
  72. dtlpy/entities/codebase.py +292 -292
  73. dtlpy/entities/collection.py +38 -38
  74. dtlpy/entities/command.py +169 -169
  75. dtlpy/entities/compute.py +449 -449
  76. dtlpy/entities/dataset.py +1299 -1299
  77. dtlpy/entities/directory_tree.py +44 -44
  78. dtlpy/entities/dpk.py +470 -470
  79. dtlpy/entities/driver.py +235 -235
  80. dtlpy/entities/execution.py +397 -397
  81. dtlpy/entities/feature.py +124 -124
  82. dtlpy/entities/feature_set.py +145 -145
  83. dtlpy/entities/filters.py +798 -798
  84. dtlpy/entities/gis_item.py +107 -107
  85. dtlpy/entities/integration.py +184 -184
  86. dtlpy/entities/item.py +959 -959
  87. dtlpy/entities/label.py +123 -123
  88. dtlpy/entities/links.py +85 -85
  89. dtlpy/entities/message.py +175 -175
  90. dtlpy/entities/model.py +684 -684
  91. dtlpy/entities/node.py +1005 -1005
  92. dtlpy/entities/ontology.py +810 -803
  93. dtlpy/entities/organization.py +287 -287
  94. dtlpy/entities/package.py +657 -657
  95. dtlpy/entities/package_defaults.py +5 -5
  96. dtlpy/entities/package_function.py +185 -185
  97. dtlpy/entities/package_module.py +113 -113
  98. dtlpy/entities/package_slot.py +118 -118
  99. dtlpy/entities/paged_entities.py +299 -299
  100. dtlpy/entities/pipeline.py +624 -624
  101. dtlpy/entities/pipeline_execution.py +279 -279
  102. dtlpy/entities/project.py +394 -394
  103. dtlpy/entities/prompt_item.py +505 -505
  104. dtlpy/entities/recipe.py +301 -301
  105. dtlpy/entities/reflect_dict.py +102 -102
  106. dtlpy/entities/resource_execution.py +138 -138
  107. dtlpy/entities/service.py +963 -963
  108. dtlpy/entities/service_driver.py +117 -117
  109. dtlpy/entities/setting.py +294 -294
  110. dtlpy/entities/task.py +495 -495
  111. dtlpy/entities/time_series.py +143 -143
  112. dtlpy/entities/trigger.py +426 -426
  113. dtlpy/entities/user.py +118 -118
  114. dtlpy/entities/webhook.py +124 -124
  115. dtlpy/examples/__init__.py +19 -19
  116. dtlpy/examples/add_labels.py +135 -135
  117. dtlpy/examples/add_metadata_to_item.py +21 -21
  118. dtlpy/examples/annotate_items_using_model.py +65 -65
  119. dtlpy/examples/annotate_video_using_model_and_tracker.py +75 -75
  120. dtlpy/examples/annotations_convert_to_voc.py +9 -9
  121. dtlpy/examples/annotations_convert_to_yolo.py +9 -9
  122. dtlpy/examples/convert_annotation_types.py +51 -51
  123. dtlpy/examples/converter.py +143 -143
  124. dtlpy/examples/copy_annotations.py +22 -22
  125. dtlpy/examples/copy_folder.py +31 -31
  126. dtlpy/examples/create_annotations.py +51 -51
  127. dtlpy/examples/create_video_annotations.py +83 -83
  128. dtlpy/examples/delete_annotations.py +26 -26
  129. dtlpy/examples/filters.py +113 -113
  130. dtlpy/examples/move_item.py +23 -23
  131. dtlpy/examples/play_video_annotation.py +13 -13
  132. dtlpy/examples/show_item_and_mask.py +53 -53
  133. dtlpy/examples/triggers.py +49 -49
  134. dtlpy/examples/upload_batch_of_items.py +20 -20
  135. dtlpy/examples/upload_items_and_custom_format_annotations.py +55 -55
  136. dtlpy/examples/upload_items_with_modalities.py +43 -43
  137. dtlpy/examples/upload_segmentation_annotations_from_mask_image.py +44 -44
  138. dtlpy/examples/upload_yolo_format_annotations.py +70 -70
  139. dtlpy/exceptions.py +125 -125
  140. dtlpy/miscellaneous/__init__.py +20 -20
  141. dtlpy/miscellaneous/dict_differ.py +95 -95
  142. dtlpy/miscellaneous/git_utils.py +217 -217
  143. dtlpy/miscellaneous/json_utils.py +14 -14
  144. dtlpy/miscellaneous/list_print.py +105 -105
  145. dtlpy/miscellaneous/zipping.py +130 -130
  146. dtlpy/ml/__init__.py +20 -20
  147. dtlpy/ml/base_feature_extractor_adapter.py +27 -27
  148. dtlpy/ml/base_model_adapter.py +1257 -1230
  149. dtlpy/ml/metrics.py +461 -461
  150. dtlpy/ml/predictions_utils.py +274 -274
  151. dtlpy/ml/summary_writer.py +57 -57
  152. dtlpy/ml/train_utils.py +60 -60
  153. dtlpy/new_instance.py +252 -252
  154. dtlpy/repositories/__init__.py +56 -56
  155. dtlpy/repositories/analytics.py +85 -85
  156. dtlpy/repositories/annotations.py +916 -916
  157. dtlpy/repositories/apps.py +383 -383
  158. dtlpy/repositories/artifacts.py +452 -452
  159. dtlpy/repositories/assignments.py +599 -599
  160. dtlpy/repositories/bots.py +213 -213
  161. dtlpy/repositories/codebases.py +559 -559
  162. dtlpy/repositories/collections.py +332 -332
  163. dtlpy/repositories/commands.py +152 -152
  164. dtlpy/repositories/compositions.py +61 -61
  165. dtlpy/repositories/computes.py +439 -439
  166. dtlpy/repositories/datasets.py +1504 -1504
  167. dtlpy/repositories/downloader.py +976 -923
  168. dtlpy/repositories/dpks.py +433 -433
  169. dtlpy/repositories/drivers.py +482 -482
  170. dtlpy/repositories/executions.py +815 -815
  171. dtlpy/repositories/feature_sets.py +226 -226
  172. dtlpy/repositories/features.py +255 -255
  173. dtlpy/repositories/integrations.py +484 -484
  174. dtlpy/repositories/items.py +912 -912
  175. dtlpy/repositories/messages.py +94 -94
  176. dtlpy/repositories/models.py +1000 -1000
  177. dtlpy/repositories/nodes.py +80 -80
  178. dtlpy/repositories/ontologies.py +511 -511
  179. dtlpy/repositories/organizations.py +525 -525
  180. dtlpy/repositories/packages.py +1941 -1941
  181. dtlpy/repositories/pipeline_executions.py +451 -451
  182. dtlpy/repositories/pipelines.py +640 -640
  183. dtlpy/repositories/projects.py +539 -539
  184. dtlpy/repositories/recipes.py +419 -399
  185. dtlpy/repositories/resource_executions.py +137 -137
  186. dtlpy/repositories/schema.py +120 -120
  187. dtlpy/repositories/service_drivers.py +213 -213
  188. dtlpy/repositories/services.py +1704 -1704
  189. dtlpy/repositories/settings.py +339 -339
  190. dtlpy/repositories/tasks.py +1477 -1477
  191. dtlpy/repositories/times_series.py +278 -278
  192. dtlpy/repositories/triggers.py +536 -536
  193. dtlpy/repositories/upload_element.py +257 -257
  194. dtlpy/repositories/uploader.py +661 -661
  195. dtlpy/repositories/webhooks.py +249 -249
  196. dtlpy/services/__init__.py +22 -22
  197. dtlpy/services/aihttp_retry.py +131 -131
  198. dtlpy/services/api_client.py +1785 -1785
  199. dtlpy/services/api_reference.py +40 -40
  200. dtlpy/services/async_utils.py +133 -133
  201. dtlpy/services/calls_counter.py +44 -44
  202. dtlpy/services/check_sdk.py +68 -68
  203. dtlpy/services/cookie.py +115 -115
  204. dtlpy/services/create_logger.py +156 -156
  205. dtlpy/services/events.py +84 -84
  206. dtlpy/services/logins.py +235 -235
  207. dtlpy/services/reporter.py +256 -256
  208. dtlpy/services/service_defaults.py +91 -91
  209. dtlpy/utilities/__init__.py +20 -20
  210. dtlpy/utilities/annotations/__init__.py +16 -16
  211. dtlpy/utilities/annotations/annotation_converters.py +269 -269
  212. dtlpy/utilities/base_package_runner.py +285 -264
  213. dtlpy/utilities/converter.py +1650 -1650
  214. dtlpy/utilities/dataset_generators/__init__.py +1 -1
  215. dtlpy/utilities/dataset_generators/dataset_generator.py +670 -670
  216. dtlpy/utilities/dataset_generators/dataset_generator_tensorflow.py +23 -23
  217. dtlpy/utilities/dataset_generators/dataset_generator_torch.py +21 -21
  218. dtlpy/utilities/local_development/__init__.py +1 -1
  219. dtlpy/utilities/local_development/local_session.py +179 -179
  220. dtlpy/utilities/reports/__init__.py +2 -2
  221. dtlpy/utilities/reports/figures.py +343 -343
  222. dtlpy/utilities/reports/report.py +71 -71
  223. dtlpy/utilities/videos/__init__.py +17 -17
  224. dtlpy/utilities/videos/video_player.py +598 -598
  225. dtlpy/utilities/videos/videos.py +470 -470
  226. {dtlpy-1.115.44.data → dtlpy-1.116.6.data}/scripts/dlp +1 -1
  227. dtlpy-1.116.6.data/scripts/dlp.bat +2 -0
  228. {dtlpy-1.115.44.data → dtlpy-1.116.6.data}/scripts/dlp.py +128 -128
  229. {dtlpy-1.115.44.dist-info → dtlpy-1.116.6.dist-info}/METADATA +186 -186
  230. dtlpy-1.116.6.dist-info/RECORD +239 -0
  231. {dtlpy-1.115.44.dist-info → dtlpy-1.116.6.dist-info}/WHEEL +1 -1
  232. {dtlpy-1.115.44.dist-info → dtlpy-1.116.6.dist-info}/licenses/LICENSE +200 -200
  233. tests/features/environment.py +551 -551
  234. dtlpy/assets/__pycache__/__init__.cpython-310.pyc +0 -0
  235. dtlpy-1.115.44.data/scripts/dlp.bat +0 -2
  236. dtlpy-1.115.44.dist-info/RECORD +0 -240
  237. {dtlpy-1.115.44.dist-info → dtlpy-1.116.6.dist-info}/entry_points.txt +0 -0
  238. {dtlpy-1.115.44.dist-info → dtlpy-1.116.6.dist-info}/top_level.txt +0 -0
dtlpy/entities/node.py CHANGED
@@ -1,1005 +1,1005 @@
1
- import warnings
2
-
3
- import inspect
4
- import json
5
- import logging
6
- import uuid
7
- from typing import Callable
8
- from enum import Enum
9
- from typing import List
10
- import datetime
11
-
12
- from .. import entities, assets, repositories, PlatformException
13
-
14
- NODE_SIZE = (200, 87)
15
-
16
- logger = logging.getLogger(name='dtlpy')
17
-
18
-
19
- class PipelineConnectionPort:
20
- def __init__(self, node_id: str, port_id: str):
21
- self.node_id = node_id
22
- self.port_id = port_id
23
-
24
- @staticmethod
25
- def from_json(_json: dict):
26
- return PipelineConnectionPort(
27
- node_id=_json.get('nodeId', None),
28
- port_id=_json.get('portId', None),
29
- )
30
-
31
- def to_json(self):
32
- _json = {
33
- 'nodeId': self.node_id,
34
- 'portId': self.port_id,
35
- }
36
- return _json
37
-
38
-
39
- class PipelineConnection:
40
- def __init__(self,
41
- source: PipelineConnectionPort,
42
- target: PipelineConnectionPort,
43
- filters: entities.Filters,
44
- action: str = None
45
- ):
46
- """
47
- :param PipelineConnectionPort source: the source pipeline connection
48
- :param PipelineConnectionPort target: the target pipeline connection
49
- :param entities.Filters filters: condition for the connection between the nodes
50
- :param str action: the action that move the input when it happen
51
- """
52
- self.source = source
53
- self.target = target
54
- self.filters = filters
55
- self.action = action
56
-
57
- @staticmethod
58
- def from_json(_json: dict):
59
- condition = _json.get('condition', None)
60
- if condition:
61
- condition = json.loads(condition)
62
- return PipelineConnection(
63
- source=PipelineConnectionPort.from_json(_json=_json.get('src', None)),
64
- target=PipelineConnectionPort.from_json(_json=_json.get('tgt', None)),
65
- filters=condition,
66
- action=_json.get('action', None)
67
- )
68
-
69
- def to_json(self):
70
- _json = {
71
- 'src': self.source.to_json(),
72
- 'tgt': self.target.to_json(),
73
- }
74
- if self.action:
75
- _json['action'] = self.action
76
- if self.filters:
77
- if isinstance(self.filters, entities.Filters):
78
- filters = self.filters.prepare(query_only=True).get('filter', dict())
79
- else:
80
- filters = self.filters
81
-
82
- _json['condition'] = json.dumps(filters)
83
- return _json
84
-
85
-
86
- class PipelineNodeIO:
87
- def __init__(self,
88
- input_type: entities.PackageInputType,
89
- name: str,
90
- display_name: str,
91
- port_id: str = None,
92
- color: tuple = None,
93
- port_percentage: int = None,
94
- default_value=None,
95
- variable_name: str = None,
96
- actions: list = None,
97
- description: str = None):
98
- """
99
- Pipeline Node
100
-
101
- :param entities.PackageInputType input_type: entities.PackageInputType of the input type of the pipeline
102
- :param str name: name of the input
103
- :param str display_name: of the input
104
- :param str port_id: port id
105
- :param tuple color: tuple the display the color
106
- :param int port_percentage: port percentage
107
- :param str action: the action that move the input when it happen
108
- :param default_value: default value of the input
109
- :param list actions: the actions list that move the input when it happen
110
- """
111
- self.port_id = port_id if port_id else str(uuid.uuid4())
112
- self.input_type = input_type
113
- self.name = name
114
- self.color = color
115
- self.display_name = display_name
116
- self.port_percentage = port_percentage
117
- self.default_value = default_value
118
- self.variable_name = variable_name
119
- self.description = description
120
- self.actions = actions
121
-
122
- @staticmethod
123
- def from_json(_json: dict):
124
- return PipelineNodeIO(
125
- port_id=_json.get('portId', None),
126
- input_type=_json.get('type', None),
127
- name=_json.get('name', None),
128
- color=_json.get('color', None),
129
- display_name=_json.get('displayName', None),
130
- port_percentage=_json.get('portPercentage', None),
131
- default_value=_json.get('defaultValue', None),
132
- variable_name=_json.get('variableName', None),
133
- actions=_json.get('actions', None),
134
- description=_json.get('description', None),
135
- )
136
-
137
- def to_json(self):
138
- _json = {
139
- 'portId': self.port_id,
140
- 'type': self.input_type,
141
- 'name': self.name,
142
- 'color': self.color,
143
- 'displayName': self.display_name,
144
- 'variableName': self.variable_name,
145
- 'portPercentage': self.port_percentage,
146
- }
147
-
148
- if self.actions:
149
- _json['actions'] = self.actions
150
- if self.default_value:
151
- _json['defaultValue'] = self.default_value
152
- if self.description:
153
- _json['description'] = self.description
154
- return _json
155
-
156
-
157
- class PipelineNodeType(str, Enum):
158
- TASK = 'task'
159
- CODE = 'code'
160
- FUNCTION = 'function'
161
- STORAGE = 'storage'
162
- ML = 'ml'
163
-
164
-
165
- class PipelineNameSpace:
166
- def __init__(self, function_name, project_name, module_name=None, service_name=None, package_name=None):
167
- self.function_name = function_name
168
- self.project_name = project_name
169
- self.module_name = module_name
170
- self.service_name = service_name
171
- self.package_name = package_name
172
-
173
- def to_json(self):
174
- _json = {
175
- "functionName": self.function_name,
176
- "projectName": self.project_name
177
- }
178
- if self.module_name:
179
- _json['moduleName'] = self.module_name
180
-
181
- if self.service_name:
182
- _json['serviceName'] = self.service_name
183
-
184
- if self.package_name:
185
- _json['packageName'] = self.package_name
186
- return _json
187
-
188
- @staticmethod
189
- def from_json(_json: dict):
190
- return PipelineNameSpace(
191
- function_name=_json.get('functionName'),
192
- project_name=_json.get('projectName'),
193
- module_name=_json.get('moduleName', None),
194
- service_name=_json.get('serviceName', None),
195
- package_name=_json.get('packageName', None)
196
- )
197
-
198
-
199
- class PipelineNode:
200
- def __init__(self,
201
- name: str,
202
- node_id: str,
203
- outputs: list,
204
- inputs: list,
205
- node_type: PipelineNodeType,
206
- namespace: PipelineNameSpace,
207
- project_id: str,
208
- metadata: dict = None,
209
- config: dict = None,
210
- position: tuple = (1, 1),
211
- app_id: str = None,
212
- dpk_name: str = None,
213
- app_name: str = None,
214
- ):
215
- """
216
- :param str name: node name
217
- :param str node_id: node id
218
- :param list outputs: list of PipelineNodeIO outputs
219
- :param list inputs: list of PipelineNodeIO inputs
220
- :param dict metadata: dict of the metadata of the node
221
- :param PipelineNodeType node_type: task, code, function
222
- :param PipelineNameSpace namespace: PipelineNameSpace of the node space
223
- :param str project_id: project id
224
- :param dict config: for the code node dict in format { package: {code : the_code}}
225
- :param tuple position: tuple of the node place
226
- :param str app_id: app id
227
- :param str dpk_name: dpk name
228
- :param str app_name: app name
229
- """
230
- self.name = name
231
- self.node_id = node_id
232
- self.outputs = outputs
233
- self.inputs = inputs
234
- self.metadata = metadata if metadata is not None else {}
235
- self.node_type = node_type
236
- self.namespace = namespace
237
- self.project_id = project_id
238
- self.config = config
239
- self.position = position
240
- self.app_id = app_id
241
- self.dpk_name = dpk_name
242
- self.app_name = app_name
243
- self._pipeline = None
244
-
245
- @property
246
- def position(self):
247
- position_tuple = (self.metadata['position']['x'],
248
- self.metadata['position']['y'])
249
- return position_tuple
250
-
251
- @position.setter
252
- def position(self, position):
253
- self.metadata['position'] = \
254
- {
255
- "x": position[0] * 1.7 * NODE_SIZE[0] + NODE_SIZE[0] / 2,
256
- "y": position[1] * 1.5 * NODE_SIZE[1] + NODE_SIZE[1],
257
- "z": 0
258
- }
259
-
260
- def _default_io(self, actions: list = None) -> PipelineNodeIO:
261
- """
262
- Create a default item pipeline input
263
-
264
- :param str actions: the action that move the input when it happen
265
- :return PipelineNodeIO: the default item PipelineNodeIO
266
- """
267
- default_io = PipelineNodeIO(port_id=str(uuid.uuid4()),
268
- input_type=entities.PackageInputType.ITEM,
269
- name='item',
270
- color=None,
271
- display_name=actions[0] if actions else 'item',
272
- actions=actions)
273
- return default_io
274
-
275
- @staticmethod
276
- def from_json(_json: dict):
277
- inputs = [PipelineNodeIO.from_json(_json=i_input) for i_input in _json.get('inputs', list())]
278
- outputs = [PipelineNodeIO.from_json(_json=i_output) for i_output in _json.get('outputs', list())]
279
- namespace = PipelineNameSpace.from_json(_json.get('namespace', {}))
280
- metadata = _json.get('metadata', {})
281
- position = ((metadata['position']['x'] - NODE_SIZE[0] / 2) / (1.7 * NODE_SIZE[0]),
282
- (metadata['position']['y'] - NODE_SIZE[1]) / (1.5 * NODE_SIZE[1]))
283
- return PipelineNode(
284
- name=_json.get('name', None),
285
- node_id=_json.get('id', None),
286
- outputs=outputs,
287
- inputs=inputs,
288
- metadata=metadata,
289
- node_type=_json.get('type', None),
290
- namespace=namespace,
291
- project_id=_json.get('projectId', None),
292
- config=_json.get('config', None),
293
- position=position,
294
- app_id=_json.get('appId', None),
295
- dpk_name=_json.get('dpkName', None),
296
- app_name=_json.get('appName', None),
297
- )
298
-
299
- def to_json(self):
300
- _json = {
301
- 'name': self.name,
302
- 'id': self.node_id,
303
- 'outputs': [_io.to_json() for _io in self.outputs],
304
- 'inputs': [_io.to_json() for _io in self.inputs],
305
- 'metadata': self.metadata,
306
- 'type': self.node_type,
307
- 'namespace': self.namespace.to_json(),
308
- 'projectId': self.project_id,
309
- 'dpkName': self.dpk_name,
310
- 'appName': self.app_name,
311
- }
312
- if self.config is not None:
313
- _json['config'] = self.config
314
- if self.app_id is not None:
315
- _json['appId'] = self.app_id
316
- return _json
317
-
318
- def is_root(self):
319
- if self._pipeline is not None:
320
- for node in self._pipeline.start_nodes:
321
- if self.node_id == node.get('nodeId', None) and node.get('type', None) == 'root':
322
- return True
323
- return False
324
-
325
- def _build_connection(self,
326
- node,
327
- source_port: PipelineNodeIO = None,
328
- target_port: PipelineNodeIO = None,
329
- filters: entities.Filters = None,
330
- action: str = None) -> PipelineConnection:
331
- """
332
- Build connection between the current node and the target node use the given ports
333
-
334
- :param PipelineNode node: the node to connect to it
335
- :param PipelineNodeIO source_port: the source PipelineNodeIO input port
336
- :param PipelineNodeIO target_port: the target PipelineNodeIO output port
337
- :param entities.Filters filters: condition for the connection between the nodes
338
- :param str action: the action that move the input when it happen
339
- :return: the connection between the nodes
340
- """
341
- if source_port is None and self.outputs:
342
- source_port = self.outputs[0]
343
-
344
- if target_port is None and node.inputs:
345
- target_port = node.inputs[0]
346
-
347
- if node.is_root():
348
- self._pipeline.set_start_node(self)
349
-
350
- source_connection = PipelineConnectionPort(node_id=self.node_id, port_id=source_port.port_id)
351
- target_connection = PipelineConnectionPort(node_id=node.node_id, port_id=target_port.port_id)
352
- if action is None and source_port.actions is not None and source_port.actions != []:
353
- action = source_port.actions[0]
354
- connection = PipelineConnection(source=source_connection, target=target_connection, filters=filters,
355
- action=action)
356
- return connection
357
-
358
- def connect(self,
359
- node,
360
- source_port: PipelineNodeIO = None,
361
- target_port: PipelineNodeIO = None,
362
- filters=None,
363
- action: str = None):
364
- """
365
- Build connection between the current node and the target node use the given ports
366
-
367
- :param PipelineNode node: the node to connect to it
368
- :param PipelineNodeIO source_port: the source PipelineNodeIO input port
369
- :param PipelineNodeIO target_port: the target PipelineNodeIO output port
370
- :param entities.Filters filters: condition for the connection between the nodes
371
- :param str action: the action that move the input when it happen
372
- :return: the connected node
373
- """
374
- if self._pipeline is None:
375
- raise Exception("must add the node to the pipeline first, e.g pipeline.nodes.add(node)")
376
- connection = self._build_connection(node=node,
377
- source_port=source_port,
378
- target_port=target_port,
379
- filters=filters,
380
- action=action)
381
- self._pipeline.connections.append(connection)
382
- self._pipeline.nodes.add(node)
383
- return node
384
-
385
- def disconnect(self,
386
- node,
387
- source_port: PipelineNodeIO = None,
388
- target_port: PipelineNodeIO = None) -> bool:
389
- """
390
- remove connection between the current node and the target node use the given ports
391
-
392
- :param PipelineNode node: the node to connect to it
393
- :param PipelineNodeIO source_port: the source PipelineNodeIO input port
394
- :param PipelineNodeIO target_port: the target PipelineNodeIO output port
395
- :return: true if success and false if not
396
- """
397
- if self._pipeline is None:
398
- raise Exception("must add the node to the pipeline first, e.g pipeline.nodes.add(node)")
399
- connection = self._build_connection(node=node,
400
- source_port=source_port,
401
- target_port=target_port,
402
- filters=None)
403
-
404
- current_connection = connection.to_json()
405
- if 'condition' in current_connection:
406
- current_connection = current_connection.pop('condition')
407
-
408
- for connection_index in range(len(self._pipeline.connections)):
409
- pipeline_connection = self._pipeline.connections[connection_index].to_json()
410
- if 'condition' in pipeline_connection:
411
- pipeline_connection = pipeline_connection.pop('condition')
412
-
413
- if current_connection == pipeline_connection:
414
- self._pipeline.connections.pop(connection_index)
415
- return True
416
- logger.warning('do not found a connection')
417
- return False
418
-
419
- def add_trigger(self,
420
- trigger_type: entities.TriggerType = entities.TriggerType.EVENT,
421
- filters=None,
422
- resource: entities.TriggerResource = entities.TriggerResource.ITEM,
423
- actions: entities.TriggerAction = entities.TriggerAction.CREATED,
424
- execution_mode: entities.TriggerExecutionMode = entities.TriggerExecutionMode.ONCE,
425
- cron: str = None,
426
- ):
427
- """
428
- Create a Trigger. Can create two types: a cron trigger or an event trigger.
429
- Inputs are different for each type
430
-
431
- Inputs for all types:
432
-
433
- :param trigger_type: can be cron or event. use enum dl.TriggerType for the full list
434
-
435
- Inputs for event trigger:
436
- :param filters: optional - Item/Annotation metadata filters, default = none
437
- :param resource: optional - Dataset/Item/Annotation/ItemStatus, default = Item
438
- :param actions: optional - Created/Updated/Deleted, default = create
439
- :param execution_mode: how many time trigger should be activate. default is "Once". enum dl.TriggerExecutionMode
440
-
441
- Inputs for cron trigger:
442
- :param str cron: cron spec specifying when it should run. more information: https://en.wikipedia.org/wiki/Cron
443
-
444
- :return: Trigger entity
445
- """
446
- if self._pipeline is None:
447
- raise Exception("must add the node to the pipeline first, e.g pipeline.nodes.add(node)")
448
-
449
- if not isinstance(actions, list):
450
- actions = [actions]
451
-
452
- if filters is None:
453
- filters = {}
454
- else:
455
- filters = json.dumps(filters.prepare(query_only=True).get('filter', dict()))
456
-
457
- if trigger_type == entities.TriggerType.EVENT:
458
- spec = {
459
- 'filter': filters,
460
- 'resource': resource,
461
- 'executionMode': execution_mode,
462
- 'actions': actions
463
- }
464
- elif trigger_type == entities.TriggerType.CRON:
465
- spec = {
466
- 'cron': cron,
467
- }
468
- else:
469
- raise ValueError('Unknown trigger type: "{}". Use dl.TriggerType for known types'.format(trigger_type))
470
-
471
- trigger = {
472
- "type": trigger_type,
473
- "spec": spec
474
- }
475
-
476
- set_trigger = False
477
- for pipe_node in self._pipeline.start_nodes:
478
- if pipe_node['nodeId'] == self.node_id:
479
- set_trigger = True
480
- pipe_node['trigger'] = trigger
481
-
482
- if not set_trigger:
483
- self._pipeline.start_nodes.append(
484
- {
485
- "nodeId": self.node_id,
486
- "type": "trigger",
487
- 'trigger': trigger
488
- }
489
- )
490
-
491
-
492
- class CodeNode(PipelineNode):
493
- def __init__(self,
494
- name: str,
495
- project_id: str,
496
- project_name: str,
497
- method: Callable,
498
- outputs: List[PipelineNodeIO] = None,
499
- inputs: List[PipelineNodeIO] = None,
500
- position: tuple = (1, 1),
501
- ):
502
- """
503
- :param str name: node name
504
- :param str project_id: project id
505
- :param str project_name: project name
506
- :param Callable method: function to deploy
507
- :param list outputs: list of PipelineNodeIO outputs
508
- :param list inputs: list of PipelineNodeIO inputs
509
- :param tuple position: tuple of the node place
510
- """
511
- if inputs is None:
512
- inputs = [self._default_io()]
513
- if outputs is None:
514
- outputs = [self._default_io()]
515
-
516
- if method is None or not isinstance(method, Callable):
517
- raise Exception('must provide a function as input')
518
- else:
519
- function_code = self._build_code_from_func(method)
520
- function_name = method.__name__
521
-
522
- super().__init__(name=name,
523
- node_id=str(uuid.uuid4()),
524
- outputs=outputs,
525
- inputs=inputs,
526
- metadata={},
527
- node_type=PipelineNodeType.CODE,
528
- namespace=PipelineNameSpace(function_name=function_name, project_name=project_name),
529
- project_id=project_id,
530
- position=position)
531
-
532
- self.config = {
533
- "package":
534
- {
535
- "code": function_code,
536
- "name": function_name,
537
- "type": "code"
538
- }
539
- }
540
-
541
- def _build_code_from_func(self, func: Callable) -> str:
542
- """
543
- Build a code format from the given function
544
-
545
- :param Callable func: function to deploy
546
- :return: a string the display the code with the package format
547
- """
548
- with open(assets.paths.PARTIAL_MAIN_FILEPATH, 'r') as f:
549
- main_string = f.read()
550
- lines = inspect.getsourcelines(func)
551
-
552
- tabs_diff = lines[0][0].count(' ') - 1
553
- for line_index in range(len(lines[0])):
554
- line_tabs = lines[0][line_index].count(' ') - tabs_diff
555
- lines[0][line_index] = (' ' * line_tabs) + lines[0][line_index].strip() + '\n'
556
-
557
- method_func_string = "".join(lines[0])
558
-
559
- code = '{}\n{}\n @staticmethod\n{}'.format('', main_string,
560
- method_func_string)
561
- return code
562
-
563
- @staticmethod
564
- def from_json(_json: dict):
565
- parent = PipelineNode.from_json(_json)
566
- parent.__class__ = CodeNode
567
- return parent
568
-
569
-
570
- class TaskNode(PipelineNode):
571
- def __init__(self,
572
- name: str,
573
- project_id: str,
574
- dataset_id: str,
575
- recipe_title: str,
576
- recipe_id: str,
577
- task_owner: str,
578
- workload: List[entities.WorkloadUnit],
579
- task_type: str = 'annotation',
580
- position: tuple = (1, 1),
581
- actions: list = None,
582
- repeatable: bool = True,
583
- batch_size=None,
584
- max_batch_workload=None,
585
- priority=entities.TaskPriority.MEDIUM,
586
- due_date=None,
587
- consensus_task_type=None,
588
- consensus_percentage=None,
589
- consensus_assignees=None,
590
- groups=None
591
- ):
592
- """
593
- :param str name: node name
594
- :param str project_id: project id
595
- :param str dataset_id: dataset id
596
- :param str recipe_title: recipe title
597
- :param str recipe_id: recipe id
598
- :param str task_owner: email of task owner
599
- :param List[WorkloadUnit] workload: list of WorkloadUnit
600
- :param str task_type: 'annotation' or 'qa'
601
- :param tuple position: tuple of the node place
602
- :param list actions: list of task actions
603
- :param bool repeatable: can repeat in the item
604
- :param int groups: groups to assign the task to
605
- :param int batch_size: Pulling batch size (items) . Restrictions - Min 3, max 100 - for create pulling task
606
- :param int max_batch_workload: Max items in assignment . Restrictions - Min batchSize + 2 , max batchSize * 2 - for create pulling task
607
- :param entities.TaskPriority priority: priority of the task options in entities.TaskPriority
608
- :param float due_date: date by which the task should be finished; for example, due_date = datetime.datetime(day= 1, month= 1, year= 2029).timestamp()
609
- :param entities.ConsensusTaskType consensus_task_type: consensus_task_type of the task options in entities.ConsensusTaskType
610
- :param int consensus_percentage: percentage of items to be copied to multiple annotators (consensus items)
611
- :param int consensus_assignees: the number of different annotators per item (number of copies per item)
612
- """
613
- if actions is None or actions == []:
614
- actions = []
615
- if task_type == 'qa':
616
- if 'approve' not in actions:
617
- actions.append('approve')
618
- else:
619
- if 'complete' not in actions:
620
- actions.append('complete')
621
- actions.append('discard')
622
- else:
623
- logger.warning(
624
- "The 'actions' field was updated to override the system default actions for task (complete/approve, discard) if provided, due to a bug fix.")
625
-
626
- inputs = [self._default_io()]
627
-
628
- outputs = [self._default_io(actions=actions)]
629
-
630
- if groups is not None:
631
- if not isinstance(groups, list) or not all(isinstance(group, str) for group in groups):
632
- raise ValueError('groups must be a list of strings')
633
-
634
- super().__init__(name=name,
635
- node_id=str(uuid.uuid4()),
636
- outputs=outputs,
637
- inputs=inputs,
638
- metadata=dict(),
639
- node_type=PipelineNodeType.TASK,
640
- namespace=PipelineNameSpace(function_name="move_to_task",
641
- project_name="DataloopTasks",
642
- service_name="pipeline-utils"),
643
- project_id=project_id,
644
- position=position)
645
-
646
- self.dataset_id = dataset_id
647
- self.recipe_title = recipe_title
648
- self.recipe_id = recipe_id
649
- self.task_owner = task_owner
650
- self.task_type = task_type
651
- if not isinstance(workload, list):
652
- workload = [workload]
653
- self.workload = workload
654
- self.repeatable = repeatable
655
- if max_batch_workload:
656
- self.max_batch_workload = max_batch_workload
657
- if batch_size:
658
- self.batch_size = batch_size
659
- if consensus_task_type:
660
- self.consensus_task_type = consensus_task_type
661
- if consensus_percentage:
662
- self.consensus_percentage = consensus_percentage
663
- if consensus_assignees:
664
- self.consensus_assignees = consensus_assignees
665
- self.priority = priority
666
- if due_date is None:
667
- due_date = (datetime.datetime.now() + datetime.timedelta(days=7)).timestamp() * 1000
668
- self.due_date = due_date
669
- self.groups = groups
670
-
671
- @property
672
- def dataset_id(self):
673
- return self.metadata['datasetId']
674
-
675
- @dataset_id.setter
676
- def dataset_id(self, dataset_id: str):
677
- if not isinstance(dataset_id, str):
678
- raise PlatformException('400', 'Param dataset_id must be of type string')
679
- self.metadata['datasetId'] = dataset_id
680
-
681
- @property
682
- def groups(self):
683
- return self.metadata.get('groups')
684
-
685
- @groups.setter
686
- def groups(self, groups: List[str]):
687
- if groups is not None:
688
- self.metadata['groups'] = groups
689
-
690
- @property
691
- def repeatable(self):
692
- return self.metadata['repeatable']
693
-
694
- @repeatable.setter
695
- def repeatable(self, repeatable: bool):
696
- if not isinstance(repeatable, bool):
697
- raise PlatformException('400', 'Param repeatable must be of type bool')
698
- self.metadata['repeatable'] = repeatable
699
-
700
- @property
701
- def recipe_title(self):
702
- return self.metadata['recipeTitle']
703
-
704
- @recipe_title.setter
705
- def recipe_title(self, recipe_title: str):
706
- if not isinstance(recipe_title, str):
707
- raise PlatformException('400', 'Param recipe_title must be of type string')
708
- self.metadata['recipeTitle'] = recipe_title
709
-
710
- @property
711
- def recipe_id(self):
712
- return self.metadata['recipeId']
713
-
714
- @recipe_id.setter
715
- def recipe_id(self, recipe_id: str):
716
- if not isinstance(recipe_id, str):
717
- raise PlatformException('400', 'Param recipe_id must be of type string')
718
- self.metadata['recipeId'] = recipe_id
719
-
720
- @property
721
- def task_owner(self):
722
- return self.metadata['taskOwner']
723
-
724
- @task_owner.setter
725
- def task_owner(self, task_owner: str):
726
- if not isinstance(task_owner, str):
727
- raise PlatformException('400', 'Param task_owner must be of type string')
728
- self.metadata['taskOwner'] = task_owner
729
-
730
- @property
731
- def task_type(self):
732
- return self.metadata['taskType']
733
-
734
- @task_type.setter
735
- def task_type(self, task_type: str):
736
- if not isinstance(task_type, str):
737
- raise PlatformException('400', 'Param task_type must be of type string')
738
- self.metadata['taskType'] = task_type
739
-
740
- @property
741
- def workload(self):
742
- return self.metadata['workload']
743
-
744
- @workload.setter
745
- def workload(self, workload: list):
746
- if not isinstance(workload, list):
747
- workload = [workload]
748
- self.metadata['workload'] = [val.to_json() for val in workload]
749
-
750
- @property
751
- def batch_size(self):
752
- return self.metadata['batchSize']
753
-
754
- @batch_size.setter
755
- def batch_size(self, batch_size: int):
756
- if not isinstance(batch_size, int):
757
- raise PlatformException('400', 'Param batch_size must be of type int')
758
- self.metadata['batchSize'] = batch_size
759
-
760
- @property
761
- def max_batch_workload(self):
762
- return self.metadata['maxBatchWorkload']
763
-
764
- @max_batch_workload.setter
765
- def max_batch_workload(self, max_batch_workload: int):
766
- if not isinstance(max_batch_workload, int):
767
- raise PlatformException('400', 'Param max_batch_workload must be of type int')
768
- self.metadata['maxBatchWorkload'] = max_batch_workload
769
-
770
- @property
771
- def consensus_task_type(self):
772
- return self.metadata['consensusTaskType']
773
-
774
- @consensus_task_type.setter
775
- def consensus_task_type(self, consensus_task_type: entities.ConsensusTaskType):
776
- if not isinstance(consensus_task_type, str) and not isinstance(consensus_task_type, entities.ConsensusTaskType):
777
- raise PlatformException('400', 'Param consensus_task_type must be of type entities.ConsensusTaskType')
778
- self.metadata['consensusTaskType'] = consensus_task_type
779
-
780
- @property
781
- def consensus_percentage(self):
782
- return self.metadata['consensusPercentage']
783
-
784
- @consensus_percentage.setter
785
- def consensus_percentage(self, consensus_percentage: int):
786
- if not isinstance(consensus_percentage, int):
787
- raise PlatformException('400', 'Param consensus_percentage must be of type int')
788
- self.metadata['consensusPercentage'] = consensus_percentage
789
-
790
- @property
791
- def consensus_assignees(self):
792
- return self.metadata['consensusAssignees']
793
-
794
- @consensus_assignees.setter
795
- def consensus_assignees(self, consensus_assignees: int):
796
- if not isinstance(consensus_assignees, int):
797
- raise PlatformException('400', 'Param consensus_assignees must be of type int')
798
- self.metadata['consensusAssignees'] = consensus_assignees
799
-
800
- @property
801
- def priority(self):
802
- return self.metadata['priority']
803
-
804
- @priority.setter
805
- def priority(self, priority: entities.TaskPriority):
806
- if not isinstance(priority, int) and not isinstance(priority, entities.TaskPriority):
807
- raise PlatformException('400', 'Param priority must be of type entities.TaskPriority')
808
- self.metadata['priority'] = priority
809
-
810
- @property
811
- def due_date(self):
812
- return self.metadata['dueDate']
813
-
814
- @due_date.setter
815
- def due_date(self, due_date: float):
816
- if not isinstance(due_date, float) and not isinstance(due_date, int):
817
- raise PlatformException('400', 'Param due_date must be of type float or int')
818
- self.metadata['dueDate'] = due_date
819
-
820
- @staticmethod
821
- def from_json(_json: dict):
822
- parent = PipelineNode.from_json(_json)
823
- parent.__class__ = TaskNode
824
- return parent
825
-
826
-
827
- class FunctionNode(PipelineNode):
828
- def __init__(self,
829
- name: str,
830
- service: entities.Service,
831
- function_name,
832
- position: tuple = (1, 1),
833
- project_id=None,
834
- project_name=None
835
- ):
836
- """
837
- :param str name: node name
838
- :param entities.Service service: service to deploy
839
- :param str function_name: function name
840
- :param tuple position: tuple of the node place
841
- """
842
- self.service = service
843
-
844
- if project_id is None:
845
- project_id = service.project_id
846
- if project_id != service.project_id:
847
- logger.warning("the project id that provide different from the service project id")
848
-
849
- if project_name is None:
850
- try:
851
- project = repositories.Projects(client_api=self.service._client_api).get(project_id=project_id,
852
- log_error=False)
853
- project_name = project.name
854
- except:
855
- logger.warning(
856
- 'Service project not found using DataloopTasks project.'
857
- ' If this is incorrect please provide project_name param.')
858
- project_name = 'DataloopTasks'
859
- inputs = []
860
- outputs = []
861
- package = self.service.package
862
- modules = []
863
- if isinstance(package, entities.Package):
864
- modules = package.modules
865
- elif isinstance(package, entities.Dpk):
866
- modules = package.components.modules
867
- for model in modules:
868
- if model.name == self.service.module_name:
869
- for func in model.functions:
870
- if func.name == function_name:
871
- inputs = self._convert_from_function_io_to_pipeline_io(func.inputs)
872
- outputs = self._convert_from_function_io_to_pipeline_io(func.outputs)
873
-
874
- namespace = PipelineNameSpace(
875
- function_name=function_name,
876
- service_name=self.service.name,
877
- module_name=self.service.module_name,
878
- package_name=self.service.package.name,
879
- project_name=project_name
880
- )
881
- super().__init__(name=name,
882
- node_id=str(uuid.uuid4()),
883
- outputs=outputs,
884
- inputs=inputs,
885
- metadata={},
886
- node_type=PipelineNodeType.FUNCTION,
887
- namespace=namespace,
888
- project_id=service.project_id,
889
- position=position)
890
-
891
- def _convert_from_function_io_to_pipeline_io(self, function_io: List[entities.FunctionIO]) -> List[PipelineNodeIO]:
892
- """
893
- Get a list of FunctionIO and convert them to PipelineIO
894
- :param List[entities.FunctionIO] function_io: list of functionIO
895
- :return: list of PipelineIO
896
- """
897
- pipeline_io = []
898
- for single_input in function_io:
899
- pipeline_io.append(
900
- PipelineNodeIO(port_id=str(uuid.uuid4()),
901
- input_type=single_input.type,
902
- name=single_input.name,
903
- color=None,
904
- display_name=single_input.name,
905
- default_value=single_input.value,
906
- actions=single_input.actions if single_input.actions is not None else []))
907
- return pipeline_io
908
-
909
- @staticmethod
910
- def from_json(_json: dict):
911
- parent = PipelineNode.from_json(_json)
912
- parent.__class__ = FunctionNode
913
- return parent
914
-
915
-
916
- class DatasetNode(PipelineNode):
917
- def __init__(self,
918
- name: str,
919
- project_id: str,
920
- dataset_id: str,
921
- dataset_folder: str = None,
922
- load_existing_data: bool = False,
923
- data_filters: entities.Filters = None,
924
- position: tuple = (1, 1)):
925
- """
926
- :param str name: node name
927
- :param str project_id: project id
928
- :param str dataset_id: dataset id
929
- :param str dataset_folder: folder in dataset to work in it
930
- :param bool load_existing_data: optional - enable to automatically load existing data into the
931
- pipeline (executions) upon activation, based on the defined dataset,
932
- folder, and data_filters.
933
- :param entities.Filters data_filters: optional - filters entity or a dictionary containing filters parameters.
934
- Use to filter the data items to be loaded when load_existing_data
935
- is enabled.
936
- :param tuple position: tuple of the node place
937
- """
938
- inputs = [self._default_io()]
939
- outputs = [self._default_io()]
940
- super().__init__(name=name,
941
- node_id=str(uuid.uuid4()),
942
- outputs=outputs,
943
- inputs=inputs,
944
- metadata={},
945
- node_type=PipelineNodeType.STORAGE,
946
- namespace=PipelineNameSpace(function_name="dataset_handler",
947
- project_name="DataloopTasks",
948
- service_name="pipeline-utils"),
949
- project_id=project_id,
950
- position=position)
951
- self.dataset_id = dataset_id
952
- self.dataset_folder = dataset_folder
953
- self.load_existing_data = load_existing_data
954
- self.data_filters = data_filters
955
-
956
- @property
957
- def dataset_id(self):
958
- return self.metadata['datasetId']
959
-
960
- @dataset_id.setter
961
- def dataset_id(self, dataset_id: str):
962
- self.metadata['datasetId'] = dataset_id
963
-
964
- @property
965
- def dataset_folder(self):
966
- return self.metadata.get('dir', None)
967
-
968
- @dataset_folder.setter
969
- def dataset_folder(self, dataset_folder: str):
970
- if dataset_folder is not None:
971
- if not dataset_folder.startswith("/"):
972
- dataset_folder = '/' + dataset_folder
973
- self.metadata['dir'] = dataset_folder
974
-
975
- @property
976
- def load_existing_data(self):
977
- return self.metadata.get('triggerToPipeline', {}).get('active', False)
978
-
979
- @load_existing_data.setter
980
- def load_existing_data(self, load_existing_data: bool):
981
- if load_existing_data:
982
- self.metadata.setdefault('triggerToPipeline', {})['active'] = True
983
- else:
984
- self.metadata.pop('triggerToPipeline', None)
985
-
986
- @property
987
- def data_filters(self):
988
- data_filters = self.metadata.get('triggerToPipeline', {}).get('filter', None)
989
- if data_filters:
990
- data_filters = entities.Filters(custom_filter=json.loads(data_filters))
991
- return data_filters
992
-
993
- @data_filters.setter
994
- def data_filters(self, data_filters: entities.Filters):
995
- if data_filters is None:
996
- filters = None
997
- else:
998
- filters = json.dumps(data_filters.prepare(query_only=True).get('filter'))
999
- self.metadata.setdefault('triggerToPipeline', {})['filter'] = filters
1000
-
1001
- @staticmethod
1002
- def from_json(_json: dict):
1003
- parent = PipelineNode.from_json(_json)
1004
- parent.__class__ = DatasetNode
1005
- return parent
1
+ import warnings
2
+
3
+ import inspect
4
+ import json
5
+ import logging
6
+ import uuid
7
+ from typing import Callable
8
+ from enum import Enum
9
+ from typing import List
10
+ import datetime
11
+
12
+ from .. import entities, assets, repositories, PlatformException
13
+
14
+ NODE_SIZE = (200, 87)
15
+
16
+ logger = logging.getLogger(name='dtlpy')
17
+
18
+
19
+ class PipelineConnectionPort:
20
+ def __init__(self, node_id: str, port_id: str):
21
+ self.node_id = node_id
22
+ self.port_id = port_id
23
+
24
+ @staticmethod
25
+ def from_json(_json: dict):
26
+ return PipelineConnectionPort(
27
+ node_id=_json.get('nodeId', None),
28
+ port_id=_json.get('portId', None),
29
+ )
30
+
31
+ def to_json(self):
32
+ _json = {
33
+ 'nodeId': self.node_id,
34
+ 'portId': self.port_id,
35
+ }
36
+ return _json
37
+
38
+
39
+ class PipelineConnection:
40
+ def __init__(self,
41
+ source: PipelineConnectionPort,
42
+ target: PipelineConnectionPort,
43
+ filters: entities.Filters,
44
+ action: str = None
45
+ ):
46
+ """
47
+ :param PipelineConnectionPort source: the source pipeline connection
48
+ :param PipelineConnectionPort target: the target pipeline connection
49
+ :param entities.Filters filters: condition for the connection between the nodes
50
+ :param str action: the action that move the input when it happen
51
+ """
52
+ self.source = source
53
+ self.target = target
54
+ self.filters = filters
55
+ self.action = action
56
+
57
+ @staticmethod
58
+ def from_json(_json: dict):
59
+ condition = _json.get('condition', None)
60
+ if condition:
61
+ condition = json.loads(condition)
62
+ return PipelineConnection(
63
+ source=PipelineConnectionPort.from_json(_json=_json.get('src', None)),
64
+ target=PipelineConnectionPort.from_json(_json=_json.get('tgt', None)),
65
+ filters=condition,
66
+ action=_json.get('action', None)
67
+ )
68
+
69
+ def to_json(self):
70
+ _json = {
71
+ 'src': self.source.to_json(),
72
+ 'tgt': self.target.to_json(),
73
+ }
74
+ if self.action:
75
+ _json['action'] = self.action
76
+ if self.filters:
77
+ if isinstance(self.filters, entities.Filters):
78
+ filters = self.filters.prepare(query_only=True).get('filter', dict())
79
+ else:
80
+ filters = self.filters
81
+
82
+ _json['condition'] = json.dumps(filters)
83
+ return _json
84
+
85
+
86
+ class PipelineNodeIO:
87
+ def __init__(self,
88
+ input_type: entities.PackageInputType,
89
+ name: str,
90
+ display_name: str,
91
+ port_id: str = None,
92
+ color: tuple = None,
93
+ port_percentage: int = None,
94
+ default_value=None,
95
+ variable_name: str = None,
96
+ actions: list = None,
97
+ description: str = None):
98
+ """
99
+ Pipeline Node
100
+
101
+ :param entities.PackageInputType input_type: entities.PackageInputType of the input type of the pipeline
102
+ :param str name: name of the input
103
+ :param str display_name: of the input
104
+ :param str port_id: port id
105
+ :param tuple color: tuple the display the color
106
+ :param int port_percentage: port percentage
107
+ :param str action: the action that move the input when it happen
108
+ :param default_value: default value of the input
109
+ :param list actions: the actions list that move the input when it happen
110
+ """
111
+ self.port_id = port_id if port_id else str(uuid.uuid4())
112
+ self.input_type = input_type
113
+ self.name = name
114
+ self.color = color
115
+ self.display_name = display_name
116
+ self.port_percentage = port_percentage
117
+ self.default_value = default_value
118
+ self.variable_name = variable_name
119
+ self.description = description
120
+ self.actions = actions
121
+
122
+ @staticmethod
123
+ def from_json(_json: dict):
124
+ return PipelineNodeIO(
125
+ port_id=_json.get('portId', None),
126
+ input_type=_json.get('type', None),
127
+ name=_json.get('name', None),
128
+ color=_json.get('color', None),
129
+ display_name=_json.get('displayName', None),
130
+ port_percentage=_json.get('portPercentage', None),
131
+ default_value=_json.get('defaultValue', None),
132
+ variable_name=_json.get('variableName', None),
133
+ actions=_json.get('actions', None),
134
+ description=_json.get('description', None),
135
+ )
136
+
137
+ def to_json(self):
138
+ _json = {
139
+ 'portId': self.port_id,
140
+ 'type': self.input_type,
141
+ 'name': self.name,
142
+ 'color': self.color,
143
+ 'displayName': self.display_name,
144
+ 'variableName': self.variable_name,
145
+ 'portPercentage': self.port_percentage,
146
+ }
147
+
148
+ if self.actions:
149
+ _json['actions'] = self.actions
150
+ if self.default_value:
151
+ _json['defaultValue'] = self.default_value
152
+ if self.description:
153
+ _json['description'] = self.description
154
+ return _json
155
+
156
+
157
+ class PipelineNodeType(str, Enum):
158
+ TASK = 'task'
159
+ CODE = 'code'
160
+ FUNCTION = 'function'
161
+ STORAGE = 'storage'
162
+ ML = 'ml'
163
+
164
+
165
+ class PipelineNameSpace:
166
+ def __init__(self, function_name, project_name, module_name=None, service_name=None, package_name=None):
167
+ self.function_name = function_name
168
+ self.project_name = project_name
169
+ self.module_name = module_name
170
+ self.service_name = service_name
171
+ self.package_name = package_name
172
+
173
+ def to_json(self):
174
+ _json = {
175
+ "functionName": self.function_name,
176
+ "projectName": self.project_name
177
+ }
178
+ if self.module_name:
179
+ _json['moduleName'] = self.module_name
180
+
181
+ if self.service_name:
182
+ _json['serviceName'] = self.service_name
183
+
184
+ if self.package_name:
185
+ _json['packageName'] = self.package_name
186
+ return _json
187
+
188
+ @staticmethod
189
+ def from_json(_json: dict):
190
+ return PipelineNameSpace(
191
+ function_name=_json.get('functionName'),
192
+ project_name=_json.get('projectName'),
193
+ module_name=_json.get('moduleName', None),
194
+ service_name=_json.get('serviceName', None),
195
+ package_name=_json.get('packageName', None)
196
+ )
197
+
198
+
199
+ class PipelineNode:
200
+ def __init__(self,
201
+ name: str,
202
+ node_id: str,
203
+ outputs: list,
204
+ inputs: list,
205
+ node_type: PipelineNodeType,
206
+ namespace: PipelineNameSpace,
207
+ project_id: str,
208
+ metadata: dict = None,
209
+ config: dict = None,
210
+ position: tuple = (1, 1),
211
+ app_id: str = None,
212
+ dpk_name: str = None,
213
+ app_name: str = None,
214
+ ):
215
+ """
216
+ :param str name: node name
217
+ :param str node_id: node id
218
+ :param list outputs: list of PipelineNodeIO outputs
219
+ :param list inputs: list of PipelineNodeIO inputs
220
+ :param dict metadata: dict of the metadata of the node
221
+ :param PipelineNodeType node_type: task, code, function
222
+ :param PipelineNameSpace namespace: PipelineNameSpace of the node space
223
+ :param str project_id: project id
224
+ :param dict config: for the code node dict in format { package: {code : the_code}}
225
+ :param tuple position: tuple of the node place
226
+ :param str app_id: app id
227
+ :param str dpk_name: dpk name
228
+ :param str app_name: app name
229
+ """
230
+ self.name = name
231
+ self.node_id = node_id
232
+ self.outputs = outputs
233
+ self.inputs = inputs
234
+ self.metadata = metadata if metadata is not None else {}
235
+ self.node_type = node_type
236
+ self.namespace = namespace
237
+ self.project_id = project_id
238
+ self.config = config
239
+ self.position = position
240
+ self.app_id = app_id
241
+ self.dpk_name = dpk_name
242
+ self.app_name = app_name
243
+ self._pipeline = None
244
+
245
+ @property
246
+ def position(self):
247
+ position_tuple = (self.metadata['position']['x'],
248
+ self.metadata['position']['y'])
249
+ return position_tuple
250
+
251
+ @position.setter
252
+ def position(self, position):
253
+ self.metadata['position'] = \
254
+ {
255
+ "x": position[0] * 1.7 * NODE_SIZE[0] + NODE_SIZE[0] / 2,
256
+ "y": position[1] * 1.5 * NODE_SIZE[1] + NODE_SIZE[1],
257
+ "z": 0
258
+ }
259
+
260
+ def _default_io(self, actions: list = None) -> PipelineNodeIO:
261
+ """
262
+ Create a default item pipeline input
263
+
264
+ :param str actions: the action that move the input when it happen
265
+ :return PipelineNodeIO: the default item PipelineNodeIO
266
+ """
267
+ default_io = PipelineNodeIO(port_id=str(uuid.uuid4()),
268
+ input_type=entities.PackageInputType.ITEM,
269
+ name='item',
270
+ color=None,
271
+ display_name=actions[0] if actions else 'item',
272
+ actions=actions)
273
+ return default_io
274
+
275
+ @staticmethod
276
+ def from_json(_json: dict):
277
+ inputs = [PipelineNodeIO.from_json(_json=i_input) for i_input in _json.get('inputs', list())]
278
+ outputs = [PipelineNodeIO.from_json(_json=i_output) for i_output in _json.get('outputs', list())]
279
+ namespace = PipelineNameSpace.from_json(_json.get('namespace', {}))
280
+ metadata = _json.get('metadata', {})
281
+ position = ((metadata['position']['x'] - NODE_SIZE[0] / 2) / (1.7 * NODE_SIZE[0]),
282
+ (metadata['position']['y'] - NODE_SIZE[1]) / (1.5 * NODE_SIZE[1]))
283
+ return PipelineNode(
284
+ name=_json.get('name', None),
285
+ node_id=_json.get('id', None),
286
+ outputs=outputs,
287
+ inputs=inputs,
288
+ metadata=metadata,
289
+ node_type=_json.get('type', None),
290
+ namespace=namespace,
291
+ project_id=_json.get('projectId', None),
292
+ config=_json.get('config', None),
293
+ position=position,
294
+ app_id=_json.get('appId', None),
295
+ dpk_name=_json.get('dpkName', None),
296
+ app_name=_json.get('appName', None),
297
+ )
298
+
299
+ def to_json(self):
300
+ _json = {
301
+ 'name': self.name,
302
+ 'id': self.node_id,
303
+ 'outputs': [_io.to_json() for _io in self.outputs],
304
+ 'inputs': [_io.to_json() for _io in self.inputs],
305
+ 'metadata': self.metadata,
306
+ 'type': self.node_type,
307
+ 'namespace': self.namespace.to_json(),
308
+ 'projectId': self.project_id,
309
+ 'dpkName': self.dpk_name,
310
+ 'appName': self.app_name,
311
+ }
312
+ if self.config is not None:
313
+ _json['config'] = self.config
314
+ if self.app_id is not None:
315
+ _json['appId'] = self.app_id
316
+ return _json
317
+
318
+ def is_root(self):
319
+ if self._pipeline is not None:
320
+ for node in self._pipeline.start_nodes:
321
+ if self.node_id == node.get('nodeId', None) and node.get('type', None) == 'root':
322
+ return True
323
+ return False
324
+
325
+ def _build_connection(self,
326
+ node,
327
+ source_port: PipelineNodeIO = None,
328
+ target_port: PipelineNodeIO = None,
329
+ filters: entities.Filters = None,
330
+ action: str = None) -> PipelineConnection:
331
+ """
332
+ Build connection between the current node and the target node use the given ports
333
+
334
+ :param PipelineNode node: the node to connect to it
335
+ :param PipelineNodeIO source_port: the source PipelineNodeIO input port
336
+ :param PipelineNodeIO target_port: the target PipelineNodeIO output port
337
+ :param entities.Filters filters: condition for the connection between the nodes
338
+ :param str action: the action that move the input when it happen
339
+ :return: the connection between the nodes
340
+ """
341
+ if source_port is None and self.outputs:
342
+ source_port = self.outputs[0]
343
+
344
+ if target_port is None and node.inputs:
345
+ target_port = node.inputs[0]
346
+
347
+ if node.is_root():
348
+ self._pipeline.set_start_node(self)
349
+
350
+ source_connection = PipelineConnectionPort(node_id=self.node_id, port_id=source_port.port_id)
351
+ target_connection = PipelineConnectionPort(node_id=node.node_id, port_id=target_port.port_id)
352
+ if action is None and source_port.actions is not None and source_port.actions != []:
353
+ action = source_port.actions[0]
354
+ connection = PipelineConnection(source=source_connection, target=target_connection, filters=filters,
355
+ action=action)
356
+ return connection
357
+
358
+ def connect(self,
359
+ node,
360
+ source_port: PipelineNodeIO = None,
361
+ target_port: PipelineNodeIO = None,
362
+ filters=None,
363
+ action: str = None):
364
+ """
365
+ Build connection between the current node and the target node use the given ports
366
+
367
+ :param PipelineNode node: the node to connect to it
368
+ :param PipelineNodeIO source_port: the source PipelineNodeIO input port
369
+ :param PipelineNodeIO target_port: the target PipelineNodeIO output port
370
+ :param entities.Filters filters: condition for the connection between the nodes
371
+ :param str action: the action that move the input when it happen
372
+ :return: the connected node
373
+ """
374
+ if self._pipeline is None:
375
+ raise Exception("must add the node to the pipeline first, e.g pipeline.nodes.add(node)")
376
+ connection = self._build_connection(node=node,
377
+ source_port=source_port,
378
+ target_port=target_port,
379
+ filters=filters,
380
+ action=action)
381
+ self._pipeline.connections.append(connection)
382
+ self._pipeline.nodes.add(node)
383
+ return node
384
+
385
+ def disconnect(self,
386
+ node,
387
+ source_port: PipelineNodeIO = None,
388
+ target_port: PipelineNodeIO = None) -> bool:
389
+ """
390
+ remove connection between the current node and the target node use the given ports
391
+
392
+ :param PipelineNode node: the node to connect to it
393
+ :param PipelineNodeIO source_port: the source PipelineNodeIO input port
394
+ :param PipelineNodeIO target_port: the target PipelineNodeIO output port
395
+ :return: true if success and false if not
396
+ """
397
+ if self._pipeline is None:
398
+ raise Exception("must add the node to the pipeline first, e.g pipeline.nodes.add(node)")
399
+ connection = self._build_connection(node=node,
400
+ source_port=source_port,
401
+ target_port=target_port,
402
+ filters=None)
403
+
404
+ current_connection = connection.to_json()
405
+ if 'condition' in current_connection:
406
+ current_connection = current_connection.pop('condition')
407
+
408
+ for connection_index in range(len(self._pipeline.connections)):
409
+ pipeline_connection = self._pipeline.connections[connection_index].to_json()
410
+ if 'condition' in pipeline_connection:
411
+ pipeline_connection = pipeline_connection.pop('condition')
412
+
413
+ if current_connection == pipeline_connection:
414
+ self._pipeline.connections.pop(connection_index)
415
+ return True
416
+ logger.warning('do not found a connection')
417
+ return False
418
+
419
+ def add_trigger(self,
420
+ trigger_type: entities.TriggerType = entities.TriggerType.EVENT,
421
+ filters=None,
422
+ resource: entities.TriggerResource = entities.TriggerResource.ITEM,
423
+ actions: entities.TriggerAction = entities.TriggerAction.CREATED,
424
+ execution_mode: entities.TriggerExecutionMode = entities.TriggerExecutionMode.ONCE,
425
+ cron: str = None,
426
+ ):
427
+ """
428
+ Create a Trigger. Can create two types: a cron trigger or an event trigger.
429
+ Inputs are different for each type
430
+
431
+ Inputs for all types:
432
+
433
+ :param trigger_type: can be cron or event. use enum dl.TriggerType for the full list
434
+
435
+ Inputs for event trigger:
436
+ :param filters: optional - Item/Annotation metadata filters, default = none
437
+ :param resource: optional - Dataset/Item/Annotation/ItemStatus, default = Item
438
+ :param actions: optional - Created/Updated/Deleted, default = create
439
+ :param execution_mode: how many time trigger should be activate. default is "Once". enum dl.TriggerExecutionMode
440
+
441
+ Inputs for cron trigger:
442
+ :param str cron: cron spec specifying when it should run. more information: https://en.wikipedia.org/wiki/Cron
443
+
444
+ :return: Trigger entity
445
+ """
446
+ if self._pipeline is None:
447
+ raise Exception("must add the node to the pipeline first, e.g pipeline.nodes.add(node)")
448
+
449
+ if not isinstance(actions, list):
450
+ actions = [actions]
451
+
452
+ if filters is None:
453
+ filters = {}
454
+ else:
455
+ filters = json.dumps(filters.prepare(query_only=True).get('filter', dict()))
456
+
457
+ if trigger_type == entities.TriggerType.EVENT:
458
+ spec = {
459
+ 'filter': filters,
460
+ 'resource': resource,
461
+ 'executionMode': execution_mode,
462
+ 'actions': actions
463
+ }
464
+ elif trigger_type == entities.TriggerType.CRON:
465
+ spec = {
466
+ 'cron': cron,
467
+ }
468
+ else:
469
+ raise ValueError('Unknown trigger type: "{}". Use dl.TriggerType for known types'.format(trigger_type))
470
+
471
+ trigger = {
472
+ "type": trigger_type,
473
+ "spec": spec
474
+ }
475
+
476
+ set_trigger = False
477
+ for pipe_node in self._pipeline.start_nodes:
478
+ if pipe_node['nodeId'] == self.node_id:
479
+ set_trigger = True
480
+ pipe_node['trigger'] = trigger
481
+
482
+ if not set_trigger:
483
+ self._pipeline.start_nodes.append(
484
+ {
485
+ "nodeId": self.node_id,
486
+ "type": "trigger",
487
+ 'trigger': trigger
488
+ }
489
+ )
490
+
491
+
492
+ class CodeNode(PipelineNode):
493
+ def __init__(self,
494
+ name: str,
495
+ project_id: str,
496
+ project_name: str,
497
+ method: Callable,
498
+ outputs: List[PipelineNodeIO] = None,
499
+ inputs: List[PipelineNodeIO] = None,
500
+ position: tuple = (1, 1),
501
+ ):
502
+ """
503
+ :param str name: node name
504
+ :param str project_id: project id
505
+ :param str project_name: project name
506
+ :param Callable method: function to deploy
507
+ :param list outputs: list of PipelineNodeIO outputs
508
+ :param list inputs: list of PipelineNodeIO inputs
509
+ :param tuple position: tuple of the node place
510
+ """
511
+ if inputs is None:
512
+ inputs = [self._default_io()]
513
+ if outputs is None:
514
+ outputs = [self._default_io()]
515
+
516
+ if method is None or not isinstance(method, Callable):
517
+ raise Exception('must provide a function as input')
518
+ else:
519
+ function_code = self._build_code_from_func(method)
520
+ function_name = method.__name__
521
+
522
+ super().__init__(name=name,
523
+ node_id=str(uuid.uuid4()),
524
+ outputs=outputs,
525
+ inputs=inputs,
526
+ metadata={},
527
+ node_type=PipelineNodeType.CODE,
528
+ namespace=PipelineNameSpace(function_name=function_name, project_name=project_name),
529
+ project_id=project_id,
530
+ position=position)
531
+
532
+ self.config = {
533
+ "package":
534
+ {
535
+ "code": function_code,
536
+ "name": function_name,
537
+ "type": "code"
538
+ }
539
+ }
540
+
541
+ def _build_code_from_func(self, func: Callable) -> str:
542
+ """
543
+ Build a code format from the given function
544
+
545
+ :param Callable func: function to deploy
546
+ :return: a string the display the code with the package format
547
+ """
548
+ with open(assets.paths.PARTIAL_MAIN_FILEPATH, 'r') as f:
549
+ main_string = f.read()
550
+ lines = inspect.getsourcelines(func)
551
+
552
+ tabs_diff = lines[0][0].count(' ') - 1
553
+ for line_index in range(len(lines[0])):
554
+ line_tabs = lines[0][line_index].count(' ') - tabs_diff
555
+ lines[0][line_index] = (' ' * line_tabs) + lines[0][line_index].strip() + '\n'
556
+
557
+ method_func_string = "".join(lines[0])
558
+
559
+ code = '{}\n{}\n @staticmethod\n{}'.format('', main_string,
560
+ method_func_string)
561
+ return code
562
+
563
+ @staticmethod
564
+ def from_json(_json: dict):
565
+ parent = PipelineNode.from_json(_json)
566
+ parent.__class__ = CodeNode
567
+ return parent
568
+
569
+
570
+ class TaskNode(PipelineNode):
571
+ def __init__(self,
572
+ name: str,
573
+ project_id: str,
574
+ dataset_id: str,
575
+ recipe_title: str,
576
+ recipe_id: str,
577
+ task_owner: str,
578
+ workload: List[entities.WorkloadUnit],
579
+ task_type: str = 'annotation',
580
+ position: tuple = (1, 1),
581
+ actions: list = None,
582
+ repeatable: bool = True,
583
+ batch_size=None,
584
+ max_batch_workload=None,
585
+ priority=entities.TaskPriority.MEDIUM,
586
+ due_date=None,
587
+ consensus_task_type=None,
588
+ consensus_percentage=None,
589
+ consensus_assignees=None,
590
+ groups=None
591
+ ):
592
+ """
593
+ :param str name: node name
594
+ :param str project_id: project id
595
+ :param str dataset_id: dataset id
596
+ :param str recipe_title: recipe title
597
+ :param str recipe_id: recipe id
598
+ :param str task_owner: email of task owner
599
+ :param List[WorkloadUnit] workload: list of WorkloadUnit
600
+ :param str task_type: 'annotation' or 'qa'
601
+ :param tuple position: tuple of the node place
602
+ :param list actions: list of task actions
603
+ :param bool repeatable: can repeat in the item
604
+ :param int groups: groups to assign the task to
605
+ :param int batch_size: Pulling batch size (items) . Restrictions - Min 3, max 100 - for create pulling task
606
+ :param int max_batch_workload: Max items in assignment . Restrictions - Min batchSize + 2 , max batchSize * 2 - for create pulling task
607
+ :param entities.TaskPriority priority: priority of the task options in entities.TaskPriority
608
+ :param float due_date: date by which the task should be finished; for example, due_date = datetime.datetime(day= 1, month= 1, year= 2029).timestamp()
609
+ :param entities.ConsensusTaskType consensus_task_type: consensus_task_type of the task options in entities.ConsensusTaskType
610
+ :param int consensus_percentage: percentage of items to be copied to multiple annotators (consensus items)
611
+ :param int consensus_assignees: the number of different annotators per item (number of copies per item)
612
+ """
613
+ if actions is None or actions == []:
614
+ actions = []
615
+ if task_type == 'qa':
616
+ if 'approve' not in actions:
617
+ actions.append('approve')
618
+ else:
619
+ if 'complete' not in actions:
620
+ actions.append('complete')
621
+ actions.append('discard')
622
+ else:
623
+ logger.warning(
624
+ "The 'actions' field was updated to override the system default actions for task (complete/approve, discard) if provided, due to a bug fix.")
625
+
626
+ inputs = [self._default_io()]
627
+
628
+ outputs = [self._default_io(actions=actions)]
629
+
630
+ if groups is not None:
631
+ if not isinstance(groups, list) or not all(isinstance(group, str) for group in groups):
632
+ raise ValueError('groups must be a list of strings')
633
+
634
+ super().__init__(name=name,
635
+ node_id=str(uuid.uuid4()),
636
+ outputs=outputs,
637
+ inputs=inputs,
638
+ metadata=dict(),
639
+ node_type=PipelineNodeType.TASK,
640
+ namespace=PipelineNameSpace(function_name="move_to_task",
641
+ project_name="DataloopTasks",
642
+ service_name="pipeline-utils"),
643
+ project_id=project_id,
644
+ position=position)
645
+
646
+ self.dataset_id = dataset_id
647
+ self.recipe_title = recipe_title
648
+ self.recipe_id = recipe_id
649
+ self.task_owner = task_owner
650
+ self.task_type = task_type
651
+ if not isinstance(workload, list):
652
+ workload = [workload]
653
+ self.workload = workload
654
+ self.repeatable = repeatable
655
+ if max_batch_workload:
656
+ self.max_batch_workload = max_batch_workload
657
+ if batch_size:
658
+ self.batch_size = batch_size
659
+ if consensus_task_type:
660
+ self.consensus_task_type = consensus_task_type
661
+ if consensus_percentage:
662
+ self.consensus_percentage = consensus_percentage
663
+ if consensus_assignees:
664
+ self.consensus_assignees = consensus_assignees
665
+ self.priority = priority
666
+ if due_date is None:
667
+ due_date = (datetime.datetime.now() + datetime.timedelta(days=7)).timestamp() * 1000
668
+ self.due_date = due_date
669
+ self.groups = groups
670
+
671
+ @property
672
+ def dataset_id(self):
673
+ return self.metadata['datasetId']
674
+
675
+ @dataset_id.setter
676
+ def dataset_id(self, dataset_id: str):
677
+ if not isinstance(dataset_id, str):
678
+ raise PlatformException('400', 'Param dataset_id must be of type string')
679
+ self.metadata['datasetId'] = dataset_id
680
+
681
+ @property
682
+ def groups(self):
683
+ return self.metadata.get('groups')
684
+
685
+ @groups.setter
686
+ def groups(self, groups: List[str]):
687
+ if groups is not None:
688
+ self.metadata['groups'] = groups
689
+
690
+ @property
691
+ def repeatable(self):
692
+ return self.metadata['repeatable']
693
+
694
+ @repeatable.setter
695
+ def repeatable(self, repeatable: bool):
696
+ if not isinstance(repeatable, bool):
697
+ raise PlatformException('400', 'Param repeatable must be of type bool')
698
+ self.metadata['repeatable'] = repeatable
699
+
700
+ @property
701
+ def recipe_title(self):
702
+ return self.metadata['recipeTitle']
703
+
704
+ @recipe_title.setter
705
+ def recipe_title(self, recipe_title: str):
706
+ if not isinstance(recipe_title, str):
707
+ raise PlatformException('400', 'Param recipe_title must be of type string')
708
+ self.metadata['recipeTitle'] = recipe_title
709
+
710
+ @property
711
+ def recipe_id(self):
712
+ return self.metadata['recipeId']
713
+
714
+ @recipe_id.setter
715
+ def recipe_id(self, recipe_id: str):
716
+ if not isinstance(recipe_id, str):
717
+ raise PlatformException('400', 'Param recipe_id must be of type string')
718
+ self.metadata['recipeId'] = recipe_id
719
+
720
+ @property
721
+ def task_owner(self):
722
+ return self.metadata['taskOwner']
723
+
724
+ @task_owner.setter
725
+ def task_owner(self, task_owner: str):
726
+ if not isinstance(task_owner, str):
727
+ raise PlatformException('400', 'Param task_owner must be of type string')
728
+ self.metadata['taskOwner'] = task_owner
729
+
730
+ @property
731
+ def task_type(self):
732
+ return self.metadata['taskType']
733
+
734
+ @task_type.setter
735
+ def task_type(self, task_type: str):
736
+ if not isinstance(task_type, str):
737
+ raise PlatformException('400', 'Param task_type must be of type string')
738
+ self.metadata['taskType'] = task_type
739
+
740
+ @property
741
+ def workload(self):
742
+ return self.metadata['workload']
743
+
744
+ @workload.setter
745
+ def workload(self, workload: list):
746
+ if not isinstance(workload, list):
747
+ workload = [workload]
748
+ self.metadata['workload'] = [val.to_json() for val in workload]
749
+
750
+ @property
751
+ def batch_size(self):
752
+ return self.metadata['batchSize']
753
+
754
+ @batch_size.setter
755
+ def batch_size(self, batch_size: int):
756
+ if not isinstance(batch_size, int):
757
+ raise PlatformException('400', 'Param batch_size must be of type int')
758
+ self.metadata['batchSize'] = batch_size
759
+
760
+ @property
761
+ def max_batch_workload(self):
762
+ return self.metadata['maxBatchWorkload']
763
+
764
+ @max_batch_workload.setter
765
+ def max_batch_workload(self, max_batch_workload: int):
766
+ if not isinstance(max_batch_workload, int):
767
+ raise PlatformException('400', 'Param max_batch_workload must be of type int')
768
+ self.metadata['maxBatchWorkload'] = max_batch_workload
769
+
770
+ @property
771
+ def consensus_task_type(self):
772
+ return self.metadata['consensusTaskType']
773
+
774
+ @consensus_task_type.setter
775
+ def consensus_task_type(self, consensus_task_type: entities.ConsensusTaskType):
776
+ if not isinstance(consensus_task_type, str) and not isinstance(consensus_task_type, entities.ConsensusTaskType):
777
+ raise PlatformException('400', 'Param consensus_task_type must be of type entities.ConsensusTaskType')
778
+ self.metadata['consensusTaskType'] = consensus_task_type
779
+
780
+ @property
781
+ def consensus_percentage(self):
782
+ return self.metadata['consensusPercentage']
783
+
784
+ @consensus_percentage.setter
785
+ def consensus_percentage(self, consensus_percentage: int):
786
+ if not isinstance(consensus_percentage, int):
787
+ raise PlatformException('400', 'Param consensus_percentage must be of type int')
788
+ self.metadata['consensusPercentage'] = consensus_percentage
789
+
790
+ @property
791
+ def consensus_assignees(self):
792
+ return self.metadata['consensusAssignees']
793
+
794
+ @consensus_assignees.setter
795
+ def consensus_assignees(self, consensus_assignees: int):
796
+ if not isinstance(consensus_assignees, int):
797
+ raise PlatformException('400', 'Param consensus_assignees must be of type int')
798
+ self.metadata['consensusAssignees'] = consensus_assignees
799
+
800
+ @property
801
+ def priority(self):
802
+ return self.metadata['priority']
803
+
804
+ @priority.setter
805
+ def priority(self, priority: entities.TaskPriority):
806
+ if not isinstance(priority, int) and not isinstance(priority, entities.TaskPriority):
807
+ raise PlatformException('400', 'Param priority must be of type entities.TaskPriority')
808
+ self.metadata['priority'] = priority
809
+
810
+ @property
811
+ def due_date(self):
812
+ return self.metadata['dueDate']
813
+
814
+ @due_date.setter
815
+ def due_date(self, due_date: float):
816
+ if not isinstance(due_date, float) and not isinstance(due_date, int):
817
+ raise PlatformException('400', 'Param due_date must be of type float or int')
818
+ self.metadata['dueDate'] = due_date
819
+
820
+ @staticmethod
821
+ def from_json(_json: dict):
822
+ parent = PipelineNode.from_json(_json)
823
+ parent.__class__ = TaskNode
824
+ return parent
825
+
826
+
827
+ class FunctionNode(PipelineNode):
828
+ def __init__(self,
829
+ name: str,
830
+ service: entities.Service,
831
+ function_name,
832
+ position: tuple = (1, 1),
833
+ project_id=None,
834
+ project_name=None
835
+ ):
836
+ """
837
+ :param str name: node name
838
+ :param entities.Service service: service to deploy
839
+ :param str function_name: function name
840
+ :param tuple position: tuple of the node place
841
+ """
842
+ self.service = service
843
+
844
+ if project_id is None:
845
+ project_id = service.project_id
846
+ if project_id != service.project_id:
847
+ logger.warning("the project id that provide different from the service project id")
848
+
849
+ if project_name is None:
850
+ try:
851
+ project = repositories.Projects(client_api=self.service._client_api).get(project_id=project_id,
852
+ log_error=False)
853
+ project_name = project.name
854
+ except:
855
+ logger.warning(
856
+ 'Service project not found using DataloopTasks project.'
857
+ ' If this is incorrect please provide project_name param.')
858
+ project_name = 'DataloopTasks'
859
+ inputs = []
860
+ outputs = []
861
+ package = self.service.package
862
+ modules = []
863
+ if isinstance(package, entities.Package):
864
+ modules = package.modules
865
+ elif isinstance(package, entities.Dpk):
866
+ modules = package.components.modules
867
+ for model in modules:
868
+ if model.name == self.service.module_name:
869
+ for func in model.functions:
870
+ if func.name == function_name:
871
+ inputs = self._convert_from_function_io_to_pipeline_io(func.inputs)
872
+ outputs = self._convert_from_function_io_to_pipeline_io(func.outputs)
873
+
874
+ namespace = PipelineNameSpace(
875
+ function_name=function_name,
876
+ service_name=self.service.name,
877
+ module_name=self.service.module_name,
878
+ package_name=self.service.package.name,
879
+ project_name=project_name
880
+ )
881
+ super().__init__(name=name,
882
+ node_id=str(uuid.uuid4()),
883
+ outputs=outputs,
884
+ inputs=inputs,
885
+ metadata={},
886
+ node_type=PipelineNodeType.FUNCTION,
887
+ namespace=namespace,
888
+ project_id=service.project_id,
889
+ position=position)
890
+
891
+ def _convert_from_function_io_to_pipeline_io(self, function_io: List[entities.FunctionIO]) -> List[PipelineNodeIO]:
892
+ """
893
+ Get a list of FunctionIO and convert them to PipelineIO
894
+ :param List[entities.FunctionIO] function_io: list of functionIO
895
+ :return: list of PipelineIO
896
+ """
897
+ pipeline_io = []
898
+ for single_input in function_io:
899
+ pipeline_io.append(
900
+ PipelineNodeIO(port_id=str(uuid.uuid4()),
901
+ input_type=single_input.type,
902
+ name=single_input.name,
903
+ color=None,
904
+ display_name=single_input.name,
905
+ default_value=single_input.value,
906
+ actions=single_input.actions if single_input.actions is not None else []))
907
+ return pipeline_io
908
+
909
+ @staticmethod
910
+ def from_json(_json: dict):
911
+ parent = PipelineNode.from_json(_json)
912
+ parent.__class__ = FunctionNode
913
+ return parent
914
+
915
+
916
+ class DatasetNode(PipelineNode):
917
+ def __init__(self,
918
+ name: str,
919
+ project_id: str,
920
+ dataset_id: str,
921
+ dataset_folder: str = None,
922
+ load_existing_data: bool = False,
923
+ data_filters: entities.Filters = None,
924
+ position: tuple = (1, 1)):
925
+ """
926
+ :param str name: node name
927
+ :param str project_id: project id
928
+ :param str dataset_id: dataset id
929
+ :param str dataset_folder: folder in dataset to work in it
930
+ :param bool load_existing_data: optional - enable to automatically load existing data into the
931
+ pipeline (executions) upon activation, based on the defined dataset,
932
+ folder, and data_filters.
933
+ :param entities.Filters data_filters: optional - filters entity or a dictionary containing filters parameters.
934
+ Use to filter the data items to be loaded when load_existing_data
935
+ is enabled.
936
+ :param tuple position: tuple of the node place
937
+ """
938
+ inputs = [self._default_io()]
939
+ outputs = [self._default_io()]
940
+ super().__init__(name=name,
941
+ node_id=str(uuid.uuid4()),
942
+ outputs=outputs,
943
+ inputs=inputs,
944
+ metadata={},
945
+ node_type=PipelineNodeType.STORAGE,
946
+ namespace=PipelineNameSpace(function_name="dataset_handler",
947
+ project_name="DataloopTasks",
948
+ service_name="pipeline-utils"),
949
+ project_id=project_id,
950
+ position=position)
951
+ self.dataset_id = dataset_id
952
+ self.dataset_folder = dataset_folder
953
+ self.load_existing_data = load_existing_data
954
+ self.data_filters = data_filters
955
+
956
+ @property
957
+ def dataset_id(self):
958
+ return self.metadata['datasetId']
959
+
960
+ @dataset_id.setter
961
+ def dataset_id(self, dataset_id: str):
962
+ self.metadata['datasetId'] = dataset_id
963
+
964
+ @property
965
+ def dataset_folder(self):
966
+ return self.metadata.get('dir', None)
967
+
968
+ @dataset_folder.setter
969
+ def dataset_folder(self, dataset_folder: str):
970
+ if dataset_folder is not None:
971
+ if not dataset_folder.startswith("/"):
972
+ dataset_folder = '/' + dataset_folder
973
+ self.metadata['dir'] = dataset_folder
974
+
975
+ @property
976
+ def load_existing_data(self):
977
+ return self.metadata.get('triggerToPipeline', {}).get('active', False)
978
+
979
+ @load_existing_data.setter
980
+ def load_existing_data(self, load_existing_data: bool):
981
+ if load_existing_data:
982
+ self.metadata.setdefault('triggerToPipeline', {})['active'] = True
983
+ else:
984
+ self.metadata.pop('triggerToPipeline', None)
985
+
986
+ @property
987
+ def data_filters(self):
988
+ data_filters = self.metadata.get('triggerToPipeline', {}).get('filter', None)
989
+ if data_filters:
990
+ data_filters = entities.Filters(custom_filter=json.loads(data_filters))
991
+ return data_filters
992
+
993
+ @data_filters.setter
994
+ def data_filters(self, data_filters: entities.Filters):
995
+ if data_filters is None:
996
+ filters = None
997
+ else:
998
+ filters = json.dumps(data_filters.prepare(query_only=True).get('filter'))
999
+ self.metadata.setdefault('triggerToPipeline', {})['filter'] = filters
1000
+
1001
+ @staticmethod
1002
+ def from_json(_json: dict):
1003
+ parent = PipelineNode.from_json(_json)
1004
+ parent.__class__ = DatasetNode
1005
+ return parent