dtlpy 1.113.10__py3-none-any.whl → 1.114.13__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 (243) hide show
  1. dtlpy/__init__.py +488 -488
  2. dtlpy/__version__.py +1 -1
  3. dtlpy/assets/__init__.py +26 -26
  4. dtlpy/assets/__pycache__/__init__.cpython-38.pyc +0 -0
  5. dtlpy/assets/code_server/config.yaml +2 -2
  6. dtlpy/assets/code_server/installation.sh +24 -24
  7. dtlpy/assets/code_server/launch.json +13 -13
  8. dtlpy/assets/code_server/settings.json +2 -2
  9. dtlpy/assets/main.py +53 -53
  10. dtlpy/assets/main_partial.py +18 -18
  11. dtlpy/assets/mock.json +11 -11
  12. dtlpy/assets/model_adapter.py +83 -83
  13. dtlpy/assets/package.json +61 -61
  14. dtlpy/assets/package_catalog.json +29 -29
  15. dtlpy/assets/package_gitignore +307 -307
  16. dtlpy/assets/service_runners/__init__.py +33 -33
  17. dtlpy/assets/service_runners/converter.py +96 -96
  18. dtlpy/assets/service_runners/multi_method.py +49 -49
  19. dtlpy/assets/service_runners/multi_method_annotation.py +54 -54
  20. dtlpy/assets/service_runners/multi_method_dataset.py +55 -55
  21. dtlpy/assets/service_runners/multi_method_item.py +52 -52
  22. dtlpy/assets/service_runners/multi_method_json.py +52 -52
  23. dtlpy/assets/service_runners/single_method.py +37 -37
  24. dtlpy/assets/service_runners/single_method_annotation.py +43 -43
  25. dtlpy/assets/service_runners/single_method_dataset.py +43 -43
  26. dtlpy/assets/service_runners/single_method_item.py +41 -41
  27. dtlpy/assets/service_runners/single_method_json.py +42 -42
  28. dtlpy/assets/service_runners/single_method_multi_input.py +45 -45
  29. dtlpy/assets/voc_annotation_template.xml +23 -23
  30. dtlpy/caches/base_cache.py +32 -32
  31. dtlpy/caches/cache.py +473 -473
  32. dtlpy/caches/dl_cache.py +201 -201
  33. dtlpy/caches/filesystem_cache.py +89 -89
  34. dtlpy/caches/redis_cache.py +84 -84
  35. dtlpy/dlp/__init__.py +20 -20
  36. dtlpy/dlp/cli_utilities.py +367 -367
  37. dtlpy/dlp/command_executor.py +764 -764
  38. dtlpy/dlp/dlp +1 -1
  39. dtlpy/dlp/dlp.bat +1 -1
  40. dtlpy/dlp/dlp.py +128 -128
  41. dtlpy/dlp/parser.py +651 -651
  42. dtlpy/entities/__init__.py +83 -83
  43. dtlpy/entities/analytic.py +311 -311
  44. dtlpy/entities/annotation.py +1879 -1879
  45. dtlpy/entities/annotation_collection.py +699 -699
  46. dtlpy/entities/annotation_definitions/__init__.py +20 -20
  47. dtlpy/entities/annotation_definitions/base_annotation_definition.py +100 -100
  48. dtlpy/entities/annotation_definitions/box.py +195 -195
  49. dtlpy/entities/annotation_definitions/classification.py +67 -67
  50. dtlpy/entities/annotation_definitions/comparison.py +72 -72
  51. dtlpy/entities/annotation_definitions/cube.py +204 -204
  52. dtlpy/entities/annotation_definitions/cube_3d.py +149 -149
  53. dtlpy/entities/annotation_definitions/description.py +32 -32
  54. dtlpy/entities/annotation_definitions/ellipse.py +124 -124
  55. dtlpy/entities/annotation_definitions/free_text.py +62 -62
  56. dtlpy/entities/annotation_definitions/gis.py +69 -69
  57. dtlpy/entities/annotation_definitions/note.py +139 -139
  58. dtlpy/entities/annotation_definitions/point.py +117 -117
  59. dtlpy/entities/annotation_definitions/polygon.py +182 -182
  60. dtlpy/entities/annotation_definitions/polyline.py +111 -111
  61. dtlpy/entities/annotation_definitions/pose.py +92 -92
  62. dtlpy/entities/annotation_definitions/ref_image.py +86 -86
  63. dtlpy/entities/annotation_definitions/segmentation.py +240 -240
  64. dtlpy/entities/annotation_definitions/subtitle.py +34 -34
  65. dtlpy/entities/annotation_definitions/text.py +85 -85
  66. dtlpy/entities/annotation_definitions/undefined_annotation.py +74 -74
  67. dtlpy/entities/app.py +220 -220
  68. dtlpy/entities/app_module.py +107 -107
  69. dtlpy/entities/artifact.py +174 -174
  70. dtlpy/entities/assignment.py +399 -399
  71. dtlpy/entities/base_entity.py +214 -214
  72. dtlpy/entities/bot.py +113 -113
  73. dtlpy/entities/codebase.py +296 -296
  74. dtlpy/entities/collection.py +38 -38
  75. dtlpy/entities/command.py +169 -169
  76. dtlpy/entities/compute.py +442 -442
  77. dtlpy/entities/dataset.py +1285 -1285
  78. dtlpy/entities/directory_tree.py +44 -44
  79. dtlpy/entities/dpk.py +470 -470
  80. dtlpy/entities/driver.py +222 -222
  81. dtlpy/entities/execution.py +397 -397
  82. dtlpy/entities/feature.py +124 -124
  83. dtlpy/entities/feature_set.py +145 -145
  84. dtlpy/entities/filters.py +641 -641
  85. dtlpy/entities/gis_item.py +107 -107
  86. dtlpy/entities/integration.py +184 -184
  87. dtlpy/entities/item.py +953 -953
  88. dtlpy/entities/label.py +123 -123
  89. dtlpy/entities/links.py +85 -85
  90. dtlpy/entities/message.py +175 -175
  91. dtlpy/entities/model.py +694 -691
  92. dtlpy/entities/node.py +1005 -1005
  93. dtlpy/entities/ontology.py +803 -803
  94. dtlpy/entities/organization.py +287 -287
  95. dtlpy/entities/package.py +657 -657
  96. dtlpy/entities/package_defaults.py +5 -5
  97. dtlpy/entities/package_function.py +185 -185
  98. dtlpy/entities/package_module.py +113 -113
  99. dtlpy/entities/package_slot.py +118 -118
  100. dtlpy/entities/paged_entities.py +290 -267
  101. dtlpy/entities/pipeline.py +593 -593
  102. dtlpy/entities/pipeline_execution.py +279 -279
  103. dtlpy/entities/project.py +394 -394
  104. dtlpy/entities/prompt_item.py +499 -499
  105. dtlpy/entities/recipe.py +301 -301
  106. dtlpy/entities/reflect_dict.py +102 -102
  107. dtlpy/entities/resource_execution.py +138 -138
  108. dtlpy/entities/service.py +958 -958
  109. dtlpy/entities/service_driver.py +117 -117
  110. dtlpy/entities/setting.py +294 -294
  111. dtlpy/entities/task.py +491 -491
  112. dtlpy/entities/time_series.py +143 -143
  113. dtlpy/entities/trigger.py +426 -426
  114. dtlpy/entities/user.py +118 -118
  115. dtlpy/entities/webhook.py +124 -124
  116. dtlpy/examples/__init__.py +19 -19
  117. dtlpy/examples/add_labels.py +135 -135
  118. dtlpy/examples/add_metadata_to_item.py +21 -21
  119. dtlpy/examples/annotate_items_using_model.py +65 -65
  120. dtlpy/examples/annotate_video_using_model_and_tracker.py +75 -75
  121. dtlpy/examples/annotations_convert_to_voc.py +9 -9
  122. dtlpy/examples/annotations_convert_to_yolo.py +9 -9
  123. dtlpy/examples/convert_annotation_types.py +51 -51
  124. dtlpy/examples/converter.py +143 -143
  125. dtlpy/examples/copy_annotations.py +22 -22
  126. dtlpy/examples/copy_folder.py +31 -31
  127. dtlpy/examples/create_annotations.py +51 -51
  128. dtlpy/examples/create_video_annotations.py +83 -83
  129. dtlpy/examples/delete_annotations.py +26 -26
  130. dtlpy/examples/filters.py +113 -113
  131. dtlpy/examples/move_item.py +23 -23
  132. dtlpy/examples/play_video_annotation.py +13 -13
  133. dtlpy/examples/show_item_and_mask.py +53 -53
  134. dtlpy/examples/triggers.py +49 -49
  135. dtlpy/examples/upload_batch_of_items.py +20 -20
  136. dtlpy/examples/upload_items_and_custom_format_annotations.py +55 -55
  137. dtlpy/examples/upload_items_with_modalities.py +43 -43
  138. dtlpy/examples/upload_segmentation_annotations_from_mask_image.py +44 -44
  139. dtlpy/examples/upload_yolo_format_annotations.py +70 -70
  140. dtlpy/exceptions.py +125 -125
  141. dtlpy/miscellaneous/__init__.py +20 -20
  142. dtlpy/miscellaneous/dict_differ.py +95 -95
  143. dtlpy/miscellaneous/git_utils.py +217 -217
  144. dtlpy/miscellaneous/json_utils.py +14 -14
  145. dtlpy/miscellaneous/list_print.py +105 -105
  146. dtlpy/miscellaneous/zipping.py +130 -130
  147. dtlpy/ml/__init__.py +20 -20
  148. dtlpy/ml/base_feature_extractor_adapter.py +27 -27
  149. dtlpy/ml/base_model_adapter.py +945 -940
  150. dtlpy/ml/metrics.py +461 -461
  151. dtlpy/ml/predictions_utils.py +274 -274
  152. dtlpy/ml/summary_writer.py +57 -57
  153. dtlpy/ml/train_utils.py +60 -60
  154. dtlpy/new_instance.py +252 -252
  155. dtlpy/repositories/__init__.py +56 -56
  156. dtlpy/repositories/analytics.py +85 -85
  157. dtlpy/repositories/annotations.py +916 -916
  158. dtlpy/repositories/apps.py +383 -383
  159. dtlpy/repositories/artifacts.py +452 -452
  160. dtlpy/repositories/assignments.py +599 -599
  161. dtlpy/repositories/bots.py +213 -213
  162. dtlpy/repositories/codebases.py +559 -559
  163. dtlpy/repositories/collections.py +332 -348
  164. dtlpy/repositories/commands.py +158 -158
  165. dtlpy/repositories/compositions.py +61 -61
  166. dtlpy/repositories/computes.py +434 -406
  167. dtlpy/repositories/datasets.py +1291 -1291
  168. dtlpy/repositories/downloader.py +895 -895
  169. dtlpy/repositories/dpks.py +433 -433
  170. dtlpy/repositories/drivers.py +266 -266
  171. dtlpy/repositories/executions.py +817 -817
  172. dtlpy/repositories/feature_sets.py +226 -226
  173. dtlpy/repositories/features.py +238 -238
  174. dtlpy/repositories/integrations.py +484 -484
  175. dtlpy/repositories/items.py +909 -915
  176. dtlpy/repositories/messages.py +94 -94
  177. dtlpy/repositories/models.py +877 -867
  178. dtlpy/repositories/nodes.py +80 -80
  179. dtlpy/repositories/ontologies.py +511 -511
  180. dtlpy/repositories/organizations.py +525 -525
  181. dtlpy/repositories/packages.py +1941 -1941
  182. dtlpy/repositories/pipeline_executions.py +448 -448
  183. dtlpy/repositories/pipelines.py +642 -642
  184. dtlpy/repositories/projects.py +539 -539
  185. dtlpy/repositories/recipes.py +399 -399
  186. dtlpy/repositories/resource_executions.py +137 -137
  187. dtlpy/repositories/schema.py +120 -120
  188. dtlpy/repositories/service_drivers.py +213 -213
  189. dtlpy/repositories/services.py +1704 -1704
  190. dtlpy/repositories/settings.py +339 -339
  191. dtlpy/repositories/tasks.py +1124 -1124
  192. dtlpy/repositories/times_series.py +278 -278
  193. dtlpy/repositories/triggers.py +536 -536
  194. dtlpy/repositories/upload_element.py +257 -257
  195. dtlpy/repositories/uploader.py +651 -651
  196. dtlpy/repositories/webhooks.py +249 -249
  197. dtlpy/services/__init__.py +22 -22
  198. dtlpy/services/aihttp_retry.py +131 -131
  199. dtlpy/services/api_client.py +1782 -1782
  200. dtlpy/services/api_reference.py +40 -40
  201. dtlpy/services/async_utils.py +133 -133
  202. dtlpy/services/calls_counter.py +44 -44
  203. dtlpy/services/check_sdk.py +68 -68
  204. dtlpy/services/cookie.py +115 -115
  205. dtlpy/services/create_logger.py +156 -156
  206. dtlpy/services/events.py +84 -84
  207. dtlpy/services/logins.py +235 -235
  208. dtlpy/services/reporter.py +256 -256
  209. dtlpy/services/service_defaults.py +91 -91
  210. dtlpy/utilities/__init__.py +20 -20
  211. dtlpy/utilities/annotations/__init__.py +16 -16
  212. dtlpy/utilities/annotations/annotation_converters.py +269 -269
  213. dtlpy/utilities/base_package_runner.py +264 -264
  214. dtlpy/utilities/converter.py +1650 -1650
  215. dtlpy/utilities/dataset_generators/__init__.py +1 -1
  216. dtlpy/utilities/dataset_generators/dataset_generator.py +670 -670
  217. dtlpy/utilities/dataset_generators/dataset_generator_tensorflow.py +23 -23
  218. dtlpy/utilities/dataset_generators/dataset_generator_torch.py +21 -21
  219. dtlpy/utilities/local_development/__init__.py +1 -1
  220. dtlpy/utilities/local_development/local_session.py +179 -179
  221. dtlpy/utilities/reports/__init__.py +2 -2
  222. dtlpy/utilities/reports/figures.py +343 -343
  223. dtlpy/utilities/reports/report.py +71 -71
  224. dtlpy/utilities/videos/__init__.py +17 -17
  225. dtlpy/utilities/videos/video_player.py +598 -598
  226. dtlpy/utilities/videos/videos.py +470 -470
  227. {dtlpy-1.113.10.data → dtlpy-1.114.13.data}/scripts/dlp +1 -1
  228. dtlpy-1.114.13.data/scripts/dlp.bat +2 -0
  229. {dtlpy-1.113.10.data → dtlpy-1.114.13.data}/scripts/dlp.py +128 -128
  230. {dtlpy-1.113.10.dist-info → dtlpy-1.114.13.dist-info}/LICENSE +200 -200
  231. {dtlpy-1.113.10.dist-info → dtlpy-1.114.13.dist-info}/METADATA +172 -172
  232. dtlpy-1.114.13.dist-info/RECORD +240 -0
  233. {dtlpy-1.113.10.dist-info → dtlpy-1.114.13.dist-info}/WHEEL +1 -1
  234. tests/features/environment.py +551 -550
  235. dtlpy-1.113.10.data/scripts/dlp.bat +0 -2
  236. dtlpy-1.113.10.dist-info/RECORD +0 -244
  237. tests/assets/__init__.py +0 -0
  238. tests/assets/models_flow/__init__.py +0 -0
  239. tests/assets/models_flow/failedmain.py +0 -52
  240. tests/assets/models_flow/main.py +0 -62
  241. tests/assets/models_flow/main_model.py +0 -54
  242. {dtlpy-1.113.10.dist-info → dtlpy-1.114.13.dist-info}/entry_points.txt +0 -0
  243. {dtlpy-1.113.10.dist-info → dtlpy-1.114.13.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