mlrun 1.3.1rc5__py3-none-any.whl → 1.4.0rc2__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 (67) hide show
  1. mlrun/__main__.py +57 -4
  2. mlrun/api/api/endpoints/marketplace.py +57 -4
  3. mlrun/api/api/endpoints/runs.py +2 -0
  4. mlrun/api/api/utils.py +102 -0
  5. mlrun/api/crud/__init__.py +1 -0
  6. mlrun/api/crud/marketplace.py +133 -44
  7. mlrun/api/crud/notifications.py +80 -0
  8. mlrun/api/crud/runs.py +2 -0
  9. mlrun/api/crud/secrets.py +1 -0
  10. mlrun/api/db/base.py +32 -0
  11. mlrun/api/db/session.py +3 -11
  12. mlrun/api/db/sqldb/db.py +162 -1
  13. mlrun/api/db/sqldb/models/models_mysql.py +41 -0
  14. mlrun/api/db/sqldb/models/models_sqlite.py +35 -0
  15. mlrun/api/main.py +54 -1
  16. mlrun/api/migrations_mysql/versions/c905d15bd91d_notifications.py +70 -0
  17. mlrun/api/migrations_sqlite/versions/959ae00528ad_notifications.py +61 -0
  18. mlrun/api/schemas/__init__.py +1 -0
  19. mlrun/api/schemas/marketplace.py +18 -8
  20. mlrun/api/{db/filedb/__init__.py → schemas/notification.py} +17 -1
  21. mlrun/api/utils/singletons/db.py +8 -14
  22. mlrun/builder.py +37 -26
  23. mlrun/config.py +12 -2
  24. mlrun/data_types/spark.py +9 -2
  25. mlrun/datastore/base.py +10 -1
  26. mlrun/datastore/sources.py +1 -1
  27. mlrun/db/__init__.py +6 -4
  28. mlrun/db/base.py +1 -2
  29. mlrun/db/httpdb.py +32 -6
  30. mlrun/db/nopdb.py +463 -0
  31. mlrun/db/sqldb.py +47 -7
  32. mlrun/execution.py +3 -0
  33. mlrun/feature_store/api.py +26 -12
  34. mlrun/feature_store/common.py +1 -1
  35. mlrun/feature_store/steps.py +110 -13
  36. mlrun/k8s_utils.py +10 -0
  37. mlrun/model.py +43 -0
  38. mlrun/projects/operations.py +5 -2
  39. mlrun/projects/pipelines.py +4 -3
  40. mlrun/projects/project.py +50 -10
  41. mlrun/run.py +5 -4
  42. mlrun/runtimes/__init__.py +2 -6
  43. mlrun/runtimes/base.py +82 -31
  44. mlrun/runtimes/function.py +22 -0
  45. mlrun/runtimes/kubejob.py +10 -8
  46. mlrun/runtimes/serving.py +1 -1
  47. mlrun/runtimes/sparkjob/__init__.py +0 -1
  48. mlrun/runtimes/sparkjob/abstract.py +0 -2
  49. mlrun/serving/states.py +2 -2
  50. mlrun/utils/helpers.py +1 -1
  51. mlrun/utils/notifications/notification/__init__.py +1 -1
  52. mlrun/utils/notifications/notification/base.py +14 -13
  53. mlrun/utils/notifications/notification/console.py +6 -3
  54. mlrun/utils/notifications/notification/git.py +19 -12
  55. mlrun/utils/notifications/notification/ipython.py +6 -3
  56. mlrun/utils/notifications/notification/slack.py +13 -12
  57. mlrun/utils/notifications/notification_pusher.py +185 -37
  58. mlrun/utils/version/version.json +2 -2
  59. {mlrun-1.3.1rc5.dist-info → mlrun-1.4.0rc2.dist-info}/METADATA +6 -2
  60. {mlrun-1.3.1rc5.dist-info → mlrun-1.4.0rc2.dist-info}/RECORD +64 -63
  61. mlrun/api/db/filedb/db.py +0 -518
  62. mlrun/db/filedb.py +0 -899
  63. mlrun/runtimes/sparkjob/spark2job.py +0 -59
  64. {mlrun-1.3.1rc5.dist-info → mlrun-1.4.0rc2.dist-info}/LICENSE +0 -0
  65. {mlrun-1.3.1rc5.dist-info → mlrun-1.4.0rc2.dist-info}/WHEEL +0 -0
  66. {mlrun-1.3.1rc5.dist-info → mlrun-1.4.0rc2.dist-info}/entry_points.txt +0 -0
  67. {mlrun-1.3.1rc5.dist-info → mlrun-1.4.0rc2.dist-info}/top_level.txt +0 -0
mlrun/__main__.py CHANGED
@@ -1052,6 +1052,15 @@ def logs(uid, project, offset, db, watch):
1052
1052
  is_flag=True,
1053
1053
  help="Store the project secrets as k8s secrets",
1054
1054
  )
1055
+ @click.option(
1056
+ "--notifications",
1057
+ "--notification",
1058
+ "-nt",
1059
+ multiple=True,
1060
+ help="To have a notification for the run set notification file "
1061
+ "destination define: file=notification.json or a "
1062
+ 'dictionary configuration e.g \'{"slack":{"webhook":"<webhook>"}}\'',
1063
+ )
1055
1064
  def project(
1056
1065
  context,
1057
1066
  name,
@@ -1077,6 +1086,7 @@ def project(
1077
1086
  timeout,
1078
1087
  ensure_project,
1079
1088
  schedule,
1089
+ notifications,
1080
1090
  overwrite_schedule,
1081
1091
  save_secrets,
1082
1092
  save,
@@ -1152,6 +1162,8 @@ def project(
1152
1162
  "token": proj.get_param("GIT_TOKEN"),
1153
1163
  },
1154
1164
  )
1165
+ if notifications:
1166
+ load_notification(notifications, proj)
1155
1167
  try:
1156
1168
  proj.run(
1157
1169
  name=run,
@@ -1169,11 +1181,9 @@ def project(
1169
1181
  timeout=timeout,
1170
1182
  overwrite=overwrite_schedule,
1171
1183
  )
1172
-
1173
- except Exception as exc:
1184
+ except Exception as err:
1174
1185
  print(traceback.format_exc())
1175
- message = f"failed to run pipeline, {err_to_str(exc)}"
1176
- proj.notifiers.push(message, "error")
1186
+ send_workflow_error_notification(run, proj, err)
1177
1187
  exit(1)
1178
1188
 
1179
1189
  elif sync:
@@ -1450,5 +1460,48 @@ def func_url_to_runtime(func_url, ensure_project: bool = False):
1450
1460
  return runtime
1451
1461
 
1452
1462
 
1463
+ def load_notification(notifications: str, project: mlrun.projects.MlrunProject):
1464
+ """
1465
+ A dictionary or json file containing notification dictionaries can be used by the user to set notifications.
1466
+ Each notification is stored in a tuple called notifications.
1467
+ The code then goes through each value in the notifications tuple and check
1468
+ if the notification starts with "file=", such as "file=notification.json," in those cases it loads the
1469
+ notification.json file and uses add_notification_to_project to add the notifications from the file to
1470
+ the project. If not, it adds the notification dictionary to the project.
1471
+ :param notifications: Notifications file or a dictionary to be added to the project
1472
+ :param project: The object to which the notifications will be added
1473
+ :return:
1474
+ """
1475
+ for notification in notifications:
1476
+ if notification.startswith("file="):
1477
+ file_path = notification.split("=")[-1]
1478
+ notification = open(file_path, "r")
1479
+ notification = json.load(notification)
1480
+ else:
1481
+ notification = json.loads(notification)
1482
+ add_notification_to_project(notification, project)
1483
+
1484
+
1485
+ def add_notification_to_project(
1486
+ notification: str, project: mlrun.projects.MlrunProject
1487
+ ):
1488
+ for notification_type, notification_params in notification.items():
1489
+ project.notifiers.add_notification(
1490
+ notification_type=notification_type, params=notification_params
1491
+ )
1492
+
1493
+
1494
+ def send_workflow_error_notification(
1495
+ run_id: str, project: mlrun.projects.MlrunProject, error: KeyError
1496
+ ):
1497
+ message = (
1498
+ f":x: Failed to run scheduled workflow {run_id} in Project {project.name} !\n"
1499
+ f"error: ```{err_to_str(error)}```"
1500
+ )
1501
+ project.notifiers.push(
1502
+ message=message, severity=mlrun.api.schemas.NotificationSeverity.ERROR
1503
+ )
1504
+
1505
+
1453
1506
  if __name__ == "__main__":
1454
1507
  main()
@@ -159,7 +159,6 @@ async def store_source(
159
159
  )
160
160
  async def get_catalog(
161
161
  source_name: str,
162
- channel: Optional[str] = Query(None),
163
162
  version: Optional[str] = Query(None),
164
163
  tag: Optional[str] = Query(None),
165
164
  force_refresh: Optional[bool] = Query(False, alias="force-refresh"),
@@ -180,7 +179,6 @@ async def get_catalog(
180
179
  return await run_in_threadpool(
181
180
  mlrun.api.crud.Marketplace().get_source_catalog,
182
181
  ordered_source.source,
183
- channel,
184
182
  version,
185
183
  tag,
186
184
  force_refresh,
@@ -194,7 +192,6 @@ async def get_catalog(
194
192
  async def get_item(
195
193
  source_name: str,
196
194
  item_name: str,
197
- channel: Optional[str] = Query("development"),
198
195
  version: Optional[str] = Query(None),
199
196
  tag: Optional[str] = Query("latest"),
200
197
  force_refresh: Optional[bool] = Query(False, alias="force-refresh"),
@@ -216,7 +213,6 @@ async def get_item(
216
213
  mlrun.api.crud.Marketplace().get_item,
217
214
  ordered_source.source,
218
215
  item_name,
219
- channel,
220
216
  version,
221
217
  tag,
222
218
  force_refresh,
@@ -255,3 +251,60 @@ async def get_object(
255
251
  if not ctype:
256
252
  ctype = "application/octet-stream"
257
253
  return Response(content=object_data, media_type=ctype)
254
+
255
+
256
+ @router.get("/marketplace/sources/{source_name}/items/{item_name}/assets/{asset_name}")
257
+ async def get_asset(
258
+ source_name: str,
259
+ item_name: str,
260
+ asset_name: str,
261
+ tag: Optional[str] = Query("latest"),
262
+ version: Optional[str] = Query(None),
263
+ db_session: Session = Depends(mlrun.api.api.deps.get_db_session),
264
+ auth_info: mlrun.api.schemas.AuthInfo = Depends(
265
+ mlrun.api.api.deps.authenticate_request
266
+ ),
267
+ ):
268
+ """
269
+ Retrieve asset from a specific item in specific marketplace source.
270
+
271
+ :param source_name: marketplace source name
272
+ :param item_name: the name of the item
273
+ :param asset_name: the name of the asset to retrieve
274
+ :param tag: tag of item - latest or version number
275
+ :param version: item version
276
+ :param db_session: a session that manages the current dialog with the database
277
+ :param auth_info: the auth info of the request
278
+
279
+ :return: fastapi response with the asset in content
280
+ """
281
+ source = await run_in_threadpool(
282
+ get_db().get_marketplace_source, db_session, source_name
283
+ )
284
+
285
+ await mlrun.api.utils.auth.verifier.AuthVerifier().query_global_resource_permissions(
286
+ mlrun.api.schemas.AuthorizationResourceTypes.marketplace_source,
287
+ AuthorizationAction.read,
288
+ auth_info,
289
+ )
290
+ # Getting the relevant item which hold the asset information
291
+ item = await run_in_threadpool(
292
+ mlrun.api.crud.Marketplace().get_item,
293
+ source.source,
294
+ item_name,
295
+ version,
296
+ tag,
297
+ )
298
+
299
+ # Getting the asset from the item
300
+ asset, url = await run_in_threadpool(
301
+ mlrun.api.crud.Marketplace().get_asset,
302
+ source.source,
303
+ item,
304
+ asset_name,
305
+ )
306
+
307
+ ctype, _ = mimetypes.guess_type(url)
308
+ if not ctype:
309
+ ctype = "application/octet-stream"
310
+ return Response(content=asset, media_type=ctype)
@@ -178,6 +178,7 @@ async def list_runs(
178
178
  mlrun.api.schemas.OrderType.desc, alias="partition-order"
179
179
  ),
180
180
  max_partitions: int = Query(0, alias="max-partitions", ge=0),
181
+ with_notifications: bool = Query(False, alias="with-notifications"),
181
182
  auth_info: mlrun.api.schemas.AuthInfo = Depends(deps.authenticate_request),
182
183
  db_session: Session = Depends(deps.get_db_session),
183
184
  ):
@@ -207,6 +208,7 @@ async def list_runs(
207
208
  partition_sort_by,
208
209
  partition_order,
209
210
  max_partitions,
211
+ with_notifications=with_notifications,
210
212
  )
211
213
  filtered_runs = await mlrun.api.utils.auth.verifier.AuthVerifier().filter_project_resources_by_permissions(
212
214
  mlrun.api.schemas.AuthorizationResourceTypes.run,
mlrun/api/api/utils.py CHANGED
@@ -13,6 +13,7 @@
13
13
  # limitations under the License.
14
14
  #
15
15
  import collections
16
+ import json
16
17
  import re
17
18
  import traceback
18
19
  import typing
@@ -29,6 +30,7 @@ from sqlalchemy.orm import Session
29
30
  import mlrun.api.crud
30
31
  import mlrun.api.utils.auth.verifier
31
32
  import mlrun.api.utils.clients.iguazio
33
+ import mlrun.api.utils.singletons.k8s
32
34
  import mlrun.errors
33
35
  import mlrun.runtimes.pod
34
36
  import mlrun.utils.helpers
@@ -185,6 +187,7 @@ def _generate_function_and_task_from_submit_run_body(
185
187
  function = enrich_function_from_dict(function, function_dict)
186
188
 
187
189
  apply_enrichment_and_validation_on_function(function, auth_info)
190
+ apply_enrichment_and_validation_on_task(task)
188
191
 
189
192
  return function, task
190
193
 
@@ -196,6 +199,105 @@ async def submit_run(db_session: Session, auth_info: mlrun.api.schemas.AuthInfo,
196
199
  return response
197
200
 
198
201
 
202
+ def apply_enrichment_and_validation_on_task(task):
203
+
204
+ # Masking notification config params from the task object
205
+ mask_notification_params_on_task(task)
206
+
207
+
208
+ def mask_notification_params_on_task(task):
209
+ run_uid = get_in(task, "metadata.uid")
210
+ project = get_in(task, "metadata.project")
211
+ notifications = task.get("spec", {}).get("notifications", [])
212
+ if notifications:
213
+ for notification in notifications:
214
+ notification_object = mlrun.model.Notification.from_dict(notification)
215
+ mask_notification_params_with_secret(project, run_uid, notification_object)
216
+
217
+
218
+ def mask_notification_params_with_secret(
219
+ project: str, run_uid: str, notification_object: mlrun.model.Notification
220
+ ) -> mlrun.model.Notification:
221
+ if notification_object.params and "secret" not in notification_object.params:
222
+ secret_key = mlrun.api.crud.Secrets().generate_client_project_secret_key(
223
+ mlrun.api.crud.SecretsClientType.notifications,
224
+ run_uid,
225
+ notification_object.name,
226
+ )
227
+ mlrun.api.crud.Secrets().store_project_secrets(
228
+ project,
229
+ mlrun.api.schemas.SecretsData(
230
+ provider=mlrun.api.schemas.SecretProviderName.kubernetes,
231
+ secrets={secret_key: json.dumps(notification_object.params)},
232
+ ),
233
+ allow_internal_secrets=True,
234
+ )
235
+ notification_object.params = {"secret": secret_key}
236
+
237
+ return notification_object
238
+
239
+
240
+ def unmask_notification_params_secret_on_task(run):
241
+ if isinstance(run, dict):
242
+ run = mlrun.model.RunObject.from_dict(run)
243
+
244
+ run.spec.notifications = [
245
+ unmask_notification_params_secret(run.metadata.project, notification)
246
+ for notification in run.spec.notifications
247
+ ]
248
+ return run
249
+
250
+
251
+ def unmask_notification_params_secret(
252
+ project: str, notification_object: mlrun.model.Notification
253
+ ) -> mlrun.model.Notification:
254
+ params = notification_object.params or {}
255
+ params_secret = params.get("secret", "")
256
+ if not params_secret:
257
+ return notification_object
258
+
259
+ k8s = mlrun.api.utils.singletons.k8s.get_k8s()
260
+ if not k8s:
261
+ raise mlrun.errors.MLRunRuntimeError(
262
+ "Not running in k8s environment, cannot load notification params secret"
263
+ )
264
+
265
+ notification_object.params = json.loads(
266
+ mlrun.api.crud.Secrets().get_project_secret(
267
+ project,
268
+ mlrun.api.schemas.SecretProviderName.kubernetes,
269
+ secret_key=params_secret,
270
+ allow_internal_secrets=True,
271
+ allow_secrets_from_k8s=True,
272
+ )
273
+ )
274
+
275
+ return notification_object
276
+
277
+
278
+ def delete_notification_params_secret(
279
+ project: str, notification_object: mlrun.model.Notification
280
+ ) -> None:
281
+ params = notification_object.params or {}
282
+ params_secret = params.get("secret", "")
283
+ if not params_secret:
284
+ return
285
+
286
+ k8s = mlrun.api.utils.singletons.k8s.get_k8s()
287
+ if not k8s:
288
+ raise mlrun.errors.MLRunRuntimeError(
289
+ "Not running in k8s environment, cannot delete notification params secret"
290
+ )
291
+
292
+ mlrun.api.crud.Secrets().delete_project_secret(
293
+ project,
294
+ mlrun.api.schemas.SecretProviderName.kubernetes,
295
+ secret_key=params_secret,
296
+ allow_internal_secrets=True,
297
+ allow_secrets_from_k8s=True,
298
+ )
299
+
300
+
199
301
  def apply_enrichment_and_validation_on_function(
200
302
  function,
201
303
  auth_info: mlrun.api.schemas.AuthInfo,
@@ -22,6 +22,7 @@ from .functions import Functions
22
22
  from .logs import Logs
23
23
  from .marketplace import Marketplace
24
24
  from .model_monitoring import ModelEndpoints
25
+ from .notifications import Notifications
25
26
  from .pipelines import Pipelines
26
27
  from .projects import Projects
27
28
  from .runs import Runs
@@ -13,6 +13,7 @@
13
13
  # limitations under the License.
14
14
  #
15
15
  import json
16
+ from typing import Any, Dict, List, Optional, Tuple
16
17
 
17
18
  import mlrun.errors
18
19
  import mlrun.utils.singleton
@@ -121,43 +122,79 @@ class Marketplace(metaclass=mlrun.utils.singleton.Singleton):
121
122
  return source_secrets
122
123
 
123
124
  @staticmethod
124
- def _transform_catalog_dict_to_schema(source, catalog_dict):
125
- catalog_dict = catalog_dict.get("functions")
126
- if not catalog_dict:
127
- raise mlrun.errors.MLRunInternalServerError(
128
- "Invalid catalog file - no 'functions' section found."
125
+ def _get_asset_full_path(
126
+ source: MarketplaceSource, item: MarketplaceItem, asset: str
127
+ ):
128
+ """
129
+ Combining the item path with the asset path.
130
+
131
+ :param source: Marketplace source object.
132
+ :param item: The relevant item to get the asset from.
133
+ :param asset: The asset name
134
+ :return: Full path to the asset, relative to the item directory.
135
+ """
136
+ asset_path = item.spec.assets.get(asset, None)
137
+ if not asset_path:
138
+ raise mlrun.errors.MLRunNotFoundError(
139
+ f"Asset={asset} not found. "
140
+ f"item={item.metadata.name}, version={item.metadata.version}, tag={item.metadata.tag}"
129
141
  )
142
+ item_path = item.metadata.get_relative_path()
143
+ return source.get_full_uri(item_path + asset_path)
130
144
 
131
- catalog = MarketplaceCatalog(catalog=[])
132
- # Loop over channels, then per function extract versions.
133
- for channel_name in catalog_dict:
134
- channel_dict = catalog_dict[channel_name]
135
- for function_name in channel_dict:
136
- function_dict = channel_dict[function_name]
137
- for version_tag in function_dict:
138
- version_dict = function_dict[version_tag]
139
- function_details_dict = version_dict.copy()
140
- spec_dict = function_details_dict.pop("spec", None)
141
- metadata = MarketplaceItemMetadata(
142
- channel=channel_name, tag=version_tag, **function_details_dict
143
- )
144
- item_uri = source.get_full_uri(metadata.get_relative_path())
145
- spec = MarketplaceItemSpec(item_uri=item_uri, **spec_dict)
146
- item = MarketplaceItem(
147
- metadata=metadata, spec=spec, status=ObjectStatus()
148
- )
149
- catalog.catalog.append(item)
145
+ @staticmethod
146
+ def _transform_catalog_dict_to_schema(
147
+ source: MarketplaceSource, catalog_dict: Dict[str, Any]
148
+ ) -> MarketplaceCatalog:
149
+ """
150
+ Transforms catalog dictionary to MarketplaceCatalog schema
151
+ :param source: Marketplace source object.
152
+ :param catalog_dict: raw catalog dict, top level keys are item names,
153
+ second level keys are version tags ("latest, "1.1.0", ...) and
154
+ bottom level keys include spec as a dict and all the rest is considered as metadata.
155
+ :return: catalog object
156
+ """
157
+ catalog = MarketplaceCatalog(catalog=[], channel=source.spec.channel)
158
+ # Loop over objects, then over object versions.
159
+ for object_name, object_dict in catalog_dict.items():
160
+ for version_tag, version_dict in object_dict.items():
161
+ object_details_dict = version_dict.copy()
162
+ spec_dict = object_details_dict.pop("spec", {})
163
+ assets = object_details_dict.pop("assets", {})
164
+ metadata = MarketplaceItemMetadata(
165
+ tag=version_tag, **object_details_dict
166
+ )
167
+ item_uri = source.get_full_uri(metadata.get_relative_path())
168
+ spec = MarketplaceItemSpec(
169
+ item_uri=item_uri, assets=assets, **spec_dict
170
+ )
171
+ item = MarketplaceItem(
172
+ metadata=metadata,
173
+ spec=spec,
174
+ status=ObjectStatus(),
175
+ )
176
+ catalog.catalog.append(item)
150
177
 
151
178
  return catalog
152
179
 
153
180
  def get_source_catalog(
154
181
  self,
155
182
  source: MarketplaceSource,
156
- channel=None,
157
- version=None,
158
- tag=None,
159
- force_refresh=False,
183
+ version: Optional[str] = None,
184
+ tag: Optional[str] = None,
185
+ force_refresh: bool = False,
160
186
  ) -> MarketplaceCatalog:
187
+ """
188
+ Getting the catalog object by source.
189
+ If version and/or tag are given, the catalog will be filtered accordingly.
190
+
191
+ :param source: Marketplace source object.
192
+ :param version: version of items to filter by
193
+ :param tag: tag of items to filter by
194
+ :param force_refresh: if True, the catalog will be loaded from source always,
195
+ otherwise will be pulled from db (if loaded before)
196
+ :return: catalog object
197
+ """
161
198
  source_name = source.metadata.name
162
199
  if not self._catalogs.get(source_name) or force_refresh:
163
200
  url = source.get_catalog_uri()
@@ -169,12 +206,12 @@ class Marketplace(metaclass=mlrun.utils.singleton.Singleton):
169
206
  else:
170
207
  catalog = self._catalogs[source_name]
171
208
 
172
- result_catalog = MarketplaceCatalog(catalog=[])
209
+ result_catalog = MarketplaceCatalog(catalog=[], channel=source.spec.channel)
173
210
  for item in catalog.catalog:
174
- if (
175
- (channel is None or item.metadata.channel == channel)
176
- and (tag is None or item.metadata.tag == tag)
177
- and (version is None or item.metadata.version == version)
211
+ # Because tag and version are optionals,
212
+ # we filter the catalog by one of them with priority to tag
213
+ if (tag is None or item.metadata.tag == tag) and (
214
+ version is None or item.metadata.version == version
178
215
  ):
179
216
  result_catalog.catalog.append(item)
180
217
 
@@ -183,25 +220,55 @@ class Marketplace(metaclass=mlrun.utils.singleton.Singleton):
183
220
  def get_item(
184
221
  self,
185
222
  source: MarketplaceSource,
186
- item_name,
187
- channel,
188
- version=None,
189
- tag=None,
190
- force_refresh=False,
223
+ item_name: str,
224
+ version: Optional[str] = None,
225
+ tag: Optional[str] = None,
226
+ force_refresh: bool = False,
191
227
  ) -> MarketplaceItem:
192
- catalog = self.get_source_catalog(source, channel, version, tag, force_refresh)
193
- items = [item for item in catalog.catalog if item.metadata.name == item_name]
194
- if not items:
228
+ """
229
+ Retrieve item from source. The item is filtered by tag and version.
230
+
231
+ :param source: Marketplace source object
232
+ :param item_name: name of the item to retrieve
233
+ :param version: version of the item
234
+ :param tag: tag of the item
235
+ :param force_refresh: if True, the catalog will be loaded from source always,
236
+ otherwise will be pulled from db (if loaded before)
237
+
238
+ :return: marketplace item object
239
+
240
+ :raise if the number of collected items from catalog is not exactly one.
241
+ """
242
+ catalog = self.get_source_catalog(source, version, tag, force_refresh)
243
+ items = self._get_catalog_items_filtered_by_name(catalog.catalog, item_name)
244
+ num_items = len(items)
245
+
246
+ if not num_items:
195
247
  raise mlrun.errors.MLRunNotFoundError(
196
- f"Item not found. source={item_name}, channel={channel}, version={version}"
248
+ f"Item not found. source={item_name}, version={version}"
197
249
  )
198
- if len(items) > 1:
250
+ if num_items > 1:
199
251
  raise mlrun.errors.MLRunInvalidArgumentError(
200
252
  "Query resulted in more than 1 catalog items. "
201
- + f"source={item_name}, channel={channel}, version={version}, tag={tag}"
253
+ + f"source={item_name}, version={version}, tag={tag}"
202
254
  )
203
255
  return items[0]
204
256
 
257
+ @staticmethod
258
+ def _get_catalog_items_filtered_by_name(
259
+ catalog: List[MarketplaceItem],
260
+ item_name: str,
261
+ ) -> List[MarketplaceItem]:
262
+ """
263
+ Retrieve items from catalog filtered by name
264
+
265
+ :param catalog: list of items
266
+ :param item_name: item name to filter by
267
+
268
+ :return: list of item objects from catalog
269
+ """
270
+ return [item for item in catalog if item.metadata.name == item_name]
271
+
205
272
  def get_item_object_using_source_credentials(self, source: MarketplaceSource, url):
206
273
  credentials = self._get_source_credentials(source.metadata.name)
207
274
 
@@ -219,3 +286,25 @@ class Marketplace(metaclass=mlrun.utils.singleton.Singleton):
219
286
  else:
220
287
  catalog_data = mlrun.run.get_object(url=url, secrets=credentials)
221
288
  return catalog_data
289
+
290
+ def get_asset(
291
+ self,
292
+ source: MarketplaceSource,
293
+ item: MarketplaceItem,
294
+ asset_name: str,
295
+ ) -> Tuple[bytes, str]:
296
+ """
297
+ Retrieve asset object from marketplace source.
298
+
299
+ :param source: marketplace source
300
+ :param item: marketplace item which contains the assets
301
+ :param asset_name: asset name, like source, example, etc.
302
+
303
+ :return: tuple of asset as bytes and url of asset
304
+ """
305
+ credentials = self._get_source_credentials(source.metadata.name)
306
+ asset_path = self._get_asset_full_path(source, item, asset_name)
307
+ return (
308
+ mlrun.run.get_object(url=asset_path, secrets=credentials),
309
+ asset_path,
310
+ )
@@ -0,0 +1,80 @@
1
+ # Copyright 2018 Iguazio
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ import typing
16
+
17
+ import sqlalchemy.orm
18
+
19
+ import mlrun.api.api.utils
20
+ import mlrun.api.utils.singletons.db
21
+ import mlrun.utils.singleton
22
+
23
+
24
+ class Notifications(
25
+ metaclass=mlrun.utils.singleton.Singleton,
26
+ ):
27
+ def store_run_notifications(
28
+ self,
29
+ session: sqlalchemy.orm.Session,
30
+ notification_objects: typing.List[mlrun.model.Notification],
31
+ run_uid: str,
32
+ project: str = None,
33
+ ):
34
+ project = project or mlrun.mlconf.default_project
35
+ notification_objects_to_store = []
36
+ for notification_object in notification_objects:
37
+ notification_objects_to_store.append(
38
+ mlrun.api.api.utils.mask_notification_params_with_secret(
39
+ project, run_uid, notification_object
40
+ )
41
+ )
42
+
43
+ mlrun.api.utils.singletons.db.get_db().store_run_notifications(
44
+ session, notification_objects_to_store, run_uid, project
45
+ )
46
+
47
+ def list_run_notifications(
48
+ self,
49
+ session: sqlalchemy.orm.Session,
50
+ run_uid: str,
51
+ project: str = "",
52
+ ) -> typing.List[mlrun.model.Notification]:
53
+ project = project or mlrun.mlconf.default_project
54
+ return mlrun.api.utils.singletons.db.get_db().list_run_notifications(
55
+ session, run_uid, project
56
+ )
57
+
58
+ def delete_run_notifications(
59
+ self,
60
+ session: sqlalchemy.orm.Session,
61
+ name: str = None,
62
+ run_uid: str = None,
63
+ project: str = None,
64
+ ):
65
+ project = project or mlrun.mlconf.default_project
66
+
67
+ # Delete notification param project secret
68
+ notifications = [
69
+ notification
70
+ for notification in self.list_run_notifications(session, run_uid, project)
71
+ if notification.name == name
72
+ ]
73
+ if notifications:
74
+ # unique constraint on name, run_uid, project, so the list will contain one item at most
75
+ notification = notifications[0]
76
+ mlrun.api.api.utils.delete_notification_params_secret(project, notification)
77
+
78
+ mlrun.api.utils.singletons.db.get_db().delete_run_notifications(
79
+ session, name, run_uid, project
80
+ )
mlrun/api/crud/runs.py CHANGED
@@ -127,6 +127,7 @@ class Runs(
127
127
  max_partitions: int = 0,
128
128
  requested_logs: bool = None,
129
129
  return_as_run_structs: bool = True,
130
+ with_notifications: bool = False,
130
131
  ):
131
132
  project = project or mlrun.mlconf.default_project
132
133
  return mlrun.api.utils.singletons.db.get_db().list_runs(
@@ -150,6 +151,7 @@ class Runs(
150
151
  max_partitions,
151
152
  requested_logs,
152
153
  return_as_run_structs,
154
+ with_notifications,
153
155
  )
154
156
 
155
157
  def delete_run(
mlrun/api/crud/secrets.py CHANGED
@@ -31,6 +31,7 @@ class SecretsClientType(str, enum.Enum):
31
31
  model_monitoring = "model-monitoring"
32
32
  service_accounts = "service-accounts"
33
33
  marketplace = "marketplace"
34
+ notifications = "notifications"
34
35
 
35
36
 
36
37
  class Secrets(