mlrun 1.7.0rc4__py3-none-any.whl → 1.7.0rc20__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 (200) hide show
  1. mlrun/__init__.py +11 -1
  2. mlrun/__main__.py +25 -111
  3. mlrun/{datastore/helpers.py → alerts/__init__.py} +2 -5
  4. mlrun/alerts/alert.py +144 -0
  5. mlrun/api/schemas/__init__.py +4 -3
  6. mlrun/artifacts/__init__.py +8 -3
  7. mlrun/artifacts/base.py +38 -254
  8. mlrun/artifacts/dataset.py +9 -190
  9. mlrun/artifacts/manager.py +41 -47
  10. mlrun/artifacts/model.py +30 -158
  11. mlrun/artifacts/plots.py +23 -380
  12. mlrun/common/constants.py +68 -0
  13. mlrun/common/formatters/__init__.py +19 -0
  14. mlrun/{model_monitoring/stores/models/sqlite.py → common/formatters/artifact.py} +6 -8
  15. mlrun/common/formatters/base.py +78 -0
  16. mlrun/common/formatters/function.py +41 -0
  17. mlrun/common/formatters/pipeline.py +53 -0
  18. mlrun/common/formatters/project.py +51 -0
  19. mlrun/{runtimes → common/runtimes}/constants.py +32 -4
  20. mlrun/common/schemas/__init__.py +25 -4
  21. mlrun/common/schemas/alert.py +203 -0
  22. mlrun/common/schemas/api_gateway.py +148 -0
  23. mlrun/common/schemas/artifact.py +15 -5
  24. mlrun/common/schemas/auth.py +8 -2
  25. mlrun/common/schemas/client_spec.py +2 -0
  26. mlrun/common/schemas/frontend_spec.py +1 -0
  27. mlrun/common/schemas/function.py +4 -0
  28. mlrun/common/schemas/hub.py +7 -9
  29. mlrun/common/schemas/model_monitoring/__init__.py +19 -3
  30. mlrun/common/schemas/model_monitoring/constants.py +96 -26
  31. mlrun/common/schemas/model_monitoring/grafana.py +9 -5
  32. mlrun/common/schemas/model_monitoring/model_endpoints.py +86 -2
  33. mlrun/{runtimes/mpijob/v1alpha1.py → common/schemas/pagination.py} +10 -13
  34. mlrun/common/schemas/pipeline.py +0 -9
  35. mlrun/common/schemas/project.py +22 -21
  36. mlrun/common/types.py +7 -1
  37. mlrun/config.py +87 -19
  38. mlrun/data_types/data_types.py +4 -0
  39. mlrun/data_types/to_pandas.py +9 -9
  40. mlrun/datastore/__init__.py +5 -8
  41. mlrun/datastore/alibaba_oss.py +130 -0
  42. mlrun/datastore/azure_blob.py +4 -5
  43. mlrun/datastore/base.py +69 -30
  44. mlrun/datastore/datastore.py +10 -2
  45. mlrun/datastore/datastore_profile.py +90 -6
  46. mlrun/datastore/google_cloud_storage.py +1 -1
  47. mlrun/datastore/hdfs.py +5 -0
  48. mlrun/datastore/inmem.py +2 -2
  49. mlrun/datastore/redis.py +2 -2
  50. mlrun/datastore/s3.py +5 -0
  51. mlrun/datastore/snowflake_utils.py +43 -0
  52. mlrun/datastore/sources.py +172 -44
  53. mlrun/datastore/store_resources.py +7 -7
  54. mlrun/datastore/targets.py +285 -41
  55. mlrun/datastore/utils.py +68 -5
  56. mlrun/datastore/v3io.py +27 -50
  57. mlrun/db/auth_utils.py +152 -0
  58. mlrun/db/base.py +149 -14
  59. mlrun/db/factory.py +1 -1
  60. mlrun/db/httpdb.py +608 -178
  61. mlrun/db/nopdb.py +191 -7
  62. mlrun/errors.py +11 -0
  63. mlrun/execution.py +37 -20
  64. mlrun/feature_store/__init__.py +0 -2
  65. mlrun/feature_store/api.py +21 -52
  66. mlrun/feature_store/feature_set.py +48 -23
  67. mlrun/feature_store/feature_vector.py +2 -1
  68. mlrun/feature_store/ingestion.py +7 -6
  69. mlrun/feature_store/retrieval/base.py +9 -4
  70. mlrun/feature_store/retrieval/conversion.py +9 -9
  71. mlrun/feature_store/retrieval/dask_merger.py +2 -0
  72. mlrun/feature_store/retrieval/job.py +9 -3
  73. mlrun/feature_store/retrieval/local_merger.py +2 -0
  74. mlrun/feature_store/retrieval/spark_merger.py +34 -24
  75. mlrun/feature_store/steps.py +30 -19
  76. mlrun/features.py +4 -13
  77. mlrun/frameworks/_dl_common/loggers/tensorboard_logger.py +7 -12
  78. mlrun/frameworks/auto_mlrun/auto_mlrun.py +2 -2
  79. mlrun/frameworks/lgbm/__init__.py +1 -1
  80. mlrun/frameworks/lgbm/callbacks/callback.py +2 -4
  81. mlrun/frameworks/lgbm/model_handler.py +1 -1
  82. mlrun/frameworks/parallel_coordinates.py +2 -1
  83. mlrun/frameworks/pytorch/__init__.py +2 -2
  84. mlrun/frameworks/sklearn/__init__.py +1 -1
  85. mlrun/frameworks/tf_keras/__init__.py +5 -2
  86. mlrun/frameworks/tf_keras/callbacks/logging_callback.py +1 -1
  87. mlrun/frameworks/tf_keras/mlrun_interface.py +2 -2
  88. mlrun/frameworks/xgboost/__init__.py +1 -1
  89. mlrun/k8s_utils.py +10 -11
  90. mlrun/launcher/__init__.py +1 -1
  91. mlrun/launcher/base.py +6 -5
  92. mlrun/launcher/client.py +8 -6
  93. mlrun/launcher/factory.py +1 -1
  94. mlrun/launcher/local.py +9 -3
  95. mlrun/launcher/remote.py +9 -3
  96. mlrun/lists.py +6 -2
  97. mlrun/model.py +58 -19
  98. mlrun/model_monitoring/__init__.py +1 -1
  99. mlrun/model_monitoring/api.py +127 -301
  100. mlrun/model_monitoring/application.py +5 -296
  101. mlrun/model_monitoring/applications/__init__.py +11 -0
  102. mlrun/model_monitoring/applications/_application_steps.py +157 -0
  103. mlrun/model_monitoring/applications/base.py +282 -0
  104. mlrun/model_monitoring/applications/context.py +214 -0
  105. mlrun/model_monitoring/applications/evidently_base.py +211 -0
  106. mlrun/model_monitoring/applications/histogram_data_drift.py +224 -93
  107. mlrun/model_monitoring/applications/results.py +99 -0
  108. mlrun/model_monitoring/controller.py +30 -36
  109. mlrun/model_monitoring/db/__init__.py +18 -0
  110. mlrun/model_monitoring/{stores → db/stores}/__init__.py +43 -36
  111. mlrun/model_monitoring/db/stores/base/__init__.py +15 -0
  112. mlrun/model_monitoring/{stores/model_endpoint_store.py → db/stores/base/store.py} +58 -32
  113. mlrun/model_monitoring/db/stores/sqldb/__init__.py +13 -0
  114. mlrun/model_monitoring/db/stores/sqldb/models/__init__.py +71 -0
  115. mlrun/model_monitoring/{stores → db/stores/sqldb}/models/base.py +109 -5
  116. mlrun/model_monitoring/db/stores/sqldb/models/mysql.py +88 -0
  117. mlrun/model_monitoring/{stores/models/mysql.py → db/stores/sqldb/models/sqlite.py} +19 -13
  118. mlrun/model_monitoring/db/stores/sqldb/sql_store.py +684 -0
  119. mlrun/model_monitoring/db/stores/v3io_kv/__init__.py +13 -0
  120. mlrun/model_monitoring/{stores/kv_model_endpoint_store.py → db/stores/v3io_kv/kv_store.py} +302 -155
  121. mlrun/model_monitoring/db/tsdb/__init__.py +100 -0
  122. mlrun/model_monitoring/db/tsdb/base.py +329 -0
  123. mlrun/model_monitoring/db/tsdb/helpers.py +30 -0
  124. mlrun/model_monitoring/db/tsdb/tdengine/__init__.py +15 -0
  125. mlrun/model_monitoring/db/tsdb/tdengine/schemas.py +240 -0
  126. mlrun/model_monitoring/db/tsdb/tdengine/stream_graph_steps.py +45 -0
  127. mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py +397 -0
  128. mlrun/model_monitoring/db/tsdb/v3io/__init__.py +15 -0
  129. mlrun/model_monitoring/db/tsdb/v3io/stream_graph_steps.py +117 -0
  130. mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +630 -0
  131. mlrun/model_monitoring/evidently_application.py +6 -118
  132. mlrun/model_monitoring/features_drift_table.py +34 -22
  133. mlrun/model_monitoring/helpers.py +100 -7
  134. mlrun/model_monitoring/model_endpoint.py +3 -2
  135. mlrun/model_monitoring/stream_processing.py +93 -228
  136. mlrun/model_monitoring/tracking_policy.py +7 -1
  137. mlrun/model_monitoring/writer.py +152 -124
  138. mlrun/package/packagers_manager.py +1 -0
  139. mlrun/package/utils/_formatter.py +2 -2
  140. mlrun/platforms/__init__.py +11 -10
  141. mlrun/platforms/iguazio.py +21 -202
  142. mlrun/projects/operations.py +30 -16
  143. mlrun/projects/pipelines.py +92 -99
  144. mlrun/projects/project.py +757 -268
  145. mlrun/render.py +15 -14
  146. mlrun/run.py +160 -162
  147. mlrun/runtimes/__init__.py +55 -3
  148. mlrun/runtimes/base.py +33 -19
  149. mlrun/runtimes/databricks_job/databricks_wrapper.py +1 -1
  150. mlrun/runtimes/funcdoc.py +0 -28
  151. mlrun/runtimes/kubejob.py +28 -122
  152. mlrun/runtimes/local.py +5 -2
  153. mlrun/runtimes/mpijob/__init__.py +0 -20
  154. mlrun/runtimes/mpijob/abstract.py +8 -8
  155. mlrun/runtimes/mpijob/v1.py +1 -1
  156. mlrun/runtimes/nuclio/__init__.py +1 -0
  157. mlrun/runtimes/nuclio/api_gateway.py +709 -0
  158. mlrun/runtimes/nuclio/application/__init__.py +15 -0
  159. mlrun/runtimes/nuclio/application/application.py +523 -0
  160. mlrun/runtimes/nuclio/application/reverse_proxy.go +95 -0
  161. mlrun/runtimes/nuclio/function.py +98 -58
  162. mlrun/runtimes/nuclio/serving.py +36 -42
  163. mlrun/runtimes/pod.py +196 -45
  164. mlrun/runtimes/remotesparkjob.py +1 -1
  165. mlrun/runtimes/sparkjob/spark3job.py +1 -1
  166. mlrun/runtimes/utils.py +6 -73
  167. mlrun/secrets.py +6 -2
  168. mlrun/serving/remote.py +2 -3
  169. mlrun/serving/routers.py +7 -4
  170. mlrun/serving/server.py +7 -8
  171. mlrun/serving/states.py +73 -43
  172. mlrun/serving/v2_serving.py +8 -7
  173. mlrun/track/tracker.py +2 -1
  174. mlrun/utils/async_http.py +25 -5
  175. mlrun/utils/helpers.py +141 -75
  176. mlrun/utils/http.py +1 -1
  177. mlrun/utils/logger.py +39 -7
  178. mlrun/utils/notifications/notification/__init__.py +14 -9
  179. mlrun/utils/notifications/notification/base.py +12 -0
  180. mlrun/utils/notifications/notification/console.py +2 -0
  181. mlrun/utils/notifications/notification/git.py +3 -1
  182. mlrun/utils/notifications/notification/ipython.py +2 -0
  183. mlrun/utils/notifications/notification/slack.py +101 -21
  184. mlrun/utils/notifications/notification/webhook.py +11 -1
  185. mlrun/utils/notifications/notification_pusher.py +147 -16
  186. mlrun/utils/retryer.py +3 -2
  187. mlrun/utils/v3io_clients.py +0 -1
  188. mlrun/utils/version/version.json +2 -2
  189. {mlrun-1.7.0rc4.dist-info → mlrun-1.7.0rc20.dist-info}/METADATA +33 -18
  190. mlrun-1.7.0rc20.dist-info/RECORD +353 -0
  191. {mlrun-1.7.0rc4.dist-info → mlrun-1.7.0rc20.dist-info}/WHEEL +1 -1
  192. mlrun/kfpops.py +0 -868
  193. mlrun/model_monitoring/batch.py +0 -974
  194. mlrun/model_monitoring/stores/models/__init__.py +0 -27
  195. mlrun/model_monitoring/stores/sql_model_endpoint_store.py +0 -382
  196. mlrun/platforms/other.py +0 -305
  197. mlrun-1.7.0rc4.dist-info/RECORD +0 -321
  198. {mlrun-1.7.0rc4.dist-info → mlrun-1.7.0rc20.dist-info}/LICENSE +0 -0
  199. {mlrun-1.7.0rc4.dist-info → mlrun-1.7.0rc20.dist-info}/entry_points.txt +0 -0
  200. {mlrun-1.7.0rc4.dist-info → mlrun-1.7.0rc20.dist-info}/top_level.txt +0 -0
mlrun/projects/project.py CHANGED
@@ -11,6 +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
15
  import datetime
15
16
  import getpass
16
17
  import glob
@@ -30,20 +31,29 @@ from typing import Callable, Optional, Union
30
31
  import dotenv
31
32
  import git
32
33
  import git.exc
33
- import kfp
34
- import nuclio
34
+ import mlrun_pipelines.common.models
35
+ import mlrun_pipelines.mounts
36
+ import nuclio.utils
35
37
  import requests
36
38
  import yaml
39
+ from mlrun_pipelines.models import PipelineNodeWrapper
37
40
 
38
41
  import mlrun.common.helpers
42
+ import mlrun.common.runtimes.constants
43
+ import mlrun.common.schemas.artifact
39
44
  import mlrun.common.schemas.model_monitoring.constants as mm_constants
40
45
  import mlrun.db
41
46
  import mlrun.errors
42
47
  import mlrun.k8s_utils
48
+ import mlrun.model_monitoring.applications as mm_app
43
49
  import mlrun.runtimes
50
+ import mlrun.runtimes.nuclio.api_gateway
44
51
  import mlrun.runtimes.pod
45
52
  import mlrun.runtimes.utils
53
+ import mlrun.serving
46
54
  import mlrun.utils.regex
55
+ from mlrun.alerts.alert import AlertConfig
56
+ from mlrun.common.schemas.alert import AlertTemplate
47
57
  from mlrun.datastore.datastore_profile import DatastoreProfile, DatastoreProfile2Json
48
58
  from mlrun.runtimes.nuclio.function import RemoteRuntime
49
59
 
@@ -52,15 +62,10 @@ from ..artifacts.manager import ArtifactManager, dict_to_artifact, extend_artifa
52
62
  from ..datastore import store_manager
53
63
  from ..features import Feature
54
64
  from ..model import EntrypointParam, ImageBuilder, ModelObj
55
- from ..model_monitoring.application import (
56
- ModelMonitoringApplicationBase,
57
- PushToMonitoringWriter,
58
- )
59
65
  from ..run import code_to_function, get_object, import_function, new_function
60
66
  from ..secrets import SecretsStore
61
67
  from ..utils import (
62
68
  is_ipython,
63
- is_legacy_artifact,
64
69
  is_relative_path,
65
70
  is_yaml_path,
66
71
  logger,
@@ -73,7 +78,10 @@ from ..utils.clones import (
73
78
  clone_zip,
74
79
  get_repo_url,
75
80
  )
76
- from ..utils.helpers import ensure_git_branch, resolve_git_reference_from_source
81
+ from ..utils.helpers import (
82
+ ensure_git_branch,
83
+ resolve_git_reference_from_source,
84
+ )
77
85
  from ..utils.notifications import CustomNotificationPusher, NotificationTypes
78
86
  from .operations import (
79
87
  BuildStatus,
@@ -127,6 +135,7 @@ def new_project(
127
135
  save: bool = True,
128
136
  overwrite: bool = False,
129
137
  parameters: dict = None,
138
+ default_function_node_selector: dict = None,
130
139
  ) -> "MlrunProject":
131
140
  """Create a new MLRun project, optionally load it from a yaml/zip/git template
132
141
 
@@ -137,11 +146,15 @@ def new_project(
137
146
  example::
138
147
 
139
148
  # create a project with local and hub functions, a workflow, and an artifact
140
- project = mlrun.new_project("myproj", "./", init_git=True, description="my new project")
141
- project.set_function('prep_data.py', 'prep-data', image='mlrun/mlrun', handler='prep_data')
142
- project.set_function('hub://auto-trainer', 'train')
143
- project.set_artifact('data', Artifact(target_path=data_url))
144
- project.set_workflow('main', "./myflow.py")
149
+ project = mlrun.new_project(
150
+ "myproj", "./", init_git=True, description="my new project"
151
+ )
152
+ project.set_function(
153
+ "prep_data.py", "prep-data", image="mlrun/mlrun", handler="prep_data"
154
+ )
155
+ project.set_function("hub://auto-trainer", "train")
156
+ project.set_artifact("data", Artifact(target_path=data_url))
157
+ project.set_workflow("main", "./myflow.py")
145
158
  project.save()
146
159
 
147
160
  # run the "main" workflow (watch=True to wait for run completion)
@@ -151,19 +164,25 @@ def new_project(
151
164
 
152
165
  # create a new project from a zip template (can also use yaml/git templates)
153
166
  # initialize a local git, and register the git remote path
154
- project = mlrun.new_project("myproj", "./", init_git=True,
155
- remote="git://github.com/mlrun/project-demo.git",
156
- from_template="http://mysite/proj.zip")
167
+ project = mlrun.new_project(
168
+ "myproj",
169
+ "./",
170
+ init_git=True,
171
+ remote="git://github.com/mlrun/project-demo.git",
172
+ from_template="http://mysite/proj.zip",
173
+ )
157
174
  project.run("main", watch=True)
158
175
 
159
176
 
160
177
  example using project_setup.py to init the project objects::
161
178
 
162
179
  def setup(project):
163
- project.set_function('prep_data.py', 'prep-data', image='mlrun/mlrun', handler='prep_data')
164
- project.set_function('hub://auto-trainer', 'train')
165
- project.set_artifact('data', Artifact(target_path=data_url))
166
- project.set_workflow('main', "./myflow.py")
180
+ project.set_function(
181
+ "prep_data.py", "prep-data", image="mlrun/mlrun", handler="prep_data"
182
+ )
183
+ project.set_function("hub://auto-trainer", "train")
184
+ project.set_artifact("data", Artifact(target_path=data_url))
185
+ project.set_workflow("main", "./myflow.py")
167
186
  return project
168
187
 
169
188
 
@@ -180,6 +199,7 @@ def new_project(
180
199
  :param overwrite: overwrite project using 'cascade' deletion strategy (deletes project resources)
181
200
  if project with name exists
182
201
  :param parameters: key/value pairs to add to the project.spec.params
202
+ :param default_function_node_selector: defines the default node selector for scheduling functions within the project
183
203
 
184
204
  :returns: project object
185
205
  """
@@ -192,14 +212,16 @@ def new_project(
192
212
  "Unsupported option, cannot use subpath argument with project templates"
193
213
  )
194
214
  if from_template.endswith(".yaml"):
195
- project = _load_project_file(from_template, name, secrets)
215
+ project = _load_project_file(
216
+ from_template, name, secrets, allow_cross_project=True
217
+ )
196
218
  elif from_template.startswith("git://"):
197
219
  clone_git(from_template, context, secrets, clone=True)
198
220
  shutil.rmtree(path.join(context, ".git"))
199
- project = _load_project_dir(context, name)
221
+ project = _load_project_dir(context, name, allow_cross_project=True)
200
222
  elif from_template.endswith(".zip"):
201
223
  clone_zip(from_template, context, secrets)
202
- project = _load_project_dir(context, name)
224
+ project = _load_project_dir(context, name, allow_cross_project=True)
203
225
  else:
204
226
  raise ValueError("template must be a path to .yaml or .zip file")
205
227
  project.metadata.name = name
@@ -226,6 +248,11 @@ def new_project(
226
248
  project.spec.origin_url = url
227
249
  if description:
228
250
  project.spec.description = description
251
+
252
+ if default_function_node_selector:
253
+ for key, val in default_function_node_selector.items():
254
+ project.spec.default_function_node_selector[key] = val
255
+
229
256
  if parameters:
230
257
  # Enable setting project parameters at load time, can be used to customize the project_setup
231
258
  for key, val in parameters.items():
@@ -276,6 +303,7 @@ def load_project(
276
303
  save: bool = True,
277
304
  sync_functions: bool = False,
278
305
  parameters: dict = None,
306
+ allow_cross_project: bool = None,
279
307
  ) -> "MlrunProject":
280
308
  """Load an MLRun project from git or tar or dir
281
309
 
@@ -289,7 +317,7 @@ def load_project(
289
317
  # When using git as the url source the context directory must be an empty or
290
318
  # non-existent folder as the git repo will be cloned there
291
319
  project = load_project("./demo_proj", "git://github.com/mlrun/project-demo.git")
292
- project.run("main", arguments={'data': data_url})
320
+ project.run("main", arguments={"data": data_url})
293
321
 
294
322
 
295
323
  project_setup.py example::
@@ -322,6 +350,8 @@ def load_project(
322
350
  :param save: whether to save the created project and artifact in the DB
323
351
  :param sync_functions: sync the project's functions into the project object (will be saved to the DB if save=True)
324
352
  :param parameters: key/value pairs to add to the project.spec.params
353
+ :param allow_cross_project: if True, override the loaded project name. This flag ensures awareness of
354
+ loading an existing project yaml as a baseline for a new project with a different name
325
355
 
326
356
  :returns: project object
327
357
  """
@@ -337,7 +367,7 @@ def load_project(
337
367
  if url:
338
368
  url = str(url) # to support path objects
339
369
  if is_yaml_path(url):
340
- project = _load_project_file(url, name, secrets)
370
+ project = _load_project_file(url, name, secrets, allow_cross_project)
341
371
  project.spec.context = context
342
372
  elif url.startswith("git://"):
343
373
  url, repo = clone_git(url, context, secrets, clone)
@@ -364,7 +394,7 @@ def load_project(
364
394
  repo, url = init_repo(context, url, init_git)
365
395
 
366
396
  if not project:
367
- project = _load_project_dir(context, name, subpath)
397
+ project = _load_project_dir(context, name, subpath, allow_cross_project)
368
398
 
369
399
  if not project.metadata.name:
370
400
  raise ValueError("Project name must be specified")
@@ -418,6 +448,7 @@ def get_or_create_project(
418
448
  from_template: str = None,
419
449
  save: bool = True,
420
450
  parameters: dict = None,
451
+ allow_cross_project: bool = None,
421
452
  ) -> "MlrunProject":
422
453
  """Load a project from MLRun DB, or create/import if it does not exist
423
454
 
@@ -428,9 +459,11 @@ def get_or_create_project(
428
459
  Usage example::
429
460
 
430
461
  # load project from the DB (if exist) or the source repo
431
- project = get_or_create_project("myproj", "./", "git://github.com/mlrun/demo-xgb-project.git")
462
+ project = get_or_create_project(
463
+ "myproj", "./", "git://github.com/mlrun/demo-xgb-project.git"
464
+ )
432
465
  project.pull("development") # pull the latest code from git
433
- project.run("main", arguments={'data': data_url}) # run the workflow "main"
466
+ project.run("main", arguments={"data": data_url}) # run the workflow "main"
434
467
 
435
468
 
436
469
  project_setup.py example::
@@ -460,12 +493,12 @@ def get_or_create_project(
460
493
  :param from_template: path to project YAML file that will be used as from_template (for new projects)
461
494
  :param save: whether to save the created project in the DB
462
495
  :param parameters: key/value pairs to add to the project.spec.params
496
+ :param allow_cross_project: if True, override the loaded project name. This flag ensures awareness of
497
+ loading an existing project yaml as a baseline for a new project with a different name
463
498
 
464
499
  :returns: project object
465
500
  """
466
501
  context = context or "./"
467
- spec_path = path.join(context, subpath or "", "project.yaml")
468
- load_from_path = url or path.isfile(spec_path)
469
502
  try:
470
503
  # load project from the DB.
471
504
  # use `name` as `url` as we load the project from the DB
@@ -481,13 +514,15 @@ def get_or_create_project(
481
514
  # only loading project from db so no need to save it
482
515
  save=False,
483
516
  parameters=parameters,
517
+ allow_cross_project=allow_cross_project,
484
518
  )
485
519
  logger.info("Project loaded successfully", project_name=name)
486
520
  return project
487
-
488
521
  except mlrun.errors.MLRunNotFoundError:
489
522
  logger.debug("Project not found in db", project_name=name)
490
523
 
524
+ spec_path = path.join(context, subpath or "", "project.yaml")
525
+ load_from_path = url or path.isfile(spec_path)
491
526
  # do not nest under "try" or else the exceptions raised below will be logged along with the "not found" message
492
527
  if load_from_path:
493
528
  # loads a project from archive or local project.yaml
@@ -503,6 +538,7 @@ def get_or_create_project(
503
538
  user_project=user_project,
504
539
  save=save,
505
540
  parameters=parameters,
541
+ allow_cross_project=allow_cross_project,
506
542
  )
507
543
 
508
544
  logger.info(
@@ -577,7 +613,7 @@ def _run_project_setup(
577
613
  return project
578
614
 
579
615
 
580
- def _load_project_dir(context, name="", subpath=""):
616
+ def _load_project_dir(context, name="", subpath="", allow_cross_project=None):
581
617
  subpath_str = subpath or ""
582
618
 
583
619
  # support both .yaml and .yml file extensions
@@ -591,7 +627,7 @@ def _load_project_dir(context, name="", subpath=""):
591
627
  with open(project_file_path) as fp:
592
628
  data = fp.read()
593
629
  struct = yaml.load(data, Loader=yaml.FullLoader)
594
- project = _project_instance_from_struct(struct, name)
630
+ project = _project_instance_from_struct(struct, name, allow_cross_project)
595
631
  project.spec.context = context
596
632
  elif function_files := glob.glob(function_file_path):
597
633
  function_path = function_files[0]
@@ -664,19 +700,41 @@ def _delete_project_from_db(project_name, secrets, deletion_strategy):
664
700
  return db.delete_project(project_name, deletion_strategy=deletion_strategy)
665
701
 
666
702
 
667
- def _load_project_file(url, name="", secrets=None):
703
+ def _load_project_file(url, name="", secrets=None, allow_cross_project=None):
668
704
  try:
669
705
  obj = get_object(url, secrets)
670
706
  except FileNotFoundError as exc:
671
707
  raise FileNotFoundError(f"cant find project file at {url}") from exc
672
708
  struct = yaml.load(obj, Loader=yaml.FullLoader)
673
- return _project_instance_from_struct(struct, name)
709
+ return _project_instance_from_struct(struct, name, allow_cross_project)
674
710
 
675
711
 
676
- def _project_instance_from_struct(struct, name):
677
- struct.setdefault("metadata", {})["name"] = name or struct.get("metadata", {}).get(
678
- "name", ""
679
- )
712
+ def _project_instance_from_struct(struct, name, allow_cross_project):
713
+ name_from_struct = struct.get("metadata", {}).get("name", "")
714
+ if name and name_from_struct and name_from_struct != name:
715
+ error_message = (
716
+ f"project name mismatch, {name_from_struct} != {name}, please do one of the following:\n"
717
+ "1. Set the `allow_cross_project=True` when loading the project.\n"
718
+ f"2. Delete the existing project yaml, or ensure its name is equal to {name}.\n"
719
+ "3. Use different project context dir."
720
+ )
721
+
722
+ if allow_cross_project is None:
723
+ # TODO: Remove this warning in version 1.9.0 and also fix cli to support allow_cross_project
724
+ logger.warn(
725
+ "Project name is different than specified on its project yaml."
726
+ "You should fix it until version 1.9.0",
727
+ description=error_message,
728
+ )
729
+ elif allow_cross_project:
730
+ logger.warn(
731
+ "Project name is different than specified on its project yaml. Overriding.",
732
+ existing_name=name_from_struct,
733
+ overriding_name=name,
734
+ )
735
+ else:
736
+ raise ValueError(error_message)
737
+ struct.setdefault("metadata", {})["name"] = name or name_from_struct
680
738
  return MlrunProject.from_dict(struct)
681
739
 
682
740
 
@@ -759,6 +817,7 @@ class ProjectSpec(ModelObj):
759
817
  default_image=None,
760
818
  build=None,
761
819
  custom_packagers: list[tuple[str, bool]] = None,
820
+ default_function_node_selector=None,
762
821
  ):
763
822
  self.repo = None
764
823
 
@@ -798,6 +857,7 @@ class ProjectSpec(ModelObj):
798
857
  # in a tuple where the first index is the packager module's path (str) and the second is a flag (bool) for
799
858
  # whether it is mandatory for a run (raise exception on collection error) or not.
800
859
  self.custom_packagers = custom_packagers or []
860
+ self.default_function_node_selector = default_function_node_selector or {}
801
861
 
802
862
  @property
803
863
  def source(self) -> str:
@@ -936,13 +996,9 @@ class ProjectSpec(ModelObj):
936
996
  if not isinstance(artifact, dict) and not hasattr(artifact, "to_dict"):
937
997
  raise ValueError("artifacts must be a dict or class")
938
998
  if isinstance(artifact, dict):
939
- # Support legacy artifacts
940
- if is_legacy_artifact(artifact) or _is_imported_artifact(artifact):
941
- key = artifact.get("key")
942
- else:
943
- key = artifact.get("metadata").get("key", "")
999
+ key = artifact.get("metadata", {}).get("key", "")
944
1000
  if not key:
945
- raise ValueError('artifacts "key" must be specified')
1001
+ raise ValueError('artifacts "metadata.key" must be specified')
946
1002
  else:
947
1003
  key = artifact.key
948
1004
  artifact = artifact.to_dict()
@@ -1219,6 +1275,14 @@ class MlrunProject(ModelObj):
1219
1275
  def description(self, description):
1220
1276
  self.spec.description = description
1221
1277
 
1278
+ @property
1279
+ def default_function_node_selector(self) -> dict:
1280
+ return self.spec.default_function_node_selector
1281
+
1282
+ @default_function_node_selector.setter
1283
+ def default_function_node_selector(self, default_function_node_selector):
1284
+ self.spec.default_function_node_selector = default_function_node_selector
1285
+
1222
1286
  @property
1223
1287
  def default_image(self) -> str:
1224
1288
  return self.spec.default_image
@@ -1333,13 +1397,15 @@ class MlrunProject(ModelObj):
1333
1397
  example::
1334
1398
 
1335
1399
  # register a simple file artifact
1336
- project.set_artifact('data', target_path=data_url)
1400
+ project.set_artifact("data", target_path=data_url)
1337
1401
  # register a model artifact
1338
- project.set_artifact('model', ModelArtifact(model_file="model.pkl"), target_path=model_dir_url)
1402
+ project.set_artifact(
1403
+ "model", ModelArtifact(model_file="model.pkl"), target_path=model_dir_url
1404
+ )
1339
1405
 
1340
1406
  # register a path to artifact package (will be imported on project load)
1341
1407
  # to generate such package use `artifact.export(target_path)`
1342
- project.set_artifact('model', 'https://mystuff.com/models/mymodel.zip')
1408
+ project.set_artifact("model", "https://mystuff.com/models/mymodel.zip")
1343
1409
 
1344
1410
  :param key: artifact key/name
1345
1411
  :param artifact: mlrun Artifact object/dict (or its subclasses) or path to artifact
@@ -1374,14 +1440,7 @@ class MlrunProject(ModelObj):
1374
1440
  artifact_path = mlrun.utils.helpers.template_artifact_path(
1375
1441
  self.spec.artifact_path or mlrun.mlconf.artifact_path, self.metadata.name
1376
1442
  )
1377
- # TODO: To correctly maintain the list of artifacts from an exported project,
1378
- # we need to maintain the different trees that generated them
1379
- producer = ArtifactProducer(
1380
- "project",
1381
- self.metadata.name,
1382
- self.metadata.name,
1383
- tag=self._get_hexsha() or str(uuid.uuid4()),
1384
- )
1443
+ project_tag = self._get_project_tag()
1385
1444
  for artifact_dict in self.spec.artifacts:
1386
1445
  if _is_imported_artifact(artifact_dict):
1387
1446
  import_from = artifact_dict["import_from"]
@@ -1401,8 +1460,23 @@ class MlrunProject(ModelObj):
1401
1460
  artifact.src_path = path.join(
1402
1461
  self.spec.get_code_path(), artifact.src_path
1403
1462
  )
1463
+ producer, is_retained_producer = self._resolve_artifact_producer(
1464
+ artifact, project_tag
1465
+ )
1466
+ # log the artifact only if it doesn't already exist
1467
+ if (
1468
+ producer.name != self.metadata.name
1469
+ and self._resolve_existing_artifact(
1470
+ artifact,
1471
+ )
1472
+ ):
1473
+ continue
1404
1474
  artifact_manager.log_artifact(
1405
- producer, artifact, artifact_path=artifact_path
1475
+ producer,
1476
+ artifact,
1477
+ artifact_path=artifact_path,
1478
+ project=self.metadata.name,
1479
+ is_retained_producer=is_retained_producer,
1406
1480
  )
1407
1481
 
1408
1482
  def _get_artifact_manager(self):
@@ -1497,12 +1571,20 @@ class MlrunProject(ModelObj):
1497
1571
  artifact_path = mlrun.utils.helpers.template_artifact_path(
1498
1572
  artifact_path, self.metadata.name
1499
1573
  )
1500
- producer = ArtifactProducer(
1501
- "project",
1502
- self.metadata.name,
1503
- self.metadata.name,
1504
- tag=self._get_hexsha() or str(uuid.uuid4()),
1505
- )
1574
+ producer, is_retained_producer = self._resolve_artifact_producer(item)
1575
+ if producer.name != self.metadata.name:
1576
+ # the artifact producer is retained, log it only if it doesn't already exist
1577
+ if existing_artifact := self._resolve_existing_artifact(
1578
+ item,
1579
+ tag,
1580
+ ):
1581
+ artifact_key = item if isinstance(item, str) else item.key
1582
+ logger.info(
1583
+ "Artifact already exists, skipping logging",
1584
+ key=artifact_key,
1585
+ tag=tag,
1586
+ )
1587
+ return existing_artifact
1506
1588
  item = am.log_artifact(
1507
1589
  producer,
1508
1590
  item,
@@ -1514,10 +1596,29 @@ class MlrunProject(ModelObj):
1514
1596
  upload=upload,
1515
1597
  labels=labels,
1516
1598
  target_path=target_path,
1599
+ project=self.metadata.name,
1600
+ is_retained_producer=is_retained_producer,
1517
1601
  **kwargs,
1518
1602
  )
1519
1603
  return item
1520
1604
 
1605
+ def delete_artifact(
1606
+ self,
1607
+ item: Artifact,
1608
+ deletion_strategy: mlrun.common.schemas.artifact.ArtifactsDeletionStrategies = (
1609
+ mlrun.common.schemas.artifact.ArtifactsDeletionStrategies.metadata_only
1610
+ ),
1611
+ secrets: dict = None,
1612
+ ):
1613
+ """Delete an artifact object in the DB and optionally delete the artifact data
1614
+
1615
+ :param item: Artifact object (can be any type, such as dataset, model, feature store).
1616
+ :param deletion_strategy: The artifact deletion strategy types.
1617
+ :param secrets: Credentials needed to access the artifact data.
1618
+ """
1619
+ am = self._get_artifact_manager()
1620
+ am.delete_artifact(item, deletion_strategy, secrets)
1621
+
1521
1622
  def log_dataset(
1522
1623
  self,
1523
1624
  key,
@@ -1548,7 +1649,9 @@ class MlrunProject(ModelObj):
1548
1649
  "age": [42, 52, 36, 24, 73],
1549
1650
  "testScore": [25, 94, 57, 62, 70],
1550
1651
  }
1551
- df = pd.DataFrame(raw_data, columns=["first_name", "last_name", "age", "testScore"])
1652
+ df = pd.DataFrame(
1653
+ raw_data, columns=["first_name", "last_name", "age", "testScore"]
1654
+ )
1552
1655
  project.log_dataset("mydf", df=df, stats=True)
1553
1656
 
1554
1657
  :param key: artifact key
@@ -1622,13 +1725,16 @@ class MlrunProject(ModelObj):
1622
1725
 
1623
1726
  example::
1624
1727
 
1625
- project.log_model("model", body=dumps(model),
1626
- model_file="model.pkl",
1627
- metrics=context.results,
1628
- training_set=training_df,
1629
- label_column='label',
1630
- feature_vector=feature_vector_uri,
1631
- labels={"app": "fraud"})
1728
+ project.log_model(
1729
+ "model",
1730
+ body=dumps(model),
1731
+ model_file="model.pkl",
1732
+ metrics=context.results,
1733
+ training_set=training_df,
1734
+ label_column="label",
1735
+ feature_vector=feature_vector_uri,
1736
+ labels={"app": "fraud"},
1737
+ )
1632
1738
 
1633
1739
  :param key: artifact key or artifact class ()
1634
1740
  :param body: will use the body as the artifact content
@@ -1738,14 +1844,16 @@ class MlrunProject(ModelObj):
1738
1844
  artifact = get_artifact(spec)
1739
1845
  with open(f"{temp_dir}/_body", "rb") as fp:
1740
1846
  artifact.spec._body = fp.read()
1741
- artifact.target_path = ""
1742
1847
 
1743
1848
  # if the dataitem is not a file, it means we downloaded it from a remote source to a temp file,
1744
1849
  # so we need to remove it after we're done with it
1745
1850
  dataitem.remove_local()
1746
1851
 
1747
1852
  return self.log_artifact(
1748
- artifact, local_path=temp_dir, artifact_path=artifact_path
1853
+ artifact,
1854
+ local_path=temp_dir,
1855
+ artifact_path=artifact_path,
1856
+ upload=True,
1749
1857
  )
1750
1858
 
1751
1859
  else:
@@ -1763,10 +1871,18 @@ class MlrunProject(ModelObj):
1763
1871
  """
1764
1872
  context = context or self.spec.context
1765
1873
  if context:
1766
- project = _load_project_dir(context, self.metadata.name, self.spec.subpath)
1874
+ project = _load_project_dir(
1875
+ context,
1876
+ self.metadata.name,
1877
+ self.spec.subpath,
1878
+ allow_cross_project=False,
1879
+ )
1767
1880
  else:
1768
1881
  project = _load_project_file(
1769
- self.spec.origin_url, self.metadata.name, self._secrets
1882
+ self.spec.origin_url,
1883
+ self.metadata.name,
1884
+ self._secrets,
1885
+ allow_cross_project=None,
1770
1886
  )
1771
1887
  project.spec.source = self.spec.source
1772
1888
  project.spec.repo = self.spec.repo
@@ -1795,7 +1911,11 @@ class MlrunProject(ModelObj):
1795
1911
  def set_model_monitoring_function(
1796
1912
  self,
1797
1913
  func: typing.Union[str, mlrun.runtimes.BaseRuntime, None] = None,
1798
- application_class: typing.Union[str, ModelMonitoringApplicationBase] = None,
1914
+ application_class: typing.Union[
1915
+ str,
1916
+ mm_app.ModelMonitoringApplicationBase,
1917
+ mm_app.ModelMonitoringApplicationBaseV2,
1918
+ ] = None,
1799
1919
  name: str = None,
1800
1920
  image: str = None,
1801
1921
  handler=None,
@@ -1833,11 +1953,6 @@ class MlrunProject(ModelObj):
1833
1953
  monitoring application's constructor.
1834
1954
  """
1835
1955
 
1836
- if name in mm_constants.MonitoringFunctionNames.all():
1837
- raise mlrun.errors.MLRunInvalidArgumentError(
1838
- f"Application name can not be on of the following name : "
1839
- f"{mm_constants.MonitoringFunctionNames.all()}"
1840
- )
1841
1956
  function_object: RemoteRuntime = None
1842
1957
  (
1843
1958
  resolved_function_name,
@@ -1855,16 +1970,6 @@ class MlrunProject(ModelObj):
1855
1970
  requirements_file,
1856
1971
  **application_kwargs,
1857
1972
  )
1858
- models_names = "all"
1859
- function_object.set_label(
1860
- mm_constants.ModelMonitoringAppLabel.KEY,
1861
- mm_constants.ModelMonitoringAppLabel.VAL,
1862
- )
1863
- function_object.set_label("models", models_names)
1864
-
1865
- if not mlrun.mlconf.is_ce_mode():
1866
- function_object.apply(mlrun.mount_v3io())
1867
-
1868
1973
  # save to project spec
1869
1974
  self.spec.set_function(resolved_function_name, function_object, func)
1870
1975
 
@@ -1873,7 +1978,11 @@ class MlrunProject(ModelObj):
1873
1978
  def create_model_monitoring_function(
1874
1979
  self,
1875
1980
  func: str = None,
1876
- application_class: typing.Union[str, ModelMonitoringApplicationBase] = None,
1981
+ application_class: typing.Union[
1982
+ str,
1983
+ mm_app.ModelMonitoringApplicationBase,
1984
+ mm_app.ModelMonitoringApplicationBaseV2,
1985
+ ] = None,
1877
1986
  name: str = None,
1878
1987
  image: str = None,
1879
1988
  handler: str = None,
@@ -1887,8 +1996,9 @@ class MlrunProject(ModelObj):
1887
1996
  Create a monitoring function object without setting it to the project
1888
1997
 
1889
1998
  examples::
1890
- project.create_model_monitoring_function(application_class_name="MyApp",
1891
- image="mlrun/mlrun", name="myApp")
1999
+ project.create_model_monitoring_function(
2000
+ application_class_name="MyApp", image="mlrun/mlrun", name="myApp"
2001
+ )
1892
2002
 
1893
2003
  :param func: Code url, None refers to current Notebook
1894
2004
  :param name: Name of the function, can be specified with a tag to support
@@ -1923,49 +2033,41 @@ class MlrunProject(ModelObj):
1923
2033
 
1924
2034
  def _instantiate_model_monitoring_function(
1925
2035
  self,
1926
- func: typing.Union[str, mlrun.runtimes.BaseRuntime] = None,
1927
- application_class: typing.Union[str, ModelMonitoringApplicationBase] = None,
1928
- name: str = None,
1929
- image: str = None,
1930
- handler: str = None,
1931
- with_repo: bool = None,
1932
- tag: str = None,
1933
- requirements: typing.Union[str, list[str]] = None,
2036
+ func: typing.Union[str, mlrun.runtimes.BaseRuntime, None] = None,
2037
+ application_class: typing.Union[
2038
+ str,
2039
+ mm_app.ModelMonitoringApplicationBase,
2040
+ mm_app.ModelMonitoringApplicationBaseV2,
2041
+ None,
2042
+ ] = None,
2043
+ name: typing.Optional[str] = None,
2044
+ image: typing.Optional[str] = None,
2045
+ handler: typing.Optional[str] = None,
2046
+ with_repo: typing.Optional[bool] = None,
2047
+ tag: typing.Optional[str] = None,
2048
+ requirements: typing.Union[str, list[str], None] = None,
1934
2049
  requirements_file: str = "",
1935
2050
  **application_kwargs,
1936
2051
  ) -> tuple[str, mlrun.runtimes.BaseRuntime, dict]:
2052
+ import mlrun.model_monitoring.api
2053
+
1937
2054
  function_object: RemoteRuntime = None
1938
2055
  kind = None
1939
2056
  if (isinstance(func, str) or func is None) and application_class is not None:
1940
- kind = "serving"
1941
- if func is None:
1942
- func = ""
1943
- func = mlrun.code_to_function(
1944
- filename=func,
2057
+ kind = mlrun.run.RuntimeKinds.serving
2058
+ func = mlrun.model_monitoring.api._create_model_monitoring_function_base(
2059
+ project=self.name,
2060
+ func=func,
2061
+ application_class=application_class,
1945
2062
  name=name,
1946
- project=self.metadata.name,
1947
- tag=tag,
1948
- kind=kind,
1949
2063
  image=image,
2064
+ tag=tag,
1950
2065
  requirements=requirements,
1951
2066
  requirements_file=requirements_file,
2067
+ **application_kwargs,
1952
2068
  )
1953
- graph = func.set_topology("flow")
1954
- if isinstance(application_class, str):
1955
- first_step = graph.to(
1956
- class_name=application_class, **application_kwargs
1957
- )
1958
- else:
1959
- first_step = graph.to(class_name=application_class)
1960
- first_step.to(
1961
- class_name=PushToMonitoringWriter(
1962
- project=self.metadata.name,
1963
- writer_application_name=mm_constants.MonitoringFunctionNames.WRITER,
1964
- stream_uri=None,
1965
- ),
1966
- ).respond()
1967
2069
  elif isinstance(func, str) and isinstance(handler, str):
1968
- kind = "nuclio"
2070
+ kind = mlrun.run.RuntimeKinds.nuclio
1969
2071
 
1970
2072
  (
1971
2073
  resolved_function_name,
@@ -1983,24 +2085,34 @@ class MlrunProject(ModelObj):
1983
2085
  requirements,
1984
2086
  requirements_file,
1985
2087
  )
1986
- models_names = "all"
1987
2088
  function_object.set_label(
1988
2089
  mm_constants.ModelMonitoringAppLabel.KEY,
1989
2090
  mm_constants.ModelMonitoringAppLabel.VAL,
1990
2091
  )
1991
- function_object.set_label("models", models_names)
1992
2092
 
1993
2093
  if not mlrun.mlconf.is_ce_mode():
1994
2094
  function_object.apply(mlrun.mount_v3io())
1995
2095
 
1996
2096
  return resolved_function_name, function_object, func
1997
2097
 
2098
+ def _wait_for_functions_deployment(self, function_names: list[str]) -> None:
2099
+ """
2100
+ Wait for the deployment of functions on the backend.
2101
+
2102
+ :param function_names: A list of function names.
2103
+ """
2104
+ for fn_name in function_names:
2105
+ fn = typing.cast(RemoteRuntime, self.get_function(key=fn_name))
2106
+ fn._wait_for_function_deployment(db=fn._get_db())
2107
+
1998
2108
  def enable_model_monitoring(
1999
2109
  self,
2000
2110
  default_controller_image: str = "mlrun/mlrun",
2001
2111
  base_period: int = 10,
2002
2112
  image: str = "mlrun/mlrun",
2113
+ *,
2003
2114
  deploy_histogram_data_drift_app: bool = True,
2115
+ wait_for_deployment: bool = False,
2004
2116
  ) -> None:
2005
2117
  """
2006
2118
  Deploy model monitoring application controller, writer and stream functions.
@@ -2010,7 +2122,6 @@ class MlrunProject(ModelObj):
2010
2122
  The stream function goal is to monitor the log of the data stream. It is triggered when a new log entry
2011
2123
  is detected. It processes the new events into statistics that are then written to statistics databases.
2012
2124
 
2013
-
2014
2125
  :param default_controller_image: Deprecated.
2015
2126
  :param base_period: The time period in minutes in which the model monitoring controller
2016
2127
  function is triggered. By default, the base period is 10 minutes.
@@ -2018,8 +2129,9 @@ class MlrunProject(ModelObj):
2018
2129
  stream & histogram data drift functions, which are real time nuclio
2019
2130
  functions. By default, the image is mlrun/mlrun.
2020
2131
  :param deploy_histogram_data_drift_app: If true, deploy the default histogram-based data drift application.
2021
-
2022
- :returns: model monitoring controller job as a dictionary.
2132
+ :param wait_for_deployment: If true, return only after the deployment is done on the backend.
2133
+ Otherwise, deploy the model monitoring infrastructure on the
2134
+ background, including the histogram data drift app if selected.
2023
2135
  """
2024
2136
  if default_controller_image != "mlrun/mlrun":
2025
2137
  # TODO: Remove this in 1.9.0
@@ -2034,34 +2146,58 @@ class MlrunProject(ModelObj):
2034
2146
  project=self.name,
2035
2147
  image=image,
2036
2148
  base_period=base_period,
2149
+ deploy_histogram_data_drift_app=deploy_histogram_data_drift_app,
2037
2150
  )
2038
- if deploy_histogram_data_drift_app:
2039
- fn = self.set_model_monitoring_function(
2040
- func=str(
2041
- pathlib.Path(__file__).parent.parent
2042
- / "model_monitoring/applications/histogram_data_drift.py"
2043
- ),
2044
- name=mm_constants.MLRUN_HISTOGRAM_DATA_DRIFT_APP_NAME,
2045
- application_class="HistogramDataDriftApplication",
2046
- image=image,
2151
+
2152
+ if wait_for_deployment:
2153
+ deployment_functions = mm_constants.MonitoringFunctionNames.list()
2154
+ if deploy_histogram_data_drift_app:
2155
+ deployment_functions.append(
2156
+ mm_constants.HistogramDataDriftApplicationConstants.NAME
2157
+ )
2158
+ self._wait_for_functions_deployment(deployment_functions)
2159
+
2160
+ def deploy_histogram_data_drift_app(
2161
+ self,
2162
+ *,
2163
+ image: str = "mlrun/mlrun",
2164
+ db: Optional[mlrun.db.RunDBInterface] = None,
2165
+ wait_for_deployment: bool = False,
2166
+ ) -> None:
2167
+ """
2168
+ Deploy the histogram data drift application.
2169
+
2170
+ :param image: The image on which the application will run.
2171
+ :param db: An optional DB object.
2172
+ :param wait_for_deployment: If true, return only after the deployment is done on the backend.
2173
+ Otherwise, deploy the application on the background.
2174
+ """
2175
+ if db is None:
2176
+ db = mlrun.db.get_run_db(secrets=self._secrets)
2177
+ db.deploy_histogram_data_drift_app(project=self.name, image=image)
2178
+
2179
+ if wait_for_deployment:
2180
+ self._wait_for_functions_deployment(
2181
+ [mm_constants.HistogramDataDriftApplicationConstants.NAME]
2047
2182
  )
2048
- fn.deploy()
2049
2183
 
2050
2184
  def update_model_monitoring_controller(
2051
2185
  self,
2052
2186
  base_period: int = 10,
2053
2187
  image: str = "mlrun/mlrun",
2188
+ *,
2189
+ wait_for_deployment: bool = False,
2054
2190
  ) -> None:
2055
2191
  """
2056
2192
  Redeploy model monitoring application controller functions.
2057
2193
 
2058
-
2059
- :param base_period: The time period in minutes in which the model monitoring controller function
2060
- is triggered. By default, the base period is 10 minutes.
2061
- :param image: The image of the model monitoring controller, writer & monitoring
2062
- stream functions, which are real time nuclio functions.
2063
- By default, the image is mlrun/mlrun.
2064
- :returns: model monitoring controller job as a dictionary.
2194
+ :param base_period: The time period in minutes in which the model monitoring controller function
2195
+ is triggered. By default, the base period is 10 minutes.
2196
+ :param image: The image of the model monitoring controller, writer & monitoring
2197
+ stream functions, which are real time nuclio functions.
2198
+ By default, the image is mlrun/mlrun.
2199
+ :param wait_for_deployment: If true, return only after the deployment is done on the backend.
2200
+ Otherwise, deploy the controller on the background.
2065
2201
  """
2066
2202
  db = mlrun.db.get_run_db(secrets=self._secrets)
2067
2203
  db.update_model_monitoring_controller(
@@ -2070,26 +2206,34 @@ class MlrunProject(ModelObj):
2070
2206
  image=image,
2071
2207
  )
2072
2208
 
2073
- def disable_model_monitoring(self):
2209
+ if wait_for_deployment:
2210
+ self._wait_for_functions_deployment(
2211
+ [mm_constants.MonitoringFunctionNames.APPLICATION_CONTROLLER]
2212
+ )
2213
+
2214
+ def disable_model_monitoring(
2215
+ self, *, delete_histogram_data_drift_app: bool = True
2216
+ ) -> None:
2217
+ """
2218
+ Note: This method is currently not advised for use. See ML-3432.
2219
+ Disable model monitoring by deleting the underlying functions infrastructure from MLRun database.
2220
+
2221
+ :param delete_histogram_data_drift_app: Whether to delete the histogram data drift app.
2222
+ """
2074
2223
  db = mlrun.db.get_run_db(secrets=self._secrets)
2075
- db.delete_function(
2076
- project=self.name,
2077
- name=mm_constants.MonitoringFunctionNames.APPLICATION_CONTROLLER,
2078
- )
2079
- db.delete_function(
2080
- project=self.name,
2081
- name=mm_constants.MonitoringFunctionNames.WRITER,
2082
- )
2083
- db.delete_function(
2084
- project=self.name,
2085
- name=mm_constants.MonitoringFunctionNames.STREAM,
2086
- )
2224
+ for fn_name in mm_constants.MonitoringFunctionNames.list():
2225
+ db.delete_function(project=self.name, name=fn_name)
2226
+ if delete_histogram_data_drift_app:
2227
+ db.delete_function(
2228
+ project=self.name,
2229
+ name=mm_constants.HistogramDataDriftApplicationConstants.NAME,
2230
+ )
2087
2231
 
2088
2232
  def set_function(
2089
2233
  self,
2090
2234
  func: typing.Union[str, mlrun.runtimes.BaseRuntime] = None,
2091
2235
  name: str = "",
2092
- kind: str = "",
2236
+ kind: str = "job",
2093
2237
  image: str = None,
2094
2238
  handler: str = None,
2095
2239
  with_repo: bool = None,
@@ -2109,19 +2253,20 @@ class MlrunProject(ModelObj):
2109
2253
  examples::
2110
2254
 
2111
2255
  proj.set_function(func_object)
2112
- proj.set_function('./src/mycode.py', 'ingest',
2113
- image='myrepo/ing:latest', with_repo=True)
2114
- proj.set_function('http://.../mynb.ipynb', 'train')
2115
- proj.set_function('./func.yaml')
2116
- proj.set_function('hub://get_toy_data', 'getdata')
2256
+ proj.set_function(
2257
+ "./src/mycode.py", "ingest", image="myrepo/ing:latest", with_repo=True
2258
+ )
2259
+ proj.set_function("http://.../mynb.ipynb", "train")
2260
+ proj.set_function("./func.yaml")
2261
+ proj.set_function("hub://get_toy_data", "getdata")
2117
2262
 
2118
2263
  # set function requirements
2119
2264
 
2120
2265
  # by providing a list of packages
2121
- proj.set_function('my.py', requirements=["requests", "pandas"])
2266
+ proj.set_function("my.py", requirements=["requests", "pandas"])
2122
2267
 
2123
2268
  # by providing a path to a pip requirements file
2124
- proj.set_function('my.py', requirements="requirements.txt")
2269
+ proj.set_function("my.py", requirements="requirements.txt")
2125
2270
 
2126
2271
  :param func: Function object or spec/code url, None refers to current Notebook
2127
2272
  :param name: Name of the function (under the project), can be specified with a tag to support
@@ -2176,16 +2321,13 @@ class MlrunProject(ModelObj):
2176
2321
  if func is None and not _has_module(handler, kind):
2177
2322
  # if function path is not provided and it is not a module (no ".")
2178
2323
  # use the current notebook as default
2179
- if not is_ipython:
2180
- raise ValueError(
2181
- "Function path or module must be specified (when not running inside a Notebook)"
2182
- )
2183
- from IPython import get_ipython
2324
+ if is_ipython:
2325
+ from IPython import get_ipython
2184
2326
 
2185
- kernel = get_ipython()
2186
- func = nuclio.utils.notebook_file_name(kernel)
2187
- if func.startswith(path.abspath(self.spec.context)):
2188
- func = path.relpath(func, self.spec.context)
2327
+ kernel = get_ipython()
2328
+ func = nuclio.utils.notebook_file_name(kernel)
2329
+ if func.startswith(path.abspath(self.spec.context)):
2330
+ func = path.relpath(func, self.spec.context)
2189
2331
 
2190
2332
  func = func or ""
2191
2333
 
@@ -2402,13 +2544,47 @@ class MlrunProject(ModelObj):
2402
2544
  clone_zip(url, self.spec.context, self._secrets)
2403
2545
 
2404
2546
  def create_remote(self, url, name="origin", branch=None):
2405
- """create remote for the project git
2547
+ """Create remote for the project git
2548
+
2549
+ This method creates a new remote repository associated with the project's Git repository.
2550
+ If a remote with the specified name already exists, it will not be overwritten.
2551
+
2552
+ If you wish to update the URL of an existing remote, use the `set_remote` method instead.
2406
2553
 
2407
2554
  :param url: remote git url
2408
2555
  :param name: name for the remote (default is 'origin')
2409
2556
  :param branch: Git branch to use as source
2410
2557
  """
2558
+ self.set_remote(url, name=name, branch=branch, overwrite=False)
2559
+
2560
+ def set_remote(self, url, name="origin", branch=None, overwrite=True):
2561
+ """Create or update a remote for the project git repository.
2562
+
2563
+ This method allows you to manage remote repositories associated with the project.
2564
+ It checks if a remote with the specified name already exists.
2565
+
2566
+ If a remote with the same name does not exist, it will be created.
2567
+ If a remote with the same name already exists,
2568
+ the behavior depends on the value of the 'overwrite' flag.
2569
+
2570
+ :param url: remote git url
2571
+ :param name: name for the remote (default is 'origin')
2572
+ :param branch: Git branch to use as source
2573
+ :param overwrite: if True (default), updates the existing remote with the given URL if it already exists.
2574
+ if False, raises an error when attempting to create a remote with a name that already exists.
2575
+ :raises MLRunConflictError: If a remote with the same name already exists and overwrite
2576
+ is set to False.
2577
+ """
2411
2578
  self._ensure_git_repo()
2579
+ if self._remote_exists(name):
2580
+ if overwrite:
2581
+ self.spec.repo.delete_remote(name)
2582
+ else:
2583
+ raise mlrun.errors.MLRunConflictError(
2584
+ f"Remote '{name}' already exists in the project, "
2585
+ f"each remote in the project must have a unique name."
2586
+ "Use 'set_remote' with 'override=True' inorder to update the remote, or choose a different name."
2587
+ )
2412
2588
  self.spec.repo.create_remote(name, url=url)
2413
2589
  url = url.replace("https://", "git://")
2414
2590
  if not branch:
@@ -2421,6 +2597,22 @@ class MlrunProject(ModelObj):
2421
2597
  self.spec._source = self.spec.source or url
2422
2598
  self.spec.origin_url = self.spec.origin_url or url
2423
2599
 
2600
+ def remove_remote(self, name):
2601
+ """Remove a remote from the project's Git repository.
2602
+
2603
+ This method removes the remote repository associated with the specified name from the project's Git repository.
2604
+
2605
+ :param name: Name of the remote to remove.
2606
+ """
2607
+ if self._remote_exists(name):
2608
+ self.spec.repo.delete_remote(name)
2609
+ else:
2610
+ logger.warning(f"The remote '{name}' does not exist. Nothing to remove.")
2611
+
2612
+ def _remote_exists(self, name):
2613
+ """Check if a remote with the given name already exists"""
2614
+ return any(remote.name == name for remote in self.spec.repo.remotes)
2615
+
2424
2616
  def _ensure_git_repo(self):
2425
2617
  if self.spec.repo:
2426
2618
  return
@@ -2552,9 +2744,9 @@ class MlrunProject(ModelObj):
2552
2744
 
2553
2745
  read secrets from a source provider to be used in workflows, example::
2554
2746
 
2555
- proj.with_secrets('file', 'file.txt')
2556
- proj.with_secrets('inline', {'key': 'val'})
2557
- proj.with_secrets('env', 'ENV1,ENV2', prefix='PFX_')
2747
+ proj.with_secrets("file", "file.txt")
2748
+ proj.with_secrets("inline", {"key": "val"})
2749
+ proj.with_secrets("env", "ENV1,ENV2", prefix="PFX_")
2558
2750
 
2559
2751
  Vault secret source has several options::
2560
2752
 
@@ -2565,7 +2757,7 @@ class MlrunProject(ModelObj):
2565
2757
  The 2nd option uses the current project name as context.
2566
2758
  Can also use empty secret list::
2567
2759
 
2568
- proj.with_secrets('vault', [])
2760
+ proj.with_secrets("vault", [])
2569
2761
 
2570
2762
  This will enable access to all secrets in vault registered to the current project.
2571
2763
 
@@ -2596,17 +2788,20 @@ class MlrunProject(ModelObj):
2596
2788
  file_path: str = None,
2597
2789
  provider: typing.Union[str, mlrun.common.schemas.SecretProviderName] = None,
2598
2790
  ):
2599
- """set project secrets from dict or secrets env file
2791
+ """
2792
+ Set project secrets from dict or secrets env file
2600
2793
  when using a secrets file it should have lines in the form KEY=VALUE, comment line start with "#"
2601
2794
  V3IO paths/credentials and MLrun service API address are dropped from the secrets
2602
2795
 
2603
- example secrets file::
2796
+ example secrets file:
2797
+
2798
+ .. code-block:: shell
2604
2799
 
2605
2800
  # this is an env file
2606
- AWS_ACCESS_KEY_ID-XXXX
2801
+ AWS_ACCESS_KEY_ID=XXXX
2607
2802
  AWS_SECRET_ACCESS_KEY=YYYY
2608
2803
 
2609
- usage::
2804
+ usage:
2610
2805
 
2611
2806
  # read env vars from dict or file and set as project secrets
2612
2807
  project.set_secrets({"SECRET1": "value"})
@@ -2686,40 +2881,42 @@ class MlrunProject(ModelObj):
2686
2881
  cleanup_ttl: int = None,
2687
2882
  notifications: list[mlrun.model.Notification] = None,
2688
2883
  ) -> _PipelineRunStatus:
2689
- """run a workflow using kubeflow pipelines
2690
-
2691
- :param name: name of the workflow
2692
- :param workflow_path:
2693
- url to a workflow file, if not a project workflow
2694
- :param arguments:
2695
- kubeflow pipelines arguments (parameters)
2696
- :param artifact_path:
2697
- target path/url for workflow artifacts, the string
2698
- '{{workflow.uid}}' will be replaced by workflow id
2699
- :param workflow_handler:
2700
- workflow function handler (for running workflow function directly)
2701
- :param namespace: kubernetes namespace if other than default
2702
- :param sync: force functions sync before run
2703
- :param watch: wait for pipeline completion
2704
- :param dirty: allow running the workflow when the git repo is dirty
2705
- :param engine: workflow engine running the workflow.
2706
- supported values are 'kfp' (default), 'local' or 'remote'.
2707
- for setting engine for remote running use 'remote:local' or 'remote:kfp'.
2708
- :param local: run local pipeline with local functions (set local=True in function.run())
2884
+ """Run a workflow using kubeflow pipelines
2885
+
2886
+ :param name: Name of the workflow
2887
+ :param workflow_path: URL to a workflow file, if not a project workflow
2888
+ :param arguments: Kubeflow pipelines arguments (parameters)
2889
+ :param artifact_path: Target path/URL for workflow artifacts, the string '{{workflow.uid}}' will be
2890
+ replaced by workflow id.
2891
+ :param workflow_handler: Workflow function handler (for running workflow function directly)
2892
+ :param namespace: Kubernetes namespace if other than default
2893
+ :param sync: Force functions sync before run
2894
+ :param watch: Wait for pipeline completion
2895
+ :param dirty: Allow running the workflow when the git repo is dirty
2896
+ :param engine: Workflow engine running the workflow.
2897
+ Supported values are 'kfp' (default), 'local' or 'remote'.
2898
+ For setting engine for remote running use 'remote:local' or 'remote:kfp'.
2899
+ :param local: Run local pipeline with local functions (set local=True in function.run())
2709
2900
  :param schedule: ScheduleCronTrigger class instance or a standard crontab expression string
2710
2901
  (which will be converted to the class using its `from_crontab` constructor),
2711
2902
  see this link for help:
2712
2903
  https://apscheduler.readthedocs.io/en/3.x/modules/triggers/cron.html#module-apscheduler.triggers.cron
2713
- for using the pre-defined workflow's schedule, set `schedule=True`
2714
- :param timeout: timeout in seconds to wait for pipeline completion (watch will be activated)
2715
- :param source: remote source to use instead of the actual `project.spec.source` (used when engine is remote).
2716
- for other engines the source is to validate that the code is up-to-date
2904
+ For using the pre-defined workflow's schedule, set `schedule=True`
2905
+ :param timeout: Timeout in seconds to wait for pipeline completion (watch will be activated)
2906
+ :param source: Source to use instead of the actual `project.spec.source` (used when engine is remote).
2907
+ Can be a one of:
2908
+ 1. Remote URL which is loaded dynamically to the workflow runner.
2909
+ 2. A path to the project's context on the workflow runner's image.
2910
+ Path can be absolute or relative to `project.spec.build.source_code_target_dir` if defined
2911
+ (enriched when building a project image with source, see `MlrunProject.build_image`).
2912
+ For other engines the source is used to validate that the code is up-to-date.
2717
2913
  :param cleanup_ttl:
2718
- pipeline cleanup ttl in secs (time to wait after workflow completion, at which point the
2914
+ Pipeline cleanup ttl in secs (time to wait after workflow completion, at which point the
2719
2915
  workflow and all its resources are deleted)
2720
2916
  :param notifications:
2721
- list of notifications to send for workflow completion
2722
- :returns: run id
2917
+ List of notifications to send for workflow completion
2918
+
2919
+ :returns: ~py:class:`~mlrun.projects.pipelines._PipelineRunStatus` instance
2723
2920
  """
2724
2921
 
2725
2922
  arguments = arguments or {}
@@ -2736,12 +2933,14 @@ class MlrunProject(ModelObj):
2736
2933
  "Remote repo is not defined, use .create_remote() + push()"
2737
2934
  )
2738
2935
 
2739
- self.sync_functions(always=sync)
2740
- if not self.spec._function_objects:
2741
- raise ValueError(
2742
- "There are no functions in the project."
2743
- " Make sure you've set your functions with project.set_function()."
2744
- )
2936
+ if engine not in ["remote"]:
2937
+ # for remote runs we don't require the functions to be synced as they can be loaded dynamically during run
2938
+ self.sync_functions(always=sync)
2939
+ if not self.spec._function_objects:
2940
+ raise ValueError(
2941
+ "There are no functions in the project."
2942
+ " Make sure you've set your functions with project.set_function()."
2943
+ )
2745
2944
 
2746
2945
  if not name and not workflow_path and not workflow_handler:
2747
2946
  raise ValueError("Workflow name, path, or handler must be specified")
@@ -2775,8 +2974,12 @@ class MlrunProject(ModelObj):
2775
2974
  engine = "remote"
2776
2975
  # The default engine is kfp if not given:
2777
2976
  workflow_engine = get_workflow_engine(engine or workflow_spec.engine, local)
2778
- if not inner_engine and engine == "remote":
2779
- inner_engine = get_workflow_engine(workflow_spec.engine, local).engine
2977
+ if not inner_engine and workflow_engine.engine == "remote":
2978
+ # if inner engine is set to remote, assume kfp as the default inner engine with remote as the runner
2979
+ engine_kind = (
2980
+ workflow_spec.engine if workflow_spec.engine != "remote" else "kfp"
2981
+ )
2982
+ inner_engine = get_workflow_engine(engine_kind, local).engine
2780
2983
  workflow_spec.engine = inner_engine or workflow_engine.engine
2781
2984
 
2782
2985
  run = workflow_engine.run(
@@ -2791,7 +2994,7 @@ class MlrunProject(ModelObj):
2791
2994
  notifications=notifications,
2792
2995
  )
2793
2996
  # run is None when scheduling
2794
- if run and run.state == mlrun.run.RunStatuses.failed:
2997
+ if run and run.state == mlrun_pipelines.common.models.RunStatuses.failed:
2795
2998
  return run
2796
2999
  if not workflow_spec.schedule:
2797
3000
  # Failure and schedule messages already logged
@@ -2800,14 +3003,17 @@ class MlrunProject(ModelObj):
2800
3003
  )
2801
3004
  workflow_spec.clear_tmp()
2802
3005
  if (timeout or watch) and not workflow_spec.schedule:
3006
+ run_status_kwargs = {}
2803
3007
  status_engine = run._engine
2804
3008
  # run's engine gets replaced with inner engine if engine is remote,
2805
3009
  # so in that case we need to get the status from the remote engine manually
2806
- # TODO: support watch for remote:local
2807
- if engine == "remote" and status_engine.engine != "local":
3010
+ if workflow_engine.engine == "remote":
2808
3011
  status_engine = _RemoteRunner
3012
+ run_status_kwargs["inner_engine"] = run._engine
2809
3013
 
2810
- status_engine.get_run_status(project=self, run=run, timeout=timeout)
3014
+ status_engine.get_run_status(
3015
+ project=self, run=run, timeout=timeout, **run_status_kwargs
3016
+ )
2811
3017
  return run
2812
3018
 
2813
3019
  def save_workflow(self, name, target, artifact_path=None, ttl=None):
@@ -2907,17 +3113,18 @@ class MlrunProject(ModelObj):
2907
3113
 
2908
3114
  def set_model_monitoring_credentials(
2909
3115
  self,
2910
- access_key: str = None,
2911
- endpoint_store_connection: str = None,
2912
- stream_path: str = None,
3116
+ access_key: Optional[str] = None,
3117
+ endpoint_store_connection: Optional[str] = None,
3118
+ stream_path: Optional[str] = None,
3119
+ tsdb_connection: Optional[str] = None,
2913
3120
  ):
2914
3121
  """Set the credentials that will be used by the project's model monitoring
2915
3122
  infrastructure functions.
2916
3123
 
2917
- :param access_key: Model Monitoring access key for managing user permissions
2918
3124
  :param access_key: Model Monitoring access key for managing user permissions
2919
3125
  :param endpoint_store_connection: Endpoint store connection string
2920
3126
  :param stream_path: Path to the model monitoring stream
3127
+ :param tsdb_connection: Connection string to the time series database
2921
3128
  """
2922
3129
 
2923
3130
  secrets_dict = {}
@@ -2940,6 +3147,16 @@ class MlrunProject(ModelObj):
2940
3147
  mlrun.common.schemas.model_monitoring.ProjectSecretKeys.STREAM_PATH
2941
3148
  ] = stream_path
2942
3149
 
3150
+ if tsdb_connection:
3151
+ if not tsdb_connection.startswith("taosws://"):
3152
+ raise mlrun.errors.MLRunInvalidArgumentError(
3153
+ "Currently only TDEngine websocket connection is supported for non-v3io TSDB,"
3154
+ "please provide a full URL (e.g. taosws://user:password@host:port)"
3155
+ )
3156
+ secrets_dict[
3157
+ mlrun.common.schemas.model_monitoring.ProjectSecretKeys.TSDB_CONNECTION
3158
+ ] = tsdb_connection
3159
+
2943
3160
  self.set_secrets(
2944
3161
  secrets=secrets_dict,
2945
3162
  provider=mlrun.common.schemas.SecretProviderName.kubernetes,
@@ -2968,7 +3185,7 @@ class MlrunProject(ModelObj):
2968
3185
  notifications: list[mlrun.model.Notification] = None,
2969
3186
  returns: Optional[list[Union[str, dict[str, str]]]] = None,
2970
3187
  builder_env: Optional[dict] = None,
2971
- ) -> typing.Union[mlrun.model.RunObject, kfp.dsl.ContainerOp]:
3188
+ ) -> typing.Union[mlrun.model.RunObject, PipelineNodeWrapper]:
2972
3189
  """Run a local or remote task as part of a local/kubeflow pipeline
2973
3190
 
2974
3191
  example (use with project)::
@@ -2980,8 +3197,11 @@ class MlrunProject(ModelObj):
2980
3197
 
2981
3198
  # run functions (refer to them by name)
2982
3199
  run1 = project.run_function("myfunc", params={"x": 7})
2983
- run2 = project.run_function("train", params={"label_columns": LABELS},
2984
- inputs={"dataset":run1.outputs["data"]})
3200
+ run2 = project.run_function(
3201
+ "train",
3202
+ params={"label_columns": LABELS},
3203
+ inputs={"dataset": run1.outputs["data"]},
3204
+ )
2985
3205
 
2986
3206
  :param function: name of the function (in the project) or function object
2987
3207
  :param handler: name of the function handler
@@ -3021,7 +3241,7 @@ class MlrunProject(ModelObj):
3021
3241
  artifact type can be given there. The artifact key must appear in the dictionary as
3022
3242
  "key": "the_key".
3023
3243
  :param builder_env: env vars dict for source archive config/credentials e.g. builder_env={"GIT_TOKEN": token}
3024
- :return: MLRun RunObject or KubeFlow containerOp
3244
+ :return: MLRun RunObject or PipelineNodeWrapper
3025
3245
  """
3026
3246
  return run_function(
3027
3247
  function,
@@ -3064,7 +3284,7 @@ class MlrunProject(ModelObj):
3064
3284
  requirements_file: str = None,
3065
3285
  extra_args: str = None,
3066
3286
  force_build: bool = False,
3067
- ) -> typing.Union[BuildStatus, kfp.dsl.ContainerOp]:
3287
+ ) -> typing.Union[BuildStatus, PipelineNodeWrapper]:
3068
3288
  """deploy ML function, build container with its dependencies
3069
3289
 
3070
3290
  :param function: name of the function (in the project) or function object
@@ -3118,6 +3338,7 @@ class MlrunProject(ModelObj):
3118
3338
  requirements_file: str = None,
3119
3339
  builder_env: dict = None,
3120
3340
  extra_args: str = None,
3341
+ source_code_target_dir: str = None,
3121
3342
  ):
3122
3343
  """specify builder configuration for the project
3123
3344
 
@@ -3138,6 +3359,8 @@ class MlrunProject(ModelObj):
3138
3359
  e.g. builder_env={"GIT_TOKEN": token}, does not work yet in KFP
3139
3360
  :param extra_args: A string containing additional builder arguments in the format of command-line options,
3140
3361
  e.g. extra_args="--skip-tls-verify --build-arg A=val"
3362
+ :param source_code_target_dir: Path on the image where source code would be extracted
3363
+ (by default `/home/mlrun_code`)
3141
3364
  """
3142
3365
  if not overwrite_build_params:
3143
3366
  # TODO: change overwrite_build_params default to True in 1.8.0
@@ -3161,6 +3384,7 @@ class MlrunProject(ModelObj):
3161
3384
  overwrite=overwrite_build_params,
3162
3385
  builder_env=builder_env,
3163
3386
  extra_args=extra_args,
3387
+ source_code_target_dir=source_code_target_dir,
3164
3388
  )
3165
3389
 
3166
3390
  if set_as_default and image != self.default_image:
@@ -3171,7 +3395,6 @@ class MlrunProject(ModelObj):
3171
3395
  image: str = None,
3172
3396
  set_as_default: bool = True,
3173
3397
  with_mlrun: bool = None,
3174
- skip_deployed: bool = False,
3175
3398
  base_image: str = None,
3176
3399
  commands: list = None,
3177
3400
  secret_name: str = None,
@@ -3182,7 +3405,7 @@ class MlrunProject(ModelObj):
3182
3405
  requirements_file: str = None,
3183
3406
  extra_args: str = None,
3184
3407
  target_dir: str = None,
3185
- ) -> typing.Union[BuildStatus, kfp.dsl.ContainerOp]:
3408
+ ) -> typing.Union[BuildStatus, PipelineNodeWrapper]:
3186
3409
  """Builder docker image for the project, based on the project's build config. Parameters allow to override
3187
3410
  the build config.
3188
3411
  If the project has a source configured and pull_at_runtime is not configured, this source will be cloned to the
@@ -3192,7 +3415,6 @@ class MlrunProject(ModelObj):
3192
3415
  used. If not set, the `mlconf.default_project_image_name` value will be used
3193
3416
  :param set_as_default: set `image` to be the project's default image (default False)
3194
3417
  :param with_mlrun: add the current mlrun package to the container build
3195
- :param skip_deployed: *Deprecated* parameter is ignored
3196
3418
  :param base_image: base image name/path (commands and source code will be added to it) defaults to
3197
3419
  mlrun.mlconf.default_base_image
3198
3420
  :param commands: list of docker build (RUN) commands e.g. ['pip install pandas']
@@ -3207,7 +3429,7 @@ class MlrunProject(ModelObj):
3207
3429
  * False: The new params are merged with the existing
3208
3430
  * True: The existing params are replaced by the new ones
3209
3431
  :param extra_args: A string containing additional builder arguments in the format of command-line options,
3210
- e.g. extra_args="--skip-tls-verify --build-arg A=val"r
3432
+ e.g. extra_args="--skip-tls-verify --build-arg A=val"
3211
3433
  :param target_dir: Path on the image where source code would be extracted (by default `/home/mlrun_code`)
3212
3434
  """
3213
3435
  if not base_image:
@@ -3217,14 +3439,6 @@ class MlrunProject(ModelObj):
3217
3439
  base_image=base_image,
3218
3440
  )
3219
3441
 
3220
- if skip_deployed:
3221
- warnings.warn(
3222
- "The 'skip_deployed' parameter is deprecated and will be removed in 1.7.0. "
3223
- "This parameter is ignored.",
3224
- # TODO: remove in 1.7.0
3225
- FutureWarning,
3226
- )
3227
-
3228
3442
  if not overwrite_build_params:
3229
3443
  # TODO: change overwrite_build_params default to True in 1.8.0
3230
3444
  warnings.warn(
@@ -3275,6 +3489,11 @@ class MlrunProject(ModelObj):
3275
3489
  force_build=True,
3276
3490
  )
3277
3491
 
3492
+ # Get the enriched target dir from the function
3493
+ self.spec.build.source_code_target_dir = (
3494
+ function.spec.build.source_code_target_dir
3495
+ )
3496
+
3278
3497
  try:
3279
3498
  mlrun.db.get_run_db(secrets=self._secrets).delete_function(
3280
3499
  name=function.metadata.name
@@ -3283,7 +3502,7 @@ class MlrunProject(ModelObj):
3283
3502
  logger.warning(
3284
3503
  f"Image was successfully built, but failed to delete temporary function {function.metadata.name}."
3285
3504
  " To remove the function, attempt to manually delete it.",
3286
- exc=repr(exc),
3505
+ exc=mlrun.errors.err_to_str(exc),
3287
3506
  )
3288
3507
 
3289
3508
  return result
@@ -3297,7 +3516,7 @@ class MlrunProject(ModelObj):
3297
3516
  verbose: bool = None,
3298
3517
  builder_env: dict = None,
3299
3518
  mock: bool = None,
3300
- ) -> typing.Union[DeployStatus, kfp.dsl.ContainerOp]:
3519
+ ) -> typing.Union[DeployStatus, PipelineNodeWrapper]:
3301
3520
  """deploy real-time (nuclio based) functions
3302
3521
 
3303
3522
  :param function: name of the function (in the project) or function object
@@ -3332,7 +3551,12 @@ class MlrunProject(ModelObj):
3332
3551
  artifact = db.read_artifact(
3333
3552
  key, tag, iter=iter, project=self.metadata.name, tree=tree
3334
3553
  )
3335
- return dict_to_artifact(artifact)
3554
+
3555
+ # in tests, if an artifact is not found, the db returns None
3556
+ # in real usage, the db should raise an exception
3557
+ if artifact:
3558
+ return dict_to_artifact(artifact)
3559
+ return None
3336
3560
 
3337
3561
  def list_artifacts(
3338
3562
  self,
@@ -3355,9 +3579,9 @@ class MlrunProject(ModelObj):
3355
3579
  Examples::
3356
3580
 
3357
3581
  # Get latest version of all artifacts in project
3358
- latest_artifacts = project.list_artifacts('', tag='latest')
3582
+ latest_artifacts = project.list_artifacts("", tag="latest")
3359
3583
  # check different artifact versions for a specific artifact, return as objects list
3360
- result_versions = project.list_artifacts('results', tag='*').to_objects()
3584
+ result_versions = project.list_artifacts("results", tag="*").to_objects()
3361
3585
 
3362
3586
  :param name: Name of artifacts to retrieve. Name with '~' prefix is used as a like query, and is not
3363
3587
  case-sensitive. This means that querying for ``~name`` may return artifacts named
@@ -3407,7 +3631,7 @@ class MlrunProject(ModelObj):
3407
3631
  Examples::
3408
3632
 
3409
3633
  # Get latest version of all models in project
3410
- latest_models = project.list_models('', tag='latest')
3634
+ latest_models = project.list_models("", tag="latest")
3411
3635
 
3412
3636
 
3413
3637
  :param name: Name of artifacts to retrieve. Name with '~' prefix is used as a like query, and is not
@@ -3477,9 +3701,7 @@ class MlrunProject(ModelObj):
3477
3701
  :returns: List of function objects.
3478
3702
  """
3479
3703
 
3480
- model_monitoring_labels_list = [
3481
- f"{mm_constants.ModelMonitoringAppLabel.KEY}={mm_constants.ModelMonitoringAppLabel.VAL}"
3482
- ]
3704
+ model_monitoring_labels_list = [str(mm_constants.ModelMonitoringAppLabel())]
3483
3705
  if labels:
3484
3706
  model_monitoring_labels_list += labels
3485
3707
  return self.list_functions(
@@ -3493,7 +3715,10 @@ class MlrunProject(ModelObj):
3493
3715
  name: Optional[str] = None,
3494
3716
  uid: Optional[Union[str, list[str]]] = None,
3495
3717
  labels: Optional[Union[str, list[str]]] = None,
3496
- state: Optional[str] = None,
3718
+ state: Optional[
3719
+ mlrun.common.runtimes.constants.RunStates
3720
+ ] = None, # Backward compatibility
3721
+ states: typing.Optional[list[mlrun.common.runtimes.constants.RunStates]] = None,
3497
3722
  sort: bool = True,
3498
3723
  last: int = 0,
3499
3724
  iter: bool = False,
@@ -3512,14 +3737,14 @@ class MlrunProject(ModelObj):
3512
3737
  Example::
3513
3738
 
3514
3739
  # return a list of runs matching the name and label and compare
3515
- runs = project.list_runs(name='download', labels='owner=admin')
3740
+ runs = project.list_runs(name="download", labels="owner=admin")
3516
3741
  runs.compare()
3517
3742
 
3518
3743
  # multi-label filter can also be provided
3519
- runs = project.list_runs(name='download', labels=["kind=job", "owner=admin"])
3744
+ runs = project.list_runs(name="download", labels=["kind=job", "owner=admin"])
3520
3745
 
3521
3746
  # If running in Jupyter, can use the .show() function to display the results
3522
- project.list_runs(name='').show()
3747
+ project.list_runs(name="").show()
3523
3748
 
3524
3749
 
3525
3750
  :param name: Name of the run to retrieve.
@@ -3527,10 +3752,11 @@ class MlrunProject(ModelObj):
3527
3752
  :param labels: A list of labels to filter by. Label filters work by either filtering a specific value
3528
3753
  of a label (i.e. list("key=value")) or by looking for the existence of a given
3529
3754
  key (i.e. "key").
3530
- :param state: List only runs whose state is specified.
3755
+ :param state: Deprecated - List only runs whose state is specified.
3756
+ :param states: List only runs whose state is one of the provided states.
3531
3757
  :param sort: Whether to sort the result according to their start time. Otherwise, results will be
3532
3758
  returned by their internal order in the DB (order will not be guaranteed).
3533
- :param last: Deprecated - currently not used (will be removed in 1.8.0).
3759
+ :param last: Deprecated - currently not used (will be removed in 1.9.0).
3534
3760
  :param iter: If ``True`` return runs from all iterations. Otherwise, return only runs whose ``iter`` is 0.
3535
3761
  :param start_time_from: Filter by run start time in ``[start_time_from, start_time_to]``.
3536
3762
  :param start_time_to: Filter by run start time in ``[start_time_from, start_time_to]``.
@@ -3538,13 +3764,22 @@ class MlrunProject(ModelObj):
3538
3764
  last_update_time_to)``.
3539
3765
  :param last_update_time_to: Filter by run last update time in ``(last_update_time_from, last_update_time_to)``.
3540
3766
  """
3767
+ if state:
3768
+ # TODO: Remove this in 1.9.0
3769
+ warnings.warn(
3770
+ "'state' is deprecated and will be removed in 1.9.0. Use 'states' instead.",
3771
+ FutureWarning,
3772
+ )
3773
+
3541
3774
  db = mlrun.db.get_run_db(secrets=self._secrets)
3542
3775
  return db.list_runs(
3543
3776
  name,
3544
3777
  uid,
3545
3778
  self.metadata.name,
3546
3779
  labels=labels,
3547
- state=state,
3780
+ states=mlrun.utils.helpers.as_list(state)
3781
+ if state is not None
3782
+ else states or None,
3548
3783
  sort=sort,
3549
3784
  last=last,
3550
3785
  iter=iter,
@@ -3625,12 +3860,179 @@ class MlrunProject(ModelObj):
3625
3860
  """
3626
3861
  self.spec.remove_custom_packager(packager=packager)
3627
3862
 
3863
+ def store_api_gateway(
3864
+ self,
3865
+ api_gateway: mlrun.runtimes.nuclio.api_gateway.APIGateway,
3866
+ wait_for_readiness=True,
3867
+ max_wait_time=90,
3868
+ ) -> mlrun.runtimes.nuclio.api_gateway.APIGateway:
3869
+ """
3870
+ Creates or updates a Nuclio API Gateway using the provided APIGateway object.
3871
+
3872
+ This method interacts with the MLRun service to create/update a Nuclio API Gateway based on the provided
3873
+ APIGateway object. Once done, it returns the updated APIGateway object containing all fields propagated
3874
+ on MLRun and Nuclio sides, such as the 'host' attribute.
3875
+ Nuclio docs here: https://docs.nuclio.io/en/latest/reference/api-gateway/http.html
3876
+
3877
+ :param api_gateway: An instance of :py:class:`~mlrun.runtimes.nuclio.APIGateway` representing the configuration
3878
+ of the API Gateway to be created or updated.
3879
+ :param wait_for_readiness: (Optional) A boolean indicating whether to wait for the API Gateway to become ready
3880
+ after creation or update (default is True)
3881
+ :param max_wait_time: (Optional) Maximum time to wait for API Gateway readiness in seconds (default is 90s)
3882
+
3883
+
3884
+ @return: An instance of :py:class:`~mlrun.runtimes.nuclio.APIGateway` with all fields populated based on the
3885
+ information retrieved from the Nuclio API
3886
+ """
3887
+
3888
+ api_gateway_json = mlrun.db.get_run_db().store_api_gateway(
3889
+ api_gateway=api_gateway,
3890
+ project=self.name,
3891
+ )
3892
+
3893
+ if api_gateway_json:
3894
+ # fill in all the fields in the user's api_gateway object
3895
+ api_gateway = mlrun.runtimes.nuclio.api_gateway.APIGateway.from_scheme(
3896
+ api_gateway_json
3897
+ )
3898
+ if wait_for_readiness:
3899
+ api_gateway.wait_for_readiness(max_wait_time=max_wait_time)
3900
+
3901
+ return api_gateway
3902
+
3903
+ def list_api_gateways(self) -> list[mlrun.runtimes.nuclio.api_gateway.APIGateway]:
3904
+ """
3905
+ Retrieves a list of Nuclio API gateways associated with the project.
3906
+
3907
+ @return: List of :py:class:`~mlrun.runtimes.nuclio.api_gateway.APIGateway` objects representing
3908
+ the Nuclio API gateways associated with the project.
3909
+ """
3910
+ gateways_list = mlrun.db.get_run_db().list_api_gateways(self.name)
3911
+ return [
3912
+ mlrun.runtimes.nuclio.api_gateway.APIGateway.from_scheme(gateway_dict)
3913
+ for gateway_dict in gateways_list.api_gateways.values()
3914
+ ]
3915
+
3916
+ def get_api_gateway(
3917
+ self,
3918
+ name: str,
3919
+ ) -> mlrun.runtimes.nuclio.api_gateway.APIGateway:
3920
+ """
3921
+ Retrieves an API gateway by name instance.
3922
+
3923
+ :param name: The name of the API gateway to retrieve.
3924
+
3925
+ Returns:
3926
+ mlrun.runtimes.nuclio.APIGateway: An instance of APIGateway.
3927
+ """
3928
+
3929
+ gateway = mlrun.db.get_run_db().get_api_gateway(name=name, project=self.name)
3930
+ return mlrun.runtimes.nuclio.api_gateway.APIGateway.from_scheme(gateway)
3931
+
3932
+ def delete_api_gateway(
3933
+ self,
3934
+ name: str,
3935
+ ):
3936
+ """
3937
+ Deletes an API gateway by name.
3938
+
3939
+ :param name: The name of the API gateway to delete.
3940
+ """
3941
+
3942
+ mlrun.db.get_run_db().delete_api_gateway(name=name, project=self.name)
3943
+
3944
+ def store_alert_config(
3945
+ self, alert_data: AlertConfig, alert_name=None
3946
+ ) -> AlertConfig:
3947
+ """
3948
+ Create/modify an alert.
3949
+ :param alert_data: The data of the alert.
3950
+ :param alert_name: The name of the alert.
3951
+ :return: the created/modified alert.
3952
+ """
3953
+ db = mlrun.db.get_run_db(secrets=self._secrets)
3954
+ if alert_name is None:
3955
+ alert_name = alert_data.name
3956
+ return db.store_alert_config(alert_name, alert_data, project=self.metadata.name)
3957
+
3958
+ def get_alert_config(self, alert_name: str) -> AlertConfig:
3959
+ """
3960
+ Retrieve an alert.
3961
+ :param alert_name: The name of the alert to retrieve.
3962
+ :return: The alert object.
3963
+ """
3964
+ db = mlrun.db.get_run_db(secrets=self._secrets)
3965
+ return db.get_alert_config(alert_name, self.metadata.name)
3966
+
3967
+ def list_alerts_configs(self) -> list[AlertConfig]:
3968
+ """
3969
+ Retrieve list of alerts of a project.
3970
+ :return: All the alerts objects of the project.
3971
+ """
3972
+ db = mlrun.db.get_run_db(secrets=self._secrets)
3973
+ return db.list_alerts_configs(self.metadata.name)
3974
+
3975
+ def delete_alert_config(
3976
+ self, alert_data: AlertConfig = None, alert_name: str = None
3977
+ ):
3978
+ """
3979
+ Delete an alert.
3980
+ :param alert_data: The data of the alert.
3981
+ :param alert_name: The name of the alert to delete.
3982
+ """
3983
+ if alert_data is None and alert_name is None:
3984
+ raise ValueError(
3985
+ "At least one of alert_data or alert_name must be provided"
3986
+ )
3987
+ if alert_data and alert_name and alert_data.name != alert_name:
3988
+ raise ValueError("Alert_data name does not match the provided alert_name")
3989
+ db = mlrun.db.get_run_db(secrets=self._secrets)
3990
+ if alert_data:
3991
+ alert_name = alert_data.name
3992
+ db.delete_alert_config(alert_name, self.metadata.name)
3993
+
3994
+ def reset_alert_config(
3995
+ self, alert_data: AlertConfig = None, alert_name: str = None
3996
+ ):
3997
+ """
3998
+ Reset an alert.
3999
+ :param alert_data: The data of the alert.
4000
+ :param alert_name: The name of the alert to reset.
4001
+ """
4002
+ if alert_data is None and alert_name is None:
4003
+ raise ValueError(
4004
+ "At least one of alert_data or alert_name must be provided"
4005
+ )
4006
+ if alert_data and alert_name and alert_data.name != alert_name:
4007
+ raise ValueError("Alert_data name does not match the provided alert_name")
4008
+ db = mlrun.db.get_run_db(secrets=self._secrets)
4009
+ if alert_data:
4010
+ alert_name = alert_data.name
4011
+ db.reset_alert_config(alert_name, self.metadata.name)
4012
+
4013
+ def get_alert_template(self, template_name: str) -> AlertTemplate:
4014
+ """
4015
+ Retrieve a specific alert template.
4016
+ :param template_name: The name of the template to retrieve.
4017
+ :return: The template object.
4018
+ """
4019
+ db = mlrun.db.get_run_db(secrets=self._secrets)
4020
+ return db.get_alert_template(template_name)
4021
+
4022
+ def list_alert_templates(self) -> list[AlertTemplate]:
4023
+ """
4024
+ Retrieve list of all alert templates.
4025
+ :return: All the alert template objects in the database.
4026
+ """
4027
+ db = mlrun.db.get_run_db(secrets=self._secrets)
4028
+ return db.list_alert_templates()
4029
+
3628
4030
  def _run_authenticated_git_action(
3629
4031
  self,
3630
4032
  action: Callable,
3631
4033
  remote: str,
3632
- args: list = [],
3633
- kwargs: dict = {},
4034
+ args: list = None,
4035
+ kwargs: dict = None,
3634
4036
  secrets: Union[SecretsStore, dict] = None,
3635
4037
  ):
3636
4038
  """Run an arbitrary Git routine while the remote is enriched with secrets
@@ -3650,6 +4052,8 @@ class MlrunProject(ModelObj):
3650
4052
  try:
3651
4053
  if is_remote_enriched:
3652
4054
  self.spec.repo.remotes[remote].set_url(enriched_remote, clean_remote)
4055
+ args = args or []
4056
+ kwargs = kwargs or {}
3653
4057
  action(*args, **kwargs)
3654
4058
  except RuntimeError as e:
3655
4059
  raise mlrun.errors.MLRunRuntimeError(
@@ -3702,6 +4106,83 @@ class MlrunProject(ModelObj):
3702
4106
  f"<project.spec.get_code_path()>/<{param_name}>)."
3703
4107
  )
3704
4108
 
4109
+ def _resolve_artifact_producer(
4110
+ self,
4111
+ artifact: typing.Union[str, Artifact],
4112
+ project_producer_tag: str = None,
4113
+ ) -> tuple[ArtifactProducer, bool]:
4114
+ """
4115
+ Resolve the artifact producer of the given artifact.
4116
+ If the artifact's producer is a run, the artifact is registered with the original producer.
4117
+ Otherwise, the artifact is registered with the current project as the producer.
4118
+
4119
+ :param artifact: The artifact to resolve its producer.
4120
+ :param project_producer_tag: The tag to use for the project as the producer. If not provided, a tag will be
4121
+ generated for the project.
4122
+ :return: A tuple of the resolved producer and whether it is retained or not.
4123
+ """
4124
+
4125
+ if not isinstance(artifact, str) and artifact.spec.producer:
4126
+ # if the artifact was imported from a yaml file, the producer can be a dict
4127
+ if isinstance(artifact.spec.producer, ArtifactProducer):
4128
+ producer_dict = artifact.spec.producer.get_meta()
4129
+ else:
4130
+ producer_dict = artifact.spec.producer
4131
+
4132
+ if producer_dict.get("kind", "") == "run":
4133
+ return ArtifactProducer(
4134
+ name=producer_dict.get("name", ""),
4135
+ kind=producer_dict.get("kind", ""),
4136
+ project=producer_dict.get("project", ""),
4137
+ tag=producer_dict.get("tag", ""),
4138
+ ), True
4139
+
4140
+ # do not retain the artifact's producer, replace it with the project as the producer
4141
+ project_producer_tag = project_producer_tag or self._get_project_tag()
4142
+ return ArtifactProducer(
4143
+ kind="project",
4144
+ name=self.metadata.name,
4145
+ project=self.metadata.name,
4146
+ tag=project_producer_tag,
4147
+ ), False
4148
+
4149
+ def _resolve_existing_artifact(
4150
+ self,
4151
+ item: typing.Union[str, Artifact],
4152
+ tag: str = None,
4153
+ ) -> typing.Optional[Artifact]:
4154
+ """
4155
+ Check if there is and existing artifact with the given item and tag.
4156
+ If there is, return the existing artifact. Otherwise, return None.
4157
+
4158
+ :param item: The item (or key) to check if there is an existing artifact for.
4159
+ :param tag: The tag to check if there is an existing artifact for.
4160
+ :return: The existing artifact if there is one, otherwise None.
4161
+ """
4162
+ try:
4163
+ if isinstance(item, str):
4164
+ existing_artifact = self.get_artifact(key=item, tag=tag)
4165
+ else:
4166
+ existing_artifact = self.get_artifact(
4167
+ key=item.key,
4168
+ tag=item.tag,
4169
+ iter=item.iter,
4170
+ tree=item.tree,
4171
+ )
4172
+ if existing_artifact is not None:
4173
+ return existing_artifact.from_dict(existing_artifact)
4174
+ except mlrun.errors.MLRunNotFoundError:
4175
+ logger.debug(
4176
+ "No existing artifact was found",
4177
+ key=item if isinstance(item, str) else item.key,
4178
+ tag=tag if isinstance(item, str) else item.tag,
4179
+ tree=None if isinstance(item, str) else item.tree,
4180
+ )
4181
+ return None
4182
+
4183
+ def _get_project_tag(self):
4184
+ return self._get_hexsha() or str(uuid.uuid4())
4185
+
3705
4186
 
3706
4187
  def _set_as_current_default_project(project: MlrunProject):
3707
4188
  mlrun.mlconf.default_project = project.metadata.name
@@ -3724,10 +4205,6 @@ def _init_function_from_dict(
3724
4205
  tag = f.get("tag", None)
3725
4206
 
3726
4207
  has_module = _has_module(handler, kind)
3727
- if not url and "spec" not in f and not has_module:
3728
- # function must point to a file or a module or have a spec
3729
- raise ValueError("Function missing a url or a spec or a module")
3730
-
3731
4208
  relative_url = url
3732
4209
  url, in_context = project.get_item_absolute_path(url)
3733
4210
 
@@ -3787,6 +4264,18 @@ def _init_function_from_dict(
3787
4264
  tag=tag,
3788
4265
  )
3789
4266
 
4267
+ elif kind in mlrun.runtimes.RuntimeKinds.nuclio_runtimes():
4268
+ func = new_function(
4269
+ name,
4270
+ command=relative_url,
4271
+ image=image,
4272
+ kind=kind,
4273
+ handler=handler,
4274
+ tag=tag,
4275
+ )
4276
+ if image and kind != mlrun.runtimes.RuntimeKinds.application:
4277
+ logger.info("Function code not specified, setting entry point to image")
4278
+ func.from_image(image)
3790
4279
  else:
3791
4280
  raise ValueError(f"Unsupported function url:handler {url}:{handler} or no spec")
3792
4281