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.
- mlrun/__main__.py +57 -4
- mlrun/api/api/endpoints/marketplace.py +57 -4
- mlrun/api/api/endpoints/runs.py +2 -0
- mlrun/api/api/utils.py +102 -0
- mlrun/api/crud/__init__.py +1 -0
- mlrun/api/crud/marketplace.py +133 -44
- mlrun/api/crud/notifications.py +80 -0
- mlrun/api/crud/runs.py +2 -0
- mlrun/api/crud/secrets.py +1 -0
- mlrun/api/db/base.py +32 -0
- mlrun/api/db/session.py +3 -11
- mlrun/api/db/sqldb/db.py +162 -1
- mlrun/api/db/sqldb/models/models_mysql.py +41 -0
- mlrun/api/db/sqldb/models/models_sqlite.py +35 -0
- mlrun/api/main.py +54 -1
- mlrun/api/migrations_mysql/versions/c905d15bd91d_notifications.py +70 -0
- mlrun/api/migrations_sqlite/versions/959ae00528ad_notifications.py +61 -0
- mlrun/api/schemas/__init__.py +1 -0
- mlrun/api/schemas/marketplace.py +18 -8
- mlrun/api/{db/filedb/__init__.py → schemas/notification.py} +17 -1
- mlrun/api/utils/singletons/db.py +8 -14
- mlrun/builder.py +37 -26
- mlrun/config.py +12 -2
- mlrun/data_types/spark.py +9 -2
- mlrun/datastore/base.py +10 -1
- mlrun/datastore/sources.py +1 -1
- mlrun/db/__init__.py +6 -4
- mlrun/db/base.py +1 -2
- mlrun/db/httpdb.py +32 -6
- mlrun/db/nopdb.py +463 -0
- mlrun/db/sqldb.py +47 -7
- mlrun/execution.py +3 -0
- mlrun/feature_store/api.py +26 -12
- mlrun/feature_store/common.py +1 -1
- mlrun/feature_store/steps.py +110 -13
- mlrun/k8s_utils.py +10 -0
- mlrun/model.py +43 -0
- mlrun/projects/operations.py +5 -2
- mlrun/projects/pipelines.py +4 -3
- mlrun/projects/project.py +50 -10
- mlrun/run.py +5 -4
- mlrun/runtimes/__init__.py +2 -6
- mlrun/runtimes/base.py +82 -31
- mlrun/runtimes/function.py +22 -0
- mlrun/runtimes/kubejob.py +10 -8
- mlrun/runtimes/serving.py +1 -1
- mlrun/runtimes/sparkjob/__init__.py +0 -1
- mlrun/runtimes/sparkjob/abstract.py +0 -2
- mlrun/serving/states.py +2 -2
- mlrun/utils/helpers.py +1 -1
- mlrun/utils/notifications/notification/__init__.py +1 -1
- mlrun/utils/notifications/notification/base.py +14 -13
- mlrun/utils/notifications/notification/console.py +6 -3
- mlrun/utils/notifications/notification/git.py +19 -12
- mlrun/utils/notifications/notification/ipython.py +6 -3
- mlrun/utils/notifications/notification/slack.py +13 -12
- mlrun/utils/notifications/notification_pusher.py +185 -37
- mlrun/utils/version/version.json +2 -2
- {mlrun-1.3.1rc5.dist-info → mlrun-1.4.0rc2.dist-info}/METADATA +6 -2
- {mlrun-1.3.1rc5.dist-info → mlrun-1.4.0rc2.dist-info}/RECORD +64 -63
- mlrun/api/db/filedb/db.py +0 -518
- mlrun/db/filedb.py +0 -899
- mlrun/runtimes/sparkjob/spark2job.py +0 -59
- {mlrun-1.3.1rc5.dist-info → mlrun-1.4.0rc2.dist-info}/LICENSE +0 -0
- {mlrun-1.3.1rc5.dist-info → mlrun-1.4.0rc2.dist-info}/WHEEL +0 -0
- {mlrun-1.3.1rc5.dist-info → mlrun-1.4.0rc2.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
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)
|
mlrun/api/api/endpoints/runs.py
CHANGED
|
@@ -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,
|
mlrun/api/crud/__init__.py
CHANGED
|
@@ -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
|
mlrun/api/crud/marketplace.py
CHANGED
|
@@ -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
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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},
|
|
248
|
+
f"Item not found. source={item_name}, version={version}"
|
|
197
249
|
)
|
|
198
|
-
if
|
|
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},
|
|
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(
|