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.

Files changed (51) hide show
  1. mlrun/__main__.py +4 -2
  2. mlrun/artifacts/manager.py +3 -1
  3. mlrun/common/formatters/__init__.py +1 -0
  4. mlrun/common/formatters/feature_set.py +33 -0
  5. mlrun/common/schemas/__init__.py +1 -0
  6. mlrun/common/schemas/alert.py +11 -11
  7. mlrun/common/schemas/auth.py +2 -0
  8. mlrun/common/schemas/client_spec.py +0 -1
  9. mlrun/common/schemas/model_monitoring/__init__.py +1 -0
  10. mlrun/common/schemas/workflow.py +1 -0
  11. mlrun/config.py +28 -21
  12. mlrun/data_types/data_types.py +5 -0
  13. mlrun/datastore/base.py +4 -4
  14. mlrun/datastore/s3.py +12 -9
  15. mlrun/datastore/storeytargets.py +2 -2
  16. mlrun/db/base.py +3 -0
  17. mlrun/db/httpdb.py +17 -12
  18. mlrun/db/nopdb.py +24 -4
  19. mlrun/execution.py +3 -1
  20. mlrun/feature_store/api.py +1 -0
  21. mlrun/feature_store/retrieval/spark_merger.py +7 -3
  22. mlrun/frameworks/_common/plan.py +3 -3
  23. mlrun/frameworks/_ml_common/plan.py +1 -1
  24. mlrun/frameworks/parallel_coordinates.py +2 -3
  25. mlrun/launcher/client.py +6 -6
  26. mlrun/model_monitoring/applications/results.py +4 -4
  27. mlrun/model_monitoring/controller.py +1 -1
  28. mlrun/model_monitoring/db/stores/sqldb/sql_store.py +15 -1
  29. mlrun/model_monitoring/db/stores/v3io_kv/kv_store.py +12 -0
  30. mlrun/model_monitoring/db/tsdb/tdengine/schemas.py +7 -7
  31. mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py +13 -12
  32. mlrun/model_monitoring/helpers.py +7 -8
  33. mlrun/model_monitoring/writer.py +3 -1
  34. mlrun/projects/pipelines.py +2 -0
  35. mlrun/projects/project.py +43 -19
  36. mlrun/render.py +3 -3
  37. mlrun/runtimes/daskjob.py +1 -1
  38. mlrun/runtimes/kubejob.py +6 -6
  39. mlrun/runtimes/nuclio/api_gateway.py +6 -0
  40. mlrun/runtimes/nuclio/application/application.py +3 -3
  41. mlrun/runtimes/nuclio/function.py +41 -0
  42. mlrun/runtimes/pod.py +19 -13
  43. mlrun/serving/server.py +2 -0
  44. mlrun/utils/helpers.py +22 -16
  45. mlrun/utils/version/version.json +2 -2
  46. {mlrun-1.7.0rc43.dist-info → mlrun-1.7.0rc46.dist-info}/METADATA +22 -22
  47. {mlrun-1.7.0rc43.dist-info → mlrun-1.7.0rc46.dist-info}/RECORD +51 -50
  48. {mlrun-1.7.0rc43.dist-info → mlrun-1.7.0rc46.dist-info}/WHEEL +1 -1
  49. {mlrun-1.7.0rc43.dist-info → mlrun-1.7.0rc46.dist-info}/LICENSE +0 -0
  50. {mlrun-1.7.0rc43.dist-info → mlrun-1.7.0rc46.dist-info}/entry_points.txt +0 -0
  51. {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
- from mlrun.utils import logger
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.is_ipython and mlrun.mlconf.ipython_widget:
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.MLRunInvalidArgumentError(
33
- "Attribute name must be of the format [a-zA-Z_][a-zA-Z0-9_]*"
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
- # `bump_model_endpoint_last_request`.
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 _create_subtable_query(
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
- values = ", ".join(f"'{values[val]}'" for val in self.tags)
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 ({values});"
108
+ return f"CREATE TABLE if NOT EXISTS {self.database}.{subtable} USING {self.super_table} TAGS ({tags});"
109
109
 
110
- def _insert_subtable_query(
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.set_tbname_tags(subtable, [])
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}_" f"{event[mm_schemas.ResultData.RESULT_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}_" f"{event[mm_schemas.MetricData.METRIC_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
- create_table_query = table._create_subtable_query(
136
- subtable=table_name, values=event
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._insert_subtable_query(
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} = '{metric.app}' AND {name} = '{metric.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
- if model_endpoint.spec.stream_path != "":
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.debug(
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
 
@@ -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(value=event_kind)
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]:
@@ -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 mlrun.errors.MLRunNotFoundError(f"{url} not found")
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 (default is True)
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 is_ipython:
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
- 1. Contains runtime spec.
2846
- 2. Contains module in the project's context.
2847
- 3. Contains path to function definition (yaml, DB, Hub).
2848
- 4. Contains path to .ipynb or .py files.
2849
- 5. Contains a Nuclio/Serving function image / an 'Application' kind definition.
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
- Path can be absolute or relative to `project.spec.build.source_code_target_dir` if defined
3092
- (enriched when building a project image with source, see `MlrunProject.build_image`).
3093
- For other engines the source is used to validate that the code is up-to-date.
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, is_ipython
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 is_ipython:
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 True if the function is ready (deployed)
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
- The given path should be supported by the deployed application
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)