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.
- dtlpy/__init__.py +491 -491
- dtlpy/__version__.py +1 -1
- dtlpy/assets/__init__.py +26 -26
- dtlpy/assets/code_server/config.yaml +2 -2
- dtlpy/assets/code_server/installation.sh +24 -24
- dtlpy/assets/code_server/launch.json +13 -13
- dtlpy/assets/code_server/settings.json +2 -2
- dtlpy/assets/main.py +53 -53
- dtlpy/assets/main_partial.py +18 -18
- dtlpy/assets/mock.json +11 -11
- dtlpy/assets/model_adapter.py +83 -83
- dtlpy/assets/package.json +61 -61
- dtlpy/assets/package_catalog.json +29 -29
- dtlpy/assets/package_gitignore +307 -307
- dtlpy/assets/service_runners/__init__.py +33 -33
- dtlpy/assets/service_runners/converter.py +96 -96
- dtlpy/assets/service_runners/multi_method.py +49 -49
- dtlpy/assets/service_runners/multi_method_annotation.py +54 -54
- dtlpy/assets/service_runners/multi_method_dataset.py +55 -55
- dtlpy/assets/service_runners/multi_method_item.py +52 -52
- dtlpy/assets/service_runners/multi_method_json.py +52 -52
- dtlpy/assets/service_runners/single_method.py +37 -37
- dtlpy/assets/service_runners/single_method_annotation.py +43 -43
- dtlpy/assets/service_runners/single_method_dataset.py +43 -43
- dtlpy/assets/service_runners/single_method_item.py +41 -41
- dtlpy/assets/service_runners/single_method_json.py +42 -42
- dtlpy/assets/service_runners/single_method_multi_input.py +45 -45
- dtlpy/assets/voc_annotation_template.xml +23 -23
- dtlpy/caches/base_cache.py +32 -32
- dtlpy/caches/cache.py +473 -473
- dtlpy/caches/dl_cache.py +201 -201
- dtlpy/caches/filesystem_cache.py +89 -89
- dtlpy/caches/redis_cache.py +84 -84
- dtlpy/dlp/__init__.py +20 -20
- dtlpy/dlp/cli_utilities.py +367 -367
- dtlpy/dlp/command_executor.py +764 -764
- dtlpy/dlp/dlp +1 -1
- dtlpy/dlp/dlp.bat +1 -1
- dtlpy/dlp/dlp.py +128 -128
- dtlpy/dlp/parser.py +651 -651
- dtlpy/entities/__init__.py +83 -83
- dtlpy/entities/analytic.py +347 -347
- dtlpy/entities/annotation.py +1879 -1879
- dtlpy/entities/annotation_collection.py +699 -699
- dtlpy/entities/annotation_definitions/__init__.py +20 -20
- dtlpy/entities/annotation_definitions/base_annotation_definition.py +100 -100
- dtlpy/entities/annotation_definitions/box.py +195 -195
- dtlpy/entities/annotation_definitions/classification.py +67 -67
- dtlpy/entities/annotation_definitions/comparison.py +72 -72
- dtlpy/entities/annotation_definitions/cube.py +204 -204
- dtlpy/entities/annotation_definitions/cube_3d.py +149 -149
- dtlpy/entities/annotation_definitions/description.py +32 -32
- dtlpy/entities/annotation_definitions/ellipse.py +124 -124
- dtlpy/entities/annotation_definitions/free_text.py +62 -62
- dtlpy/entities/annotation_definitions/gis.py +69 -69
- dtlpy/entities/annotation_definitions/note.py +139 -139
- dtlpy/entities/annotation_definitions/point.py +117 -117
- dtlpy/entities/annotation_definitions/polygon.py +182 -182
- dtlpy/entities/annotation_definitions/polyline.py +111 -111
- dtlpy/entities/annotation_definitions/pose.py +92 -92
- dtlpy/entities/annotation_definitions/ref_image.py +86 -86
- dtlpy/entities/annotation_definitions/segmentation.py +240 -240
- dtlpy/entities/annotation_definitions/subtitle.py +34 -34
- dtlpy/entities/annotation_definitions/text.py +85 -85
- dtlpy/entities/annotation_definitions/undefined_annotation.py +74 -74
- dtlpy/entities/app.py +220 -220
- dtlpy/entities/app_module.py +107 -107
- dtlpy/entities/artifact.py +174 -174
- dtlpy/entities/assignment.py +399 -399
- dtlpy/entities/base_entity.py +214 -214
- dtlpy/entities/bot.py +113 -113
- dtlpy/entities/codebase.py +292 -292
- dtlpy/entities/collection.py +38 -38
- dtlpy/entities/command.py +169 -169
- dtlpy/entities/compute.py +449 -449
- dtlpy/entities/dataset.py +1299 -1299
- dtlpy/entities/directory_tree.py +44 -44
- dtlpy/entities/dpk.py +470 -470
- dtlpy/entities/driver.py +235 -235
- dtlpy/entities/execution.py +397 -397
- dtlpy/entities/feature.py +124 -124
- dtlpy/entities/feature_set.py +145 -145
- dtlpy/entities/filters.py +798 -798
- dtlpy/entities/gis_item.py +107 -107
- dtlpy/entities/integration.py +184 -184
- dtlpy/entities/item.py +959 -959
- dtlpy/entities/label.py +123 -123
- dtlpy/entities/links.py +85 -85
- dtlpy/entities/message.py +175 -175
- dtlpy/entities/model.py +684 -684
- dtlpy/entities/node.py +1005 -1005
- dtlpy/entities/ontology.py +810 -803
- dtlpy/entities/organization.py +287 -287
- dtlpy/entities/package.py +657 -657
- dtlpy/entities/package_defaults.py +5 -5
- dtlpy/entities/package_function.py +185 -185
- dtlpy/entities/package_module.py +113 -113
- dtlpy/entities/package_slot.py +118 -118
- dtlpy/entities/paged_entities.py +299 -299
- dtlpy/entities/pipeline.py +624 -624
- dtlpy/entities/pipeline_execution.py +279 -279
- dtlpy/entities/project.py +394 -394
- dtlpy/entities/prompt_item.py +505 -505
- dtlpy/entities/recipe.py +301 -301
- dtlpy/entities/reflect_dict.py +102 -102
- dtlpy/entities/resource_execution.py +138 -138
- dtlpy/entities/service.py +963 -963
- dtlpy/entities/service_driver.py +117 -117
- dtlpy/entities/setting.py +294 -294
- dtlpy/entities/task.py +495 -495
- dtlpy/entities/time_series.py +143 -143
- dtlpy/entities/trigger.py +426 -426
- dtlpy/entities/user.py +118 -118
- dtlpy/entities/webhook.py +124 -124
- dtlpy/examples/__init__.py +19 -19
- dtlpy/examples/add_labels.py +135 -135
- dtlpy/examples/add_metadata_to_item.py +21 -21
- dtlpy/examples/annotate_items_using_model.py +65 -65
- dtlpy/examples/annotate_video_using_model_and_tracker.py +75 -75
- dtlpy/examples/annotations_convert_to_voc.py +9 -9
- dtlpy/examples/annotations_convert_to_yolo.py +9 -9
- dtlpy/examples/convert_annotation_types.py +51 -51
- dtlpy/examples/converter.py +143 -143
- dtlpy/examples/copy_annotations.py +22 -22
- dtlpy/examples/copy_folder.py +31 -31
- dtlpy/examples/create_annotations.py +51 -51
- dtlpy/examples/create_video_annotations.py +83 -83
- dtlpy/examples/delete_annotations.py +26 -26
- dtlpy/examples/filters.py +113 -113
- dtlpy/examples/move_item.py +23 -23
- dtlpy/examples/play_video_annotation.py +13 -13
- dtlpy/examples/show_item_and_mask.py +53 -53
- dtlpy/examples/triggers.py +49 -49
- dtlpy/examples/upload_batch_of_items.py +20 -20
- dtlpy/examples/upload_items_and_custom_format_annotations.py +55 -55
- dtlpy/examples/upload_items_with_modalities.py +43 -43
- dtlpy/examples/upload_segmentation_annotations_from_mask_image.py +44 -44
- dtlpy/examples/upload_yolo_format_annotations.py +70 -70
- dtlpy/exceptions.py +125 -125
- dtlpy/miscellaneous/__init__.py +20 -20
- dtlpy/miscellaneous/dict_differ.py +95 -95
- dtlpy/miscellaneous/git_utils.py +217 -217
- dtlpy/miscellaneous/json_utils.py +14 -14
- dtlpy/miscellaneous/list_print.py +105 -105
- dtlpy/miscellaneous/zipping.py +130 -130
- dtlpy/ml/__init__.py +20 -20
- dtlpy/ml/base_feature_extractor_adapter.py +27 -27
- dtlpy/ml/base_model_adapter.py +1257 -1230
- dtlpy/ml/metrics.py +461 -461
- dtlpy/ml/predictions_utils.py +274 -274
- dtlpy/ml/summary_writer.py +57 -57
- dtlpy/ml/train_utils.py +60 -60
- dtlpy/new_instance.py +252 -252
- dtlpy/repositories/__init__.py +56 -56
- dtlpy/repositories/analytics.py +85 -85
- dtlpy/repositories/annotations.py +916 -916
- dtlpy/repositories/apps.py +383 -383
- dtlpy/repositories/artifacts.py +452 -452
- dtlpy/repositories/assignments.py +599 -599
- dtlpy/repositories/bots.py +213 -213
- dtlpy/repositories/codebases.py +559 -559
- dtlpy/repositories/collections.py +332 -332
- dtlpy/repositories/commands.py +152 -152
- dtlpy/repositories/compositions.py +61 -61
- dtlpy/repositories/computes.py +439 -439
- dtlpy/repositories/datasets.py +1504 -1504
- dtlpy/repositories/downloader.py +976 -923
- dtlpy/repositories/dpks.py +433 -433
- dtlpy/repositories/drivers.py +482 -482
- dtlpy/repositories/executions.py +815 -815
- dtlpy/repositories/feature_sets.py +226 -226
- dtlpy/repositories/features.py +255 -255
- dtlpy/repositories/integrations.py +484 -484
- dtlpy/repositories/items.py +912 -912
- dtlpy/repositories/messages.py +94 -94
- dtlpy/repositories/models.py +1000 -1000
- dtlpy/repositories/nodes.py +80 -80
- dtlpy/repositories/ontologies.py +511 -511
- dtlpy/repositories/organizations.py +525 -525
- dtlpy/repositories/packages.py +1941 -1941
- dtlpy/repositories/pipeline_executions.py +451 -451
- dtlpy/repositories/pipelines.py +640 -640
- dtlpy/repositories/projects.py +539 -539
- dtlpy/repositories/recipes.py +419 -399
- dtlpy/repositories/resource_executions.py +137 -137
- dtlpy/repositories/schema.py +120 -120
- dtlpy/repositories/service_drivers.py +213 -213
- dtlpy/repositories/services.py +1704 -1704
- dtlpy/repositories/settings.py +339 -339
- dtlpy/repositories/tasks.py +1477 -1477
- dtlpy/repositories/times_series.py +278 -278
- dtlpy/repositories/triggers.py +536 -536
- dtlpy/repositories/upload_element.py +257 -257
- dtlpy/repositories/uploader.py +661 -661
- dtlpy/repositories/webhooks.py +249 -249
- dtlpy/services/__init__.py +22 -22
- dtlpy/services/aihttp_retry.py +131 -131
- dtlpy/services/api_client.py +1785 -1785
- dtlpy/services/api_reference.py +40 -40
- dtlpy/services/async_utils.py +133 -133
- dtlpy/services/calls_counter.py +44 -44
- dtlpy/services/check_sdk.py +68 -68
- dtlpy/services/cookie.py +115 -115
- dtlpy/services/create_logger.py +156 -156
- dtlpy/services/events.py +84 -84
- dtlpy/services/logins.py +235 -235
- dtlpy/services/reporter.py +256 -256
- dtlpy/services/service_defaults.py +91 -91
- dtlpy/utilities/__init__.py +20 -20
- dtlpy/utilities/annotations/__init__.py +16 -16
- dtlpy/utilities/annotations/annotation_converters.py +269 -269
- dtlpy/utilities/base_package_runner.py +285 -264
- dtlpy/utilities/converter.py +1650 -1650
- dtlpy/utilities/dataset_generators/__init__.py +1 -1
- dtlpy/utilities/dataset_generators/dataset_generator.py +670 -670
- dtlpy/utilities/dataset_generators/dataset_generator_tensorflow.py +23 -23
- dtlpy/utilities/dataset_generators/dataset_generator_torch.py +21 -21
- dtlpy/utilities/local_development/__init__.py +1 -1
- dtlpy/utilities/local_development/local_session.py +179 -179
- dtlpy/utilities/reports/__init__.py +2 -2
- dtlpy/utilities/reports/figures.py +343 -343
- dtlpy/utilities/reports/report.py +71 -71
- dtlpy/utilities/videos/__init__.py +17 -17
- dtlpy/utilities/videos/video_player.py +598 -598
- dtlpy/utilities/videos/videos.py +470 -470
- {dtlpy-1.115.44.data → dtlpy-1.116.6.data}/scripts/dlp +1 -1
- dtlpy-1.116.6.data/scripts/dlp.bat +2 -0
- {dtlpy-1.115.44.data → dtlpy-1.116.6.data}/scripts/dlp.py +128 -128
- {dtlpy-1.115.44.dist-info → dtlpy-1.116.6.dist-info}/METADATA +186 -186
- dtlpy-1.116.6.dist-info/RECORD +239 -0
- {dtlpy-1.115.44.dist-info → dtlpy-1.116.6.dist-info}/WHEEL +1 -1
- {dtlpy-1.115.44.dist-info → dtlpy-1.116.6.dist-info}/licenses/LICENSE +200 -200
- tests/features/environment.py +551 -551
- dtlpy/assets/__pycache__/__init__.cpython-310.pyc +0 -0
- dtlpy-1.115.44.data/scripts/dlp.bat +0 -2
- dtlpy-1.115.44.dist-info/RECORD +0 -240
- {dtlpy-1.115.44.dist-info → dtlpy-1.116.6.dist-info}/entry_points.txt +0 -0
- {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
|