mlrun 1.7.0rc43__py3-none-any.whl → 1.7.0rc46__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.
Potentially problematic release.
This version of mlrun might be problematic. Click here for more details.
- mlrun/__main__.py +4 -2
- mlrun/artifacts/manager.py +3 -1
- mlrun/common/formatters/__init__.py +1 -0
- mlrun/common/formatters/feature_set.py +33 -0
- mlrun/common/schemas/__init__.py +1 -0
- mlrun/common/schemas/alert.py +11 -11
- mlrun/common/schemas/auth.py +2 -0
- mlrun/common/schemas/client_spec.py +0 -1
- mlrun/common/schemas/model_monitoring/__init__.py +1 -0
- mlrun/common/schemas/workflow.py +1 -0
- mlrun/config.py +28 -21
- mlrun/data_types/data_types.py +5 -0
- mlrun/datastore/base.py +4 -4
- mlrun/datastore/s3.py +12 -9
- mlrun/datastore/storeytargets.py +2 -2
- mlrun/db/base.py +3 -0
- mlrun/db/httpdb.py +17 -12
- mlrun/db/nopdb.py +24 -4
- mlrun/execution.py +3 -1
- mlrun/feature_store/api.py +1 -0
- mlrun/feature_store/retrieval/spark_merger.py +7 -3
- mlrun/frameworks/_common/plan.py +3 -3
- mlrun/frameworks/_ml_common/plan.py +1 -1
- mlrun/frameworks/parallel_coordinates.py +2 -3
- mlrun/launcher/client.py +6 -6
- mlrun/model_monitoring/applications/results.py +4 -4
- mlrun/model_monitoring/controller.py +1 -1
- mlrun/model_monitoring/db/stores/sqldb/sql_store.py +15 -1
- mlrun/model_monitoring/db/stores/v3io_kv/kv_store.py +12 -0
- mlrun/model_monitoring/db/tsdb/tdengine/schemas.py +7 -7
- mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py +13 -12
- mlrun/model_monitoring/helpers.py +7 -8
- mlrun/model_monitoring/writer.py +3 -1
- mlrun/projects/pipelines.py +2 -0
- mlrun/projects/project.py +43 -19
- mlrun/render.py +3 -3
- mlrun/runtimes/daskjob.py +1 -1
- mlrun/runtimes/kubejob.py +6 -6
- mlrun/runtimes/nuclio/api_gateway.py +6 -0
- mlrun/runtimes/nuclio/application/application.py +3 -3
- mlrun/runtimes/nuclio/function.py +41 -0
- mlrun/runtimes/pod.py +19 -13
- mlrun/serving/server.py +2 -0
- mlrun/utils/helpers.py +22 -16
- mlrun/utils/version/version.json +2 -2
- {mlrun-1.7.0rc43.dist-info → mlrun-1.7.0rc46.dist-info}/METADATA +22 -22
- {mlrun-1.7.0rc43.dist-info → mlrun-1.7.0rc46.dist-info}/RECORD +51 -50
- {mlrun-1.7.0rc43.dist-info → mlrun-1.7.0rc46.dist-info}/WHEEL +1 -1
- {mlrun-1.7.0rc43.dist-info → mlrun-1.7.0rc46.dist-info}/LICENSE +0 -0
- {mlrun-1.7.0rc43.dist-info → mlrun-1.7.0rc46.dist-info}/entry_points.txt +0 -0
- {mlrun-1.7.0rc43.dist-info → mlrun-1.7.0rc46.dist-info}/top_level.txt +0 -0
mlrun/launcher/client.py
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import abc
|
|
15
15
|
from typing import Optional
|
|
16
16
|
|
|
17
|
-
import IPython
|
|
17
|
+
import IPython.display
|
|
18
18
|
|
|
19
19
|
import mlrun.common.constants as mlrun_constants
|
|
20
20
|
import mlrun.errors
|
|
@@ -22,7 +22,7 @@ import mlrun.launcher.base as launcher
|
|
|
22
22
|
import mlrun.lists
|
|
23
23
|
import mlrun.model
|
|
24
24
|
import mlrun.runtimes
|
|
25
|
-
|
|
25
|
+
import mlrun.utils
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
class ClientBaseLauncher(launcher.BaseLauncher, abc.ABC):
|
|
@@ -128,10 +128,10 @@ class ClientBaseLauncher(launcher.BaseLauncher, abc.ABC):
|
|
|
128
128
|
if result:
|
|
129
129
|
results_tbl.append(result)
|
|
130
130
|
else:
|
|
131
|
-
logger.info("no returned result (job may still be in progress)")
|
|
131
|
+
mlrun.utils.logger.info("no returned result (job may still be in progress)")
|
|
132
132
|
results_tbl.append(run.to_dict())
|
|
133
133
|
|
|
134
|
-
if mlrun.utils.
|
|
134
|
+
if mlrun.utils.is_jupyter and mlrun.mlconf.ipython_widget:
|
|
135
135
|
results_tbl.show()
|
|
136
136
|
print()
|
|
137
137
|
ui_url = mlrun.utils.get_ui_url(project, uid)
|
|
@@ -147,9 +147,9 @@ class ClientBaseLauncher(launcher.BaseLauncher, abc.ABC):
|
|
|
147
147
|
project_flag = f"-p {project}" if project else ""
|
|
148
148
|
info_cmd = f"mlrun get run {uid} {project_flag}"
|
|
149
149
|
logs_cmd = f"mlrun logs {uid} {project_flag}"
|
|
150
|
-
logger.info(
|
|
150
|
+
mlrun.utils.logger.info(
|
|
151
151
|
"To track results use the CLI", info_cmd=info_cmd, logs_cmd=logs_cmd
|
|
152
152
|
)
|
|
153
153
|
ui_url = mlrun.utils.get_ui_url(project, uid)
|
|
154
154
|
if ui_url:
|
|
155
|
-
logger.info("Or click for UI", ui_url=ui_url)
|
|
155
|
+
mlrun.utils.logger.info("Or click for UI", ui_url=ui_url)
|
|
@@ -29,8 +29,8 @@ class _ModelMonitoringApplicationDataRes(ABC):
|
|
|
29
29
|
def __post_init__(self):
|
|
30
30
|
pat = re.compile(r"[a-zA-Z_][a-zA-Z0-9_]*")
|
|
31
31
|
if not re.fullmatch(pat, self.name):
|
|
32
|
-
raise mlrun.errors.
|
|
33
|
-
"Attribute name must
|
|
32
|
+
raise mlrun.errors.MLRunValueError(
|
|
33
|
+
"Attribute name must comply with the regex `[a-zA-Z_][a-zA-Z0-9_]*`"
|
|
34
34
|
)
|
|
35
35
|
|
|
36
36
|
@abstractmethod
|
|
@@ -45,7 +45,7 @@ class ModelMonitoringApplicationResult(_ModelMonitoringApplicationDataRes):
|
|
|
45
45
|
|
|
46
46
|
:param name: (str) Name of the application result. This name must be
|
|
47
47
|
unique for each metric in a single application
|
|
48
|
-
(name must be of the format [a-zA-Z_][a-zA-Z0-9_]
|
|
48
|
+
(name must be of the format :code:`[a-zA-Z_][a-zA-Z0-9_]*`).
|
|
49
49
|
:param value: (float) Value of the application result.
|
|
50
50
|
:param kind: (ResultKindApp) Kind of application result.
|
|
51
51
|
:param status: (ResultStatusApp) Status of the application result.
|
|
@@ -80,7 +80,7 @@ class ModelMonitoringApplicationMetric(_ModelMonitoringApplicationDataRes):
|
|
|
80
80
|
|
|
81
81
|
:param name: (str) Name of the application metric. This name must be
|
|
82
82
|
unique for each metric in a single application
|
|
83
|
-
(name must be of the format [a-zA-Z_][a-zA-Z0-9_]
|
|
83
|
+
(name must be of the format :code:`[a-zA-Z_][a-zA-Z0-9_]*`).
|
|
84
84
|
:param value: (float) Value of the application metric.
|
|
85
85
|
"""
|
|
86
86
|
|
|
@@ -219,7 +219,7 @@ class _BatchWindowGenerator:
|
|
|
219
219
|
# If the endpoint does not have a stream, `last_updated` should be
|
|
220
220
|
# the minimum between the current time and the last updated time.
|
|
221
221
|
# This compensates for the bumping mechanism - see
|
|
222
|
-
# `
|
|
222
|
+
# `update_model_endpoint_last_request`.
|
|
223
223
|
last_updated = min(int(datetime_now().timestamp()), last_updated)
|
|
224
224
|
logger.debug(
|
|
225
225
|
"The endpoint does not have a stream", last_updated=last_updated
|
|
@@ -588,7 +588,11 @@ class SQLStoreBase(StoreBase):
|
|
|
588
588
|
|
|
589
589
|
for endpoint_dict in endpoints:
|
|
590
590
|
endpoint_id = endpoint_dict[mm_schemas.EventFieldType.UID]
|
|
591
|
-
|
|
591
|
+
logger.debug(
|
|
592
|
+
"Deleting model endpoint resources from the SQL tables",
|
|
593
|
+
endpoint_id=endpoint_id,
|
|
594
|
+
project=self.project,
|
|
595
|
+
)
|
|
592
596
|
# Delete last analyzed records
|
|
593
597
|
self._delete_last_analyzed(endpoint_id=endpoint_id)
|
|
594
598
|
|
|
@@ -598,6 +602,16 @@ class SQLStoreBase(StoreBase):
|
|
|
598
602
|
|
|
599
603
|
# Delete model endpoint record
|
|
600
604
|
self.delete_model_endpoint(endpoint_id=endpoint_id)
|
|
605
|
+
logger.debug(
|
|
606
|
+
"Successfully deleted model endpoint resources",
|
|
607
|
+
endpoint_id=endpoint_id,
|
|
608
|
+
project=self.project,
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
logger.debug(
|
|
612
|
+
"Successfully deleted model monitoring endpoints resources from the SQL tables",
|
|
613
|
+
project=self.project,
|
|
614
|
+
)
|
|
601
615
|
|
|
602
616
|
def get_model_endpoint_metrics(
|
|
603
617
|
self, endpoint_id: str, type: mm_schemas.ModelEndpointMonitoringMetricType
|
|
@@ -305,10 +305,22 @@ class KVStoreBase(StoreBase):
|
|
|
305
305
|
endpoint_id = endpoint_dict[mm_schemas.EventFieldType.ENDPOINT_ID]
|
|
306
306
|
else:
|
|
307
307
|
endpoint_id = endpoint_dict[mm_schemas.EventFieldType.UID]
|
|
308
|
+
|
|
309
|
+
logger.debug(
|
|
310
|
+
"Deleting model endpoint resources from the V3IO KV table",
|
|
311
|
+
endpoint_id=endpoint_id,
|
|
312
|
+
project=self.project,
|
|
313
|
+
)
|
|
314
|
+
|
|
308
315
|
self.delete_model_endpoint(
|
|
309
316
|
endpoint_id,
|
|
310
317
|
)
|
|
311
318
|
|
|
319
|
+
logger.debug(
|
|
320
|
+
"Successfully deleted model monitoring endpoints from the V3IO KV table",
|
|
321
|
+
project=self.project,
|
|
322
|
+
)
|
|
323
|
+
|
|
312
324
|
# Delete remain records in the KV
|
|
313
325
|
all_records = self.client.kv.new_cursor(
|
|
314
326
|
container=self.container,
|
|
@@ -94,20 +94,20 @@ class TDEngineSchema:
|
|
|
94
94
|
tags = ", ".join(f"{col} {val}" for col, val in self.tags.items())
|
|
95
95
|
return f"CREATE STABLE if NOT EXISTS {self.database}.{self.super_table} ({columns}) TAGS ({tags});"
|
|
96
96
|
|
|
97
|
-
def
|
|
97
|
+
def _create_subtable_sql(
|
|
98
98
|
self,
|
|
99
99
|
subtable: str,
|
|
100
100
|
values: dict[str, Union[str, int, float, datetime.datetime]],
|
|
101
101
|
) -> str:
|
|
102
102
|
try:
|
|
103
|
-
|
|
103
|
+
tags = ", ".join(f"'{values[val]}'" for val in self.tags)
|
|
104
104
|
except KeyError:
|
|
105
105
|
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
106
106
|
f"values must contain all tags: {self.tags.keys()}"
|
|
107
107
|
)
|
|
108
|
-
return f"CREATE TABLE if NOT EXISTS {self.database}.{subtable} USING {self.super_table} TAGS ({
|
|
108
|
+
return f"CREATE TABLE if NOT EXISTS {self.database}.{subtable} USING {self.super_table} TAGS ({tags});"
|
|
109
109
|
|
|
110
|
-
def
|
|
110
|
+
def _insert_subtable_stmt(
|
|
111
111
|
self,
|
|
112
112
|
connection: taosws.Connection,
|
|
113
113
|
subtable: str,
|
|
@@ -116,7 +116,7 @@ class TDEngineSchema:
|
|
|
116
116
|
stmt = connection.statement()
|
|
117
117
|
question_marks = ", ".join("?" * len(self.columns))
|
|
118
118
|
stmt.prepare(f"INSERT INTO ? VALUES ({question_marks});")
|
|
119
|
-
stmt.
|
|
119
|
+
stmt.set_tbname(subtable)
|
|
120
120
|
|
|
121
121
|
bind_params = []
|
|
122
122
|
|
|
@@ -163,8 +163,8 @@ class TDEngineSchema:
|
|
|
163
163
|
@staticmethod
|
|
164
164
|
def _get_records_query(
|
|
165
165
|
table: str,
|
|
166
|
-
start: datetime,
|
|
167
|
-
end: datetime,
|
|
166
|
+
start: datetime.datetime,
|
|
167
|
+
end: datetime.datetime,
|
|
168
168
|
columns_to_filter: list[str] = None,
|
|
169
169
|
filter_query: Optional[str] = None,
|
|
170
170
|
interval: Optional[str] = None,
|
|
@@ -97,7 +97,7 @@ class TDEngineConnector(TSDBConnector):
|
|
|
97
97
|
self,
|
|
98
98
|
event: dict,
|
|
99
99
|
kind: mm_schemas.WriterEventKind = mm_schemas.WriterEventKind.RESULT,
|
|
100
|
-
):
|
|
100
|
+
) -> None:
|
|
101
101
|
"""
|
|
102
102
|
Write a single result or metric to TSDB.
|
|
103
103
|
"""
|
|
@@ -113,7 +113,7 @@ class TDEngineConnector(TSDBConnector):
|
|
|
113
113
|
# Write a new result
|
|
114
114
|
table = self.tables[mm_schemas.TDEngineSuperTables.APP_RESULTS]
|
|
115
115
|
table_name = (
|
|
116
|
-
f"{table_name}_
|
|
116
|
+
f"{table_name}_{event[mm_schemas.ResultData.RESULT_NAME]}"
|
|
117
117
|
).replace("-", "_")
|
|
118
118
|
event.pop(mm_schemas.ResultData.CURRENT_STATS, None)
|
|
119
119
|
|
|
@@ -121,9 +121,13 @@ class TDEngineConnector(TSDBConnector):
|
|
|
121
121
|
# Write a new metric
|
|
122
122
|
table = self.tables[mm_schemas.TDEngineSuperTables.METRICS]
|
|
123
123
|
table_name = (
|
|
124
|
-
f"{table_name}_
|
|
124
|
+
f"{table_name}_{event[mm_schemas.MetricData.METRIC_NAME]}"
|
|
125
125
|
).replace("-", "_")
|
|
126
126
|
|
|
127
|
+
# Escape the table name for case-sensitivity (ML-7908)
|
|
128
|
+
# https://github.com/taosdata/taos-connector-python/issues/260
|
|
129
|
+
table_name = f"`{table_name}`"
|
|
130
|
+
|
|
127
131
|
# Convert the datetime strings to datetime objects
|
|
128
132
|
event[mm_schemas.WriterEvent.END_INFER_TIME] = self._convert_to_datetime(
|
|
129
133
|
val=event[mm_schemas.WriterEvent.END_INFER_TIME]
|
|
@@ -132,15 +136,11 @@ class TDEngineConnector(TSDBConnector):
|
|
|
132
136
|
val=event[mm_schemas.WriterEvent.START_INFER_TIME]
|
|
133
137
|
)
|
|
134
138
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
)
|
|
138
|
-
self.connection.execute(create_table_query)
|
|
139
|
+
create_table_sql = table._create_subtable_sql(subtable=table_name, values=event)
|
|
140
|
+
self.connection.execute(create_table_sql)
|
|
139
141
|
|
|
140
|
-
insert_statement = table.
|
|
141
|
-
self.connection,
|
|
142
|
-
subtable=table_name,
|
|
143
|
-
values=event,
|
|
142
|
+
insert_statement = table._insert_subtable_stmt(
|
|
143
|
+
self.connection, subtable=table_name, values=event
|
|
144
144
|
)
|
|
145
145
|
insert_statement.add_batch()
|
|
146
146
|
insert_statement.execute()
|
|
@@ -280,6 +280,7 @@ class TDEngineConnector(TSDBConnector):
|
|
|
280
280
|
timestamp_column=timestamp_column,
|
|
281
281
|
database=self.database,
|
|
282
282
|
)
|
|
283
|
+
logger.debug("Querying TDEngine", query=full_query)
|
|
283
284
|
try:
|
|
284
285
|
query_result = self.connection.query(full_query)
|
|
285
286
|
except taosws.QueryError as e:
|
|
@@ -336,7 +337,7 @@ class TDEngineConnector(TSDBConnector):
|
|
|
336
337
|
|
|
337
338
|
metrics_condition = " OR ".join(
|
|
338
339
|
[
|
|
339
|
-
f"({mm_schemas.WriterEvent.APPLICATION_NAME}
|
|
340
|
+
f"({mm_schemas.WriterEvent.APPLICATION_NAME}='{metric.app}' AND {name}='{metric.name}')"
|
|
340
341
|
for metric in metrics
|
|
341
342
|
]
|
|
342
343
|
)
|
|
@@ -63,7 +63,6 @@ def get_stream_path(
|
|
|
63
63
|
)
|
|
64
64
|
|
|
65
65
|
if not stream_uri or stream_uri == "v3io":
|
|
66
|
-
# TODO : remove the first part of this condition in 1.9.0
|
|
67
66
|
stream_uri = mlrun.mlconf.get_model_monitoring_file_target_path(
|
|
68
67
|
project=project,
|
|
69
68
|
kind=mm_constants.FileTargetKind.STREAM,
|
|
@@ -71,8 +70,6 @@ def get_stream_path(
|
|
|
71
70
|
function_name=function_name,
|
|
72
71
|
)
|
|
73
72
|
|
|
74
|
-
if isinstance(stream_uri, list): # ML-6043 - user side gets only the new stream uri
|
|
75
|
-
stream_uri = stream_uri[1] # get new stream path, under projects
|
|
76
73
|
return mlrun.common.model_monitoring.helpers.parse_monitoring_stream_path(
|
|
77
74
|
stream_uri=stream_uri, project=project, function_name=function_name
|
|
78
75
|
)
|
|
@@ -179,7 +176,7 @@ def _get_monitoring_time_window_from_controller_run(
|
|
|
179
176
|
def update_model_endpoint_last_request(
|
|
180
177
|
project: str,
|
|
181
178
|
model_endpoint: ModelEndpoint,
|
|
182
|
-
current_request: datetime,
|
|
179
|
+
current_request: datetime.datetime,
|
|
183
180
|
db: "RunDBInterface",
|
|
184
181
|
) -> None:
|
|
185
182
|
"""
|
|
@@ -190,7 +187,8 @@ def update_model_endpoint_last_request(
|
|
|
190
187
|
:param current_request: current request time
|
|
191
188
|
:param db: DB interface.
|
|
192
189
|
"""
|
|
193
|
-
|
|
190
|
+
is_model_server_endpoint = model_endpoint.spec.stream_path != ""
|
|
191
|
+
if is_model_server_endpoint:
|
|
194
192
|
current_request = current_request.isoformat()
|
|
195
193
|
logger.info(
|
|
196
194
|
"Update model endpoint last request time (EP with serving)",
|
|
@@ -204,12 +202,13 @@ def update_model_endpoint_last_request(
|
|
|
204
202
|
endpoint_id=model_endpoint.metadata.uid,
|
|
205
203
|
attributes={mm_constants.EventFieldType.LAST_REQUEST: current_request},
|
|
206
204
|
)
|
|
207
|
-
else:
|
|
205
|
+
else: # model endpoint without any serving function - close the window "manually"
|
|
208
206
|
try:
|
|
209
207
|
time_window = _get_monitoring_time_window_from_controller_run(project, db)
|
|
210
208
|
except mlrun.errors.MLRunNotFoundError:
|
|
211
|
-
logger.
|
|
212
|
-
"Not bumping model endpoint last request time - the monitoring controller isn't deployed yet"
|
|
209
|
+
logger.warn(
|
|
210
|
+
"Not bumping model endpoint last request time - the monitoring controller isn't deployed yet.\n"
|
|
211
|
+
"Call `project.enable_model_monitoring()` first."
|
|
213
212
|
)
|
|
214
213
|
return
|
|
215
214
|
|
mlrun/model_monitoring/writer.py
CHANGED
|
@@ -160,7 +160,9 @@ class ModelMonitoringWriter(StepToDict):
|
|
|
160
160
|
event_kind = f"{event_kind}_detected"
|
|
161
161
|
else:
|
|
162
162
|
event_kind = f"{event_kind}_suspected"
|
|
163
|
-
return alert_objects.EventKind(
|
|
163
|
+
return alert_objects.EventKind(
|
|
164
|
+
value=mlrun.utils.helpers.normalize_name(event_kind)
|
|
165
|
+
)
|
|
164
166
|
|
|
165
167
|
@staticmethod
|
|
166
168
|
def _reconstruct_event(event: _RawEvent) -> tuple[_AppResultEvent, WriterEventKind]:
|
mlrun/projects/pipelines.py
CHANGED
|
@@ -80,6 +80,7 @@ class WorkflowSpec(mlrun.model.ModelObj):
|
|
|
80
80
|
schedule: typing.Union[str, mlrun.common.schemas.ScheduleCronTrigger] = None,
|
|
81
81
|
cleanup_ttl: typing.Optional[int] = None,
|
|
82
82
|
image: typing.Optional[str] = None,
|
|
83
|
+
workflow_runner_node_selector: typing.Optional[dict[str, str]] = None,
|
|
83
84
|
):
|
|
84
85
|
self.engine = engine
|
|
85
86
|
self.code = code
|
|
@@ -93,6 +94,7 @@ class WorkflowSpec(mlrun.model.ModelObj):
|
|
|
93
94
|
self._tmp_path = None
|
|
94
95
|
self.schedule = schedule
|
|
95
96
|
self.image = image
|
|
97
|
+
self.workflow_runner_node_selector = workflow_runner_node_selector
|
|
96
98
|
|
|
97
99
|
def get_source_file(self, context=""):
|
|
98
100
|
if not self.code and not self.path:
|
mlrun/projects/project.py
CHANGED
|
@@ -67,13 +67,7 @@ from ..features import Feature
|
|
|
67
67
|
from ..model import EntrypointParam, ImageBuilder, ModelObj
|
|
68
68
|
from ..run import code_to_function, get_object, import_function, new_function
|
|
69
69
|
from ..secrets import SecretsStore
|
|
70
|
-
from ..utils import
|
|
71
|
-
is_ipython,
|
|
72
|
-
is_relative_path,
|
|
73
|
-
is_yaml_path,
|
|
74
|
-
logger,
|
|
75
|
-
update_in,
|
|
76
|
-
)
|
|
70
|
+
from ..utils import is_jupyter, is_relative_path, is_yaml_path, logger, update_in
|
|
77
71
|
from ..utils.clones import (
|
|
78
72
|
add_credentials_git_remote_url,
|
|
79
73
|
clone_git,
|
|
@@ -1558,7 +1552,7 @@ class MlrunProject(ModelObj):
|
|
|
1558
1552
|
url = path.normpath(path.join(self.spec.get_code_path(), url))
|
|
1559
1553
|
|
|
1560
1554
|
if (not in_context or check_path_in_context) and not path.isfile(url):
|
|
1561
|
-
raise
|
|
1555
|
+
raise FileNotFoundError(f"{url} not found")
|
|
1562
1556
|
|
|
1563
1557
|
return url, in_context
|
|
1564
1558
|
|
|
@@ -1599,7 +1593,9 @@ class MlrunProject(ModelObj):
|
|
|
1599
1593
|
:param format: artifact file format: csv, png, ..
|
|
1600
1594
|
:param tag: version tag
|
|
1601
1595
|
:param target_path: absolute target path (instead of using artifact_path + local_path)
|
|
1602
|
-
:param upload: upload to datastore
|
|
1596
|
+
:param upload: Whether to upload the artifact to the datastore. If not provided, and the `local_path`
|
|
1597
|
+
is not a directory, upload occurs by default. Directories are uploaded only when this
|
|
1598
|
+
flag is explicitly set to `True`.
|
|
1603
1599
|
:param labels: a set of key/value labels to tag the artifact with
|
|
1604
1600
|
|
|
1605
1601
|
:returns: artifact object
|
|
@@ -2439,7 +2435,7 @@ class MlrunProject(ModelObj):
|
|
|
2439
2435
|
):
|
|
2440
2436
|
# if function path is not provided and it is not a module (no ".")
|
|
2441
2437
|
# use the current notebook as default
|
|
2442
|
-
if
|
|
2438
|
+
if is_jupyter:
|
|
2443
2439
|
from IPython import get_ipython
|
|
2444
2440
|
|
|
2445
2441
|
kernel = get_ipython()
|
|
@@ -2842,11 +2838,13 @@ class MlrunProject(ModelObj):
|
|
|
2842
2838
|
The function objects are synced against the definitions spec in `self.spec._function_definitions`.
|
|
2843
2839
|
Referenced files/URLs in the function spec will be reloaded.
|
|
2844
2840
|
Function definitions are parsed by the following precedence:
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2841
|
+
|
|
2842
|
+
1. Contains runtime spec.
|
|
2843
|
+
2. Contains module in the project's context.
|
|
2844
|
+
3. Contains path to function definition (yaml, DB, Hub).
|
|
2845
|
+
4. Contains path to .ipynb or .py files.
|
|
2846
|
+
5. Contains a Nuclio/Serving function image / an 'Application' kind definition.
|
|
2847
|
+
|
|
2850
2848
|
If function definition is already an object, some project metadata updates will apply however,
|
|
2851
2849
|
it will not be reloaded.
|
|
2852
2850
|
|
|
@@ -2902,6 +2900,16 @@ class MlrunProject(ModelObj):
|
|
|
2902
2900
|
continue
|
|
2903
2901
|
|
|
2904
2902
|
raise mlrun.errors.MLRunMissingDependencyError(message) from exc
|
|
2903
|
+
|
|
2904
|
+
except Exception as exc:
|
|
2905
|
+
if silent:
|
|
2906
|
+
logger.warn(
|
|
2907
|
+
"Failed to instantiate function",
|
|
2908
|
+
name=name,
|
|
2909
|
+
error=mlrun.utils.err_to_str(exc),
|
|
2910
|
+
)
|
|
2911
|
+
continue
|
|
2912
|
+
raise exc
|
|
2905
2913
|
else:
|
|
2906
2914
|
message = f"Function {name} must be an object or dict."
|
|
2907
2915
|
if silent:
|
|
@@ -3060,6 +3068,7 @@ class MlrunProject(ModelObj):
|
|
|
3060
3068
|
source: str = None,
|
|
3061
3069
|
cleanup_ttl: int = None,
|
|
3062
3070
|
notifications: list[mlrun.model.Notification] = None,
|
|
3071
|
+
workflow_runner_node_selector: typing.Optional[dict[str, str]] = None,
|
|
3063
3072
|
) -> _PipelineRunStatus:
|
|
3064
3073
|
"""Run a workflow using kubeflow pipelines
|
|
3065
3074
|
|
|
@@ -3088,15 +3097,20 @@ class MlrunProject(ModelObj):
|
|
|
3088
3097
|
|
|
3089
3098
|
* Remote URL which is loaded dynamically to the workflow runner.
|
|
3090
3099
|
* A path to the project's context on the workflow runner's image.
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3100
|
+
Path can be absolute or relative to `project.spec.build.source_code_target_dir` if defined
|
|
3101
|
+
(enriched when building a project image with source, see `MlrunProject.build_image`).
|
|
3102
|
+
For other engines the source is used to validate that the code is up-to-date.
|
|
3103
|
+
|
|
3094
3104
|
:param cleanup_ttl:
|
|
3095
3105
|
Pipeline cleanup ttl in secs (time to wait after workflow completion, at which point the
|
|
3096
3106
|
workflow and all its resources are deleted)
|
|
3097
3107
|
:param notifications:
|
|
3098
3108
|
List of notifications to send for workflow completion
|
|
3099
|
-
|
|
3109
|
+
:param workflow_runner_node_selector:
|
|
3110
|
+
Defines the node selector for the workflow runner pod when using a remote engine.
|
|
3111
|
+
This allows you to control and specify where the workflow runner pod will be scheduled.
|
|
3112
|
+
This setting is only relevant when the engine is set to 'remote' or for scheduled workflows,
|
|
3113
|
+
and it will be ignored if the workflow is not run on a remote engine.
|
|
3100
3114
|
:returns: ~py:class:`~mlrun.projects.pipelines._PipelineRunStatus` instance
|
|
3101
3115
|
"""
|
|
3102
3116
|
|
|
@@ -3162,6 +3176,16 @@ class MlrunProject(ModelObj):
|
|
|
3162
3176
|
)
|
|
3163
3177
|
inner_engine = get_workflow_engine(engine_kind, local).engine
|
|
3164
3178
|
workflow_spec.engine = inner_engine or workflow_engine.engine
|
|
3179
|
+
if workflow_runner_node_selector:
|
|
3180
|
+
if workflow_engine.engine == "remote":
|
|
3181
|
+
workflow_spec.workflow_runner_node_selector = (
|
|
3182
|
+
workflow_runner_node_selector
|
|
3183
|
+
)
|
|
3184
|
+
else:
|
|
3185
|
+
logger.warn(
|
|
3186
|
+
"'workflow_runner_node_selector' applies only to remote engines"
|
|
3187
|
+
" and is ignored for non-remote runs."
|
|
3188
|
+
)
|
|
3165
3189
|
|
|
3166
3190
|
run = workflow_engine.run(
|
|
3167
3191
|
self,
|
mlrun/render.py
CHANGED
|
@@ -22,7 +22,7 @@ import mlrun.utils
|
|
|
22
22
|
|
|
23
23
|
from .config import config
|
|
24
24
|
from .datastore import uri_to_ipython
|
|
25
|
-
from .utils import dict_to_list, get_in,
|
|
25
|
+
from .utils import dict_to_list, get_in, is_jupyter
|
|
26
26
|
|
|
27
27
|
JUPYTER_SERVER_ROOT = environ.get("HOME", "/User")
|
|
28
28
|
supported_viewers = [
|
|
@@ -181,8 +181,8 @@ def run_to_html(results, display=True):
|
|
|
181
181
|
|
|
182
182
|
|
|
183
183
|
def ipython_display(html, display=True, alt_text=None):
|
|
184
|
-
if display and html and
|
|
185
|
-
import IPython
|
|
184
|
+
if display and html and is_jupyter:
|
|
185
|
+
import IPython.display
|
|
186
186
|
|
|
187
187
|
IPython.display.display(IPython.display.HTML(html))
|
|
188
188
|
elif alt_text:
|
mlrun/runtimes/daskjob.py
CHANGED
|
@@ -379,7 +379,7 @@ class DaskCluster(KubejobRuntime):
|
|
|
379
379
|
:param show_on_failure: show logs only in case of build failure
|
|
380
380
|
:param force_build: force building the image, even when no changes were made
|
|
381
381
|
|
|
382
|
-
:return
|
|
382
|
+
:return: True if the function is ready (deployed)
|
|
383
383
|
"""
|
|
384
384
|
return super().deploy(
|
|
385
385
|
watch,
|
mlrun/runtimes/kubejob.py
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
|
-
|
|
14
|
+
import typing
|
|
15
15
|
import warnings
|
|
16
16
|
|
|
17
17
|
from mlrun_pipelines.common.ops import build_op
|
|
@@ -143,11 +143,11 @@ class KubejobRuntime(KubeResource):
|
|
|
143
143
|
|
|
144
144
|
def deploy(
|
|
145
145
|
self,
|
|
146
|
-
watch=True,
|
|
147
|
-
with_mlrun=None,
|
|
148
|
-
skip_deployed=False,
|
|
149
|
-
is_kfp=False,
|
|
150
|
-
mlrun_version_specifier=None,
|
|
146
|
+
watch: bool = True,
|
|
147
|
+
with_mlrun: typing.Optional[bool] = None,
|
|
148
|
+
skip_deployed: bool = False,
|
|
149
|
+
is_kfp: bool = False,
|
|
150
|
+
mlrun_version_specifier: typing.Optional[bool] = None,
|
|
151
151
|
builder_env: dict = None,
|
|
152
152
|
show_on_failure: bool = False,
|
|
153
153
|
force_build: bool = False,
|
|
@@ -587,6 +587,12 @@ class APIGateway(ModelObj):
|
|
|
587
587
|
self.metadata.annotations, gateway_timeout
|
|
588
588
|
)
|
|
589
589
|
|
|
590
|
+
def with_annotations(self, annotations: dict):
|
|
591
|
+
"""set a key/value annotations in the metadata of the api gateway"""
|
|
592
|
+
for key, value in annotations.items():
|
|
593
|
+
self.metadata.annotations[key] = str(value)
|
|
594
|
+
return self
|
|
595
|
+
|
|
590
596
|
@classmethod
|
|
591
597
|
def from_scheme(cls, api_gateway: schemas.APIGateway):
|
|
592
598
|
project = api_gateway.metadata.labels.get(
|
|
@@ -438,9 +438,10 @@ class ApplicationRuntime(RemoteRuntime):
|
|
|
438
438
|
"""
|
|
439
439
|
Create the application API gateway. Once the application is deployed, the API gateway can be created.
|
|
440
440
|
An application without an API gateway is not accessible.
|
|
441
|
+
|
|
441
442
|
:param name: The name of the API gateway
|
|
442
443
|
:param path: Optional path of the API gateway, default value is "/".
|
|
443
|
-
|
|
444
|
+
The given path should be supported by the deployed application
|
|
444
445
|
:param direct_port_access: Set True to allow direct port access to the application sidecar
|
|
445
446
|
:param authentication_mode: API Gateway authentication mode
|
|
446
447
|
:param authentication_creds: API Gateway basic authentication credentials as a tuple (username, password)
|
|
@@ -449,8 +450,7 @@ class ApplicationRuntime(RemoteRuntime):
|
|
|
449
450
|
:param set_as_default: Set the API gateway as the default for the application (`status.api_gateway`)
|
|
450
451
|
:param gateway_timeout: nginx ingress timeout in sec (request timeout, when will the gateway return an
|
|
451
452
|
error)
|
|
452
|
-
|
|
453
|
-
:return: The API gateway URL
|
|
453
|
+
:return: The API gateway URL
|
|
454
454
|
"""
|
|
455
455
|
if not name:
|
|
456
456
|
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
@@ -23,6 +23,7 @@ import inflection
|
|
|
23
23
|
import nuclio
|
|
24
24
|
import nuclio.utils
|
|
25
25
|
import requests
|
|
26
|
+
import semver
|
|
26
27
|
from aiohttp.client import ClientSession
|
|
27
28
|
from kubernetes import client
|
|
28
29
|
from mlrun_pipelines.common.mounts import VolumeMount
|
|
@@ -296,10 +297,37 @@ class RemoteRuntime(KubeResource):
|
|
|
296
297
|
"""
|
|
297
298
|
if hasattr(spec, "to_dict"):
|
|
298
299
|
spec = spec.to_dict()
|
|
300
|
+
|
|
301
|
+
self._validate_triggers(spec)
|
|
302
|
+
|
|
299
303
|
spec["name"] = name
|
|
300
304
|
self.spec.config[f"spec.triggers.{name}"] = spec
|
|
301
305
|
return self
|
|
302
306
|
|
|
307
|
+
def _validate_triggers(self, spec):
|
|
308
|
+
# ML-7763 / NUC-233
|
|
309
|
+
min_nuclio_version = "1.13.12"
|
|
310
|
+
if mlconf.nuclio_version and semver.VersionInfo.parse(
|
|
311
|
+
mlconf.nuclio_version
|
|
312
|
+
) < semver.VersionInfo.parse(min_nuclio_version):
|
|
313
|
+
explicit_ack_enabled = False
|
|
314
|
+
num_triggers = 0
|
|
315
|
+
trigger_name = spec.get("name", "UNKNOWN")
|
|
316
|
+
for key, config in [(f"spec.triggers.{trigger_name}", spec)] + list(
|
|
317
|
+
self.spec.config.items()
|
|
318
|
+
):
|
|
319
|
+
if key.startswith("spec.triggers."):
|
|
320
|
+
num_triggers += 1
|
|
321
|
+
explicit_ack_enabled = (
|
|
322
|
+
config.get("explicitAckMode", "disable") != "disable"
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
if num_triggers > 1 and explicit_ack_enabled:
|
|
326
|
+
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
327
|
+
"Multiple triggers cannot be used in conjunction with explicit ack. "
|
|
328
|
+
f"Please upgrade to nuclio {min_nuclio_version} or newer."
|
|
329
|
+
)
|
|
330
|
+
|
|
303
331
|
def with_source_archive(
|
|
304
332
|
self,
|
|
305
333
|
source,
|
|
@@ -495,6 +523,11 @@ class RemoteRuntime(KubeResource):
|
|
|
495
523
|
extra_attributes = extra_attributes or {}
|
|
496
524
|
if ack_window_size:
|
|
497
525
|
extra_attributes["ackWindowSize"] = ack_window_size
|
|
526
|
+
|
|
527
|
+
access_key = kwargs.pop("access_key", None)
|
|
528
|
+
if not access_key:
|
|
529
|
+
access_key = self._resolve_v3io_access_key()
|
|
530
|
+
|
|
498
531
|
self.add_trigger(
|
|
499
532
|
name,
|
|
500
533
|
V3IOStreamTrigger(
|
|
@@ -506,6 +539,7 @@ class RemoteRuntime(KubeResource):
|
|
|
506
539
|
webapi=endpoint or "http://v3io-webapi:8081",
|
|
507
540
|
extra_attributes=extra_attributes,
|
|
508
541
|
read_batch_size=256,
|
|
542
|
+
access_key=access_key,
|
|
509
543
|
**kwargs,
|
|
510
544
|
),
|
|
511
545
|
)
|
|
@@ -1241,6 +1275,13 @@ class RemoteRuntime(KubeResource):
|
|
|
1241
1275
|
|
|
1242
1276
|
return self._resolve_invocation_url("", force_external_address)
|
|
1243
1277
|
|
|
1278
|
+
@staticmethod
|
|
1279
|
+
def _resolve_v3io_access_key():
|
|
1280
|
+
# Nuclio supports generating access key for v3io stream trigger only from version 1.13.11
|
|
1281
|
+
if validate_nuclio_version_compatibility("1.13.11"):
|
|
1282
|
+
return mlrun.model.Credentials.generate_access_key
|
|
1283
|
+
return None
|
|
1284
|
+
|
|
1244
1285
|
|
|
1245
1286
|
def parse_logs(logs):
|
|
1246
1287
|
logs = json.loads(logs)
|