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.
- mlrun/__init__.py +11 -1
- mlrun/__main__.py +25 -111
- mlrun/{datastore/helpers.py → alerts/__init__.py} +2 -5
- mlrun/alerts/alert.py +144 -0
- mlrun/api/schemas/__init__.py +4 -3
- mlrun/artifacts/__init__.py +8 -3
- mlrun/artifacts/base.py +38 -254
- mlrun/artifacts/dataset.py +9 -190
- mlrun/artifacts/manager.py +41 -47
- mlrun/artifacts/model.py +30 -158
- mlrun/artifacts/plots.py +23 -380
- mlrun/common/constants.py +68 -0
- mlrun/common/formatters/__init__.py +19 -0
- mlrun/{model_monitoring/stores/models/sqlite.py → common/formatters/artifact.py} +6 -8
- mlrun/common/formatters/base.py +78 -0
- mlrun/common/formatters/function.py +41 -0
- mlrun/common/formatters/pipeline.py +53 -0
- mlrun/common/formatters/project.py +51 -0
- mlrun/{runtimes → common/runtimes}/constants.py +32 -4
- mlrun/common/schemas/__init__.py +25 -4
- mlrun/common/schemas/alert.py +203 -0
- mlrun/common/schemas/api_gateway.py +148 -0
- mlrun/common/schemas/artifact.py +15 -5
- mlrun/common/schemas/auth.py +8 -2
- mlrun/common/schemas/client_spec.py +2 -0
- mlrun/common/schemas/frontend_spec.py +1 -0
- mlrun/common/schemas/function.py +4 -0
- mlrun/common/schemas/hub.py +7 -9
- mlrun/common/schemas/model_monitoring/__init__.py +19 -3
- mlrun/common/schemas/model_monitoring/constants.py +96 -26
- mlrun/common/schemas/model_monitoring/grafana.py +9 -5
- mlrun/common/schemas/model_monitoring/model_endpoints.py +86 -2
- mlrun/{runtimes/mpijob/v1alpha1.py → common/schemas/pagination.py} +10 -13
- mlrun/common/schemas/pipeline.py +0 -9
- mlrun/common/schemas/project.py +22 -21
- mlrun/common/types.py +7 -1
- mlrun/config.py +87 -19
- mlrun/data_types/data_types.py +4 -0
- mlrun/data_types/to_pandas.py +9 -9
- mlrun/datastore/__init__.py +5 -8
- mlrun/datastore/alibaba_oss.py +130 -0
- mlrun/datastore/azure_blob.py +4 -5
- mlrun/datastore/base.py +69 -30
- mlrun/datastore/datastore.py +10 -2
- mlrun/datastore/datastore_profile.py +90 -6
- mlrun/datastore/google_cloud_storage.py +1 -1
- mlrun/datastore/hdfs.py +5 -0
- mlrun/datastore/inmem.py +2 -2
- mlrun/datastore/redis.py +2 -2
- mlrun/datastore/s3.py +5 -0
- mlrun/datastore/snowflake_utils.py +43 -0
- mlrun/datastore/sources.py +172 -44
- mlrun/datastore/store_resources.py +7 -7
- mlrun/datastore/targets.py +285 -41
- mlrun/datastore/utils.py +68 -5
- mlrun/datastore/v3io.py +27 -50
- mlrun/db/auth_utils.py +152 -0
- mlrun/db/base.py +149 -14
- mlrun/db/factory.py +1 -1
- mlrun/db/httpdb.py +608 -178
- mlrun/db/nopdb.py +191 -7
- mlrun/errors.py +11 -0
- mlrun/execution.py +37 -20
- mlrun/feature_store/__init__.py +0 -2
- mlrun/feature_store/api.py +21 -52
- mlrun/feature_store/feature_set.py +48 -23
- mlrun/feature_store/feature_vector.py +2 -1
- mlrun/feature_store/ingestion.py +7 -6
- mlrun/feature_store/retrieval/base.py +9 -4
- mlrun/feature_store/retrieval/conversion.py +9 -9
- mlrun/feature_store/retrieval/dask_merger.py +2 -0
- mlrun/feature_store/retrieval/job.py +9 -3
- mlrun/feature_store/retrieval/local_merger.py +2 -0
- mlrun/feature_store/retrieval/spark_merger.py +34 -24
- mlrun/feature_store/steps.py +30 -19
- mlrun/features.py +4 -13
- mlrun/frameworks/_dl_common/loggers/tensorboard_logger.py +7 -12
- mlrun/frameworks/auto_mlrun/auto_mlrun.py +2 -2
- mlrun/frameworks/lgbm/__init__.py +1 -1
- mlrun/frameworks/lgbm/callbacks/callback.py +2 -4
- mlrun/frameworks/lgbm/model_handler.py +1 -1
- mlrun/frameworks/parallel_coordinates.py +2 -1
- mlrun/frameworks/pytorch/__init__.py +2 -2
- mlrun/frameworks/sklearn/__init__.py +1 -1
- mlrun/frameworks/tf_keras/__init__.py +5 -2
- mlrun/frameworks/tf_keras/callbacks/logging_callback.py +1 -1
- mlrun/frameworks/tf_keras/mlrun_interface.py +2 -2
- mlrun/frameworks/xgboost/__init__.py +1 -1
- mlrun/k8s_utils.py +10 -11
- mlrun/launcher/__init__.py +1 -1
- mlrun/launcher/base.py +6 -5
- mlrun/launcher/client.py +8 -6
- mlrun/launcher/factory.py +1 -1
- mlrun/launcher/local.py +9 -3
- mlrun/launcher/remote.py +9 -3
- mlrun/lists.py +6 -2
- mlrun/model.py +58 -19
- mlrun/model_monitoring/__init__.py +1 -1
- mlrun/model_monitoring/api.py +127 -301
- mlrun/model_monitoring/application.py +5 -296
- mlrun/model_monitoring/applications/__init__.py +11 -0
- mlrun/model_monitoring/applications/_application_steps.py +157 -0
- mlrun/model_monitoring/applications/base.py +282 -0
- mlrun/model_monitoring/applications/context.py +214 -0
- mlrun/model_monitoring/applications/evidently_base.py +211 -0
- mlrun/model_monitoring/applications/histogram_data_drift.py +224 -93
- mlrun/model_monitoring/applications/results.py +99 -0
- mlrun/model_monitoring/controller.py +30 -36
- mlrun/model_monitoring/db/__init__.py +18 -0
- mlrun/model_monitoring/{stores → db/stores}/__init__.py +43 -36
- mlrun/model_monitoring/db/stores/base/__init__.py +15 -0
- mlrun/model_monitoring/{stores/model_endpoint_store.py → db/stores/base/store.py} +58 -32
- mlrun/model_monitoring/db/stores/sqldb/__init__.py +13 -0
- mlrun/model_monitoring/db/stores/sqldb/models/__init__.py +71 -0
- mlrun/model_monitoring/{stores → db/stores/sqldb}/models/base.py +109 -5
- mlrun/model_monitoring/db/stores/sqldb/models/mysql.py +88 -0
- mlrun/model_monitoring/{stores/models/mysql.py → db/stores/sqldb/models/sqlite.py} +19 -13
- mlrun/model_monitoring/db/stores/sqldb/sql_store.py +684 -0
- mlrun/model_monitoring/db/stores/v3io_kv/__init__.py +13 -0
- mlrun/model_monitoring/{stores/kv_model_endpoint_store.py → db/stores/v3io_kv/kv_store.py} +302 -155
- mlrun/model_monitoring/db/tsdb/__init__.py +100 -0
- mlrun/model_monitoring/db/tsdb/base.py +329 -0
- mlrun/model_monitoring/db/tsdb/helpers.py +30 -0
- mlrun/model_monitoring/db/tsdb/tdengine/__init__.py +15 -0
- mlrun/model_monitoring/db/tsdb/tdengine/schemas.py +240 -0
- mlrun/model_monitoring/db/tsdb/tdengine/stream_graph_steps.py +45 -0
- mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py +397 -0
- mlrun/model_monitoring/db/tsdb/v3io/__init__.py +15 -0
- mlrun/model_monitoring/db/tsdb/v3io/stream_graph_steps.py +117 -0
- mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +630 -0
- mlrun/model_monitoring/evidently_application.py +6 -118
- mlrun/model_monitoring/features_drift_table.py +34 -22
- mlrun/model_monitoring/helpers.py +100 -7
- mlrun/model_monitoring/model_endpoint.py +3 -2
- mlrun/model_monitoring/stream_processing.py +93 -228
- mlrun/model_monitoring/tracking_policy.py +7 -1
- mlrun/model_monitoring/writer.py +152 -124
- mlrun/package/packagers_manager.py +1 -0
- mlrun/package/utils/_formatter.py +2 -2
- mlrun/platforms/__init__.py +11 -10
- mlrun/platforms/iguazio.py +21 -202
- mlrun/projects/operations.py +30 -16
- mlrun/projects/pipelines.py +92 -99
- mlrun/projects/project.py +757 -268
- mlrun/render.py +15 -14
- mlrun/run.py +160 -162
- mlrun/runtimes/__init__.py +55 -3
- mlrun/runtimes/base.py +33 -19
- mlrun/runtimes/databricks_job/databricks_wrapper.py +1 -1
- mlrun/runtimes/funcdoc.py +0 -28
- mlrun/runtimes/kubejob.py +28 -122
- mlrun/runtimes/local.py +5 -2
- mlrun/runtimes/mpijob/__init__.py +0 -20
- mlrun/runtimes/mpijob/abstract.py +8 -8
- mlrun/runtimes/mpijob/v1.py +1 -1
- mlrun/runtimes/nuclio/__init__.py +1 -0
- mlrun/runtimes/nuclio/api_gateway.py +709 -0
- mlrun/runtimes/nuclio/application/__init__.py +15 -0
- mlrun/runtimes/nuclio/application/application.py +523 -0
- mlrun/runtimes/nuclio/application/reverse_proxy.go +95 -0
- mlrun/runtimes/nuclio/function.py +98 -58
- mlrun/runtimes/nuclio/serving.py +36 -42
- mlrun/runtimes/pod.py +196 -45
- mlrun/runtimes/remotesparkjob.py +1 -1
- mlrun/runtimes/sparkjob/spark3job.py +1 -1
- mlrun/runtimes/utils.py +6 -73
- mlrun/secrets.py +6 -2
- mlrun/serving/remote.py +2 -3
- mlrun/serving/routers.py +7 -4
- mlrun/serving/server.py +7 -8
- mlrun/serving/states.py +73 -43
- mlrun/serving/v2_serving.py +8 -7
- mlrun/track/tracker.py +2 -1
- mlrun/utils/async_http.py +25 -5
- mlrun/utils/helpers.py +141 -75
- mlrun/utils/http.py +1 -1
- mlrun/utils/logger.py +39 -7
- mlrun/utils/notifications/notification/__init__.py +14 -9
- mlrun/utils/notifications/notification/base.py +12 -0
- mlrun/utils/notifications/notification/console.py +2 -0
- mlrun/utils/notifications/notification/git.py +3 -1
- mlrun/utils/notifications/notification/ipython.py +2 -0
- mlrun/utils/notifications/notification/slack.py +101 -21
- mlrun/utils/notifications/notification/webhook.py +11 -1
- mlrun/utils/notifications/notification_pusher.py +147 -16
- mlrun/utils/retryer.py +3 -2
- mlrun/utils/v3io_clients.py +0 -1
- mlrun/utils/version/version.json +2 -2
- {mlrun-1.7.0rc4.dist-info → mlrun-1.7.0rc20.dist-info}/METADATA +33 -18
- mlrun-1.7.0rc20.dist-info/RECORD +353 -0
- {mlrun-1.7.0rc4.dist-info → mlrun-1.7.0rc20.dist-info}/WHEEL +1 -1
- mlrun/kfpops.py +0 -868
- mlrun/model_monitoring/batch.py +0 -974
- mlrun/model_monitoring/stores/models/__init__.py +0 -27
- mlrun/model_monitoring/stores/sql_model_endpoint_store.py +0 -382
- mlrun/platforms/other.py +0 -305
- mlrun-1.7.0rc4.dist-info/RECORD +0 -321
- {mlrun-1.7.0rc4.dist-info → mlrun-1.7.0rc20.dist-info}/LICENSE +0 -0
- {mlrun-1.7.0rc4.dist-info → mlrun-1.7.0rc20.dist-info}/entry_points.txt +0 -0
- {mlrun-1.7.0rc4.dist-info → mlrun-1.7.0rc20.dist-info}/top_level.txt +0 -0
mlrun/serving/states.py
CHANGED
|
@@ -14,19 +14,21 @@
|
|
|
14
14
|
|
|
15
15
|
__all__ = ["TaskStep", "RouterStep", "RootFlowStep", "ErrorStep"]
|
|
16
16
|
|
|
17
|
-
import asyncio
|
|
18
17
|
import os
|
|
19
18
|
import pathlib
|
|
20
19
|
import traceback
|
|
21
20
|
from copy import copy, deepcopy
|
|
22
21
|
from inspect import getfullargspec, signature
|
|
23
|
-
from typing import Union
|
|
22
|
+
from typing import Any, Union
|
|
24
23
|
|
|
25
24
|
import mlrun
|
|
26
25
|
|
|
27
26
|
from ..config import config
|
|
28
27
|
from ..datastore import get_stream_pusher
|
|
29
|
-
from ..datastore.utils import
|
|
28
|
+
from ..datastore.utils import (
|
|
29
|
+
get_kafka_brokers_from_dict,
|
|
30
|
+
parse_kafka_url,
|
|
31
|
+
)
|
|
30
32
|
from ..errors import MLRunInvalidArgumentError, err_to_str
|
|
31
33
|
from ..model import ModelObj, ObjectDict
|
|
32
34
|
from ..platforms.iguazio import parse_path
|
|
@@ -325,7 +327,7 @@ class BaseStep(ModelObj):
|
|
|
325
327
|
parent = self._parent
|
|
326
328
|
else:
|
|
327
329
|
raise GraphError(
|
|
328
|
-
f"step {self.name} parent is not set or
|
|
330
|
+
f"step {self.name} parent is not set or it's not part of a graph"
|
|
329
331
|
)
|
|
330
332
|
|
|
331
333
|
name, step = params_to_step(
|
|
@@ -347,6 +349,36 @@ class BaseStep(ModelObj):
|
|
|
347
349
|
parent._last_added = step
|
|
348
350
|
return step
|
|
349
351
|
|
|
352
|
+
def set_flow(
|
|
353
|
+
self,
|
|
354
|
+
steps: list[Union[str, StepToDict, dict[str, Any]]],
|
|
355
|
+
force: bool = False,
|
|
356
|
+
):
|
|
357
|
+
"""set list of steps as downstream from this step, in the order specified. This will overwrite any existing
|
|
358
|
+
downstream steps.
|
|
359
|
+
|
|
360
|
+
:param steps: list of steps to follow this one
|
|
361
|
+
:param force: whether to overwrite existing downstream steps. If False, this method will fail if any downstream
|
|
362
|
+
steps have already been defined. Defaults to False.
|
|
363
|
+
:return: the last step added to the flow
|
|
364
|
+
|
|
365
|
+
example:
|
|
366
|
+
The below code sets the downstream nodes of step1 by using a list of steps (provided to `set_flow()`) and a
|
|
367
|
+
single step (provided to `to()`), resulting in the graph (step1 -> step2 -> step3 -> step4).
|
|
368
|
+
Notice that using `force=True` is required in case step1 already had downstream nodes (e.g. if the existing
|
|
369
|
+
graph is step1 -> step2_old) and that following the execution of this code the existing downstream steps
|
|
370
|
+
are removed. If the intention is to split the graph (and not to overwrite), please use `to()`.
|
|
371
|
+
|
|
372
|
+
step1.set_flow(
|
|
373
|
+
[
|
|
374
|
+
dict(name="step2", handler="step2_handler"),
|
|
375
|
+
dict(name="step3", class_name="Step3Class"),
|
|
376
|
+
],
|
|
377
|
+
force=True,
|
|
378
|
+
).to(dict(name="step4", class_name="Step4Class"))
|
|
379
|
+
"""
|
|
380
|
+
raise NotImplementedError("set_flow() can only be called on a FlowStep")
|
|
381
|
+
|
|
350
382
|
|
|
351
383
|
class TaskStep(BaseStep):
|
|
352
384
|
"""task execution step, runs a class or handler"""
|
|
@@ -1161,19 +1193,11 @@ class FlowStep(BaseStep):
|
|
|
1161
1193
|
if self._controller:
|
|
1162
1194
|
# async flow (using storey)
|
|
1163
1195
|
event._awaitable_result = None
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
return resp.await_result()
|
|
1170
|
-
else:
|
|
1171
|
-
resp_awaitable = self._controller.emit(
|
|
1172
|
-
event, await_result=self._wait_for_result
|
|
1173
|
-
)
|
|
1174
|
-
if self._wait_for_result:
|
|
1175
|
-
return resp_awaitable
|
|
1176
|
-
return self._await_and_return_id(resp_awaitable, event)
|
|
1196
|
+
resp = self._controller.emit(
|
|
1197
|
+
event, return_awaitable_result=self._wait_for_result
|
|
1198
|
+
)
|
|
1199
|
+
if self._wait_for_result and resp:
|
|
1200
|
+
return resp.await_result()
|
|
1177
1201
|
event = copy(event)
|
|
1178
1202
|
event.body = {"id": event.id}
|
|
1179
1203
|
return event
|
|
@@ -1213,18 +1237,9 @@ class FlowStep(BaseStep):
|
|
|
1213
1237
|
"""wait for completion of run in async flows"""
|
|
1214
1238
|
|
|
1215
1239
|
if self._controller:
|
|
1216
|
-
if
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
if hasattr(self._controller, "terminate"):
|
|
1220
|
-
await self._controller.terminate()
|
|
1221
|
-
return await self._controller.await_termination()
|
|
1222
|
-
|
|
1223
|
-
return terminate_and_await_termination()
|
|
1224
|
-
else:
|
|
1225
|
-
if hasattr(self._controller, "terminate"):
|
|
1226
|
-
self._controller.terminate()
|
|
1227
|
-
return self._controller.await_termination()
|
|
1240
|
+
if hasattr(self._controller, "terminate"):
|
|
1241
|
+
self._controller.terminate()
|
|
1242
|
+
return self._controller.await_termination()
|
|
1228
1243
|
|
|
1229
1244
|
def plot(self, filename=None, format=None, source=None, targets=None, **kw):
|
|
1230
1245
|
"""plot/save graph using graphviz
|
|
@@ -1273,6 +1288,27 @@ class FlowStep(BaseStep):
|
|
|
1273
1288
|
)
|
|
1274
1289
|
self[step_name].after_step(name)
|
|
1275
1290
|
|
|
1291
|
+
def set_flow(
|
|
1292
|
+
self,
|
|
1293
|
+
steps: list[Union[str, StepToDict, dict[str, Any]]],
|
|
1294
|
+
force: bool = False,
|
|
1295
|
+
):
|
|
1296
|
+
if not force and self.steps:
|
|
1297
|
+
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
1298
|
+
"set_flow() called on a step that already has downstream steps. "
|
|
1299
|
+
"If you want to overwrite existing steps, set force=True."
|
|
1300
|
+
)
|
|
1301
|
+
|
|
1302
|
+
self.steps = None
|
|
1303
|
+
step = self
|
|
1304
|
+
for next_step in steps:
|
|
1305
|
+
if isinstance(next_step, dict):
|
|
1306
|
+
step = step.to(**next_step)
|
|
1307
|
+
else:
|
|
1308
|
+
step = step.to(next_step)
|
|
1309
|
+
|
|
1310
|
+
return step
|
|
1311
|
+
|
|
1276
1312
|
|
|
1277
1313
|
class RootFlowStep(FlowStep):
|
|
1278
1314
|
"""root flow step"""
|
|
@@ -1512,13 +1548,11 @@ def _init_async_objects(context, steps):
|
|
|
1512
1548
|
endpoint = None
|
|
1513
1549
|
options = {}
|
|
1514
1550
|
options.update(step.options)
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
if stream_path.startswith("kafka://") or
|
|
1519
|
-
topic,
|
|
1520
|
-
stream_path, kafka_bootstrap_servers
|
|
1521
|
-
)
|
|
1551
|
+
|
|
1552
|
+
kafka_brokers = get_kafka_brokers_from_dict(options, pop=True)
|
|
1553
|
+
|
|
1554
|
+
if stream_path.startswith("kafka://") or kafka_brokers:
|
|
1555
|
+
topic, brokers = parse_kafka_url(stream_path, kafka_brokers)
|
|
1522
1556
|
|
|
1523
1557
|
kafka_producer_options = options.pop(
|
|
1524
1558
|
"kafka_producer_options", None
|
|
@@ -1526,7 +1560,7 @@ def _init_async_objects(context, steps):
|
|
|
1526
1560
|
|
|
1527
1561
|
step._async_object = storey.KafkaTarget(
|
|
1528
1562
|
topic=topic,
|
|
1529
|
-
|
|
1563
|
+
brokers=brokers,
|
|
1530
1564
|
producer_options=kafka_producer_options,
|
|
1531
1565
|
context=context,
|
|
1532
1566
|
**options,
|
|
@@ -1568,12 +1602,8 @@ def _init_async_objects(context, steps):
|
|
|
1568
1602
|
source_args = context.get_param("source_args", {})
|
|
1569
1603
|
explicit_ack = is_explicit_ack_supported(context) and mlrun.mlconf.is_explicit_ack()
|
|
1570
1604
|
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
else:
|
|
1574
|
-
source_class = storey.AsyncEmitSource
|
|
1575
|
-
|
|
1576
|
-
default_source = source_class(
|
|
1605
|
+
# TODO: Change to AsyncEmitSource once we can drop support for nuclio<1.12.10
|
|
1606
|
+
default_source = storey.SyncEmitSource(
|
|
1577
1607
|
context=context,
|
|
1578
1608
|
explicit_ack=explicit_ack,
|
|
1579
1609
|
**source_args,
|
mlrun/serving/v2_serving.py
CHANGED
|
@@ -21,6 +21,7 @@ import mlrun.common.model_monitoring
|
|
|
21
21
|
import mlrun.common.schemas.model_monitoring
|
|
22
22
|
from mlrun.artifacts import ModelArtifact # noqa: F401
|
|
23
23
|
from mlrun.config import config
|
|
24
|
+
from mlrun.errors import err_to_str
|
|
24
25
|
from mlrun.utils import logger, now_date
|
|
25
26
|
|
|
26
27
|
from ..common.helpers import parse_versioned_object_uri
|
|
@@ -62,11 +63,11 @@ class V2ModelServer(StepToDict):
|
|
|
62
63
|
class MyClass(V2ModelServer):
|
|
63
64
|
def load(self):
|
|
64
65
|
# load and initialize the model and/or other elements
|
|
65
|
-
model_file, extra_data = self.get_model(suffix=
|
|
66
|
+
model_file, extra_data = self.get_model(suffix=".pkl")
|
|
66
67
|
self.model = load(open(model_file, "rb"))
|
|
67
68
|
|
|
68
69
|
def predict(self, request):
|
|
69
|
-
events = np.array(request[
|
|
70
|
+
events = np.array(request["inputs"])
|
|
70
71
|
dmatrix = xgb.DMatrix(events)
|
|
71
72
|
result: xgb.DMatrix = self.model.predict(dmatrix)
|
|
72
73
|
return {"outputs": result.tolist()}
|
|
@@ -175,9 +176,9 @@ class V2ModelServer(StepToDict):
|
|
|
175
176
|
::
|
|
176
177
|
|
|
177
178
|
def load(self):
|
|
178
|
-
model_file, extra_data = self.get_model(suffix=
|
|
179
|
+
model_file, extra_data = self.get_model(suffix=".pkl")
|
|
179
180
|
self.model = load(open(model_file, "rb"))
|
|
180
|
-
categories = extra_data[
|
|
181
|
+
categories = extra_data["categories"].as_df()
|
|
181
182
|
|
|
182
183
|
Parameters
|
|
183
184
|
----------
|
|
@@ -523,7 +524,7 @@ def _init_endpoint_record(
|
|
|
523
524
|
graph_server.function_uri
|
|
524
525
|
)
|
|
525
526
|
except Exception as e:
|
|
526
|
-
logger.error("Failed to parse function URI", exc=e)
|
|
527
|
+
logger.error("Failed to parse function URI", exc=err_to_str(e))
|
|
527
528
|
return None
|
|
528
529
|
|
|
529
530
|
# Generating version model value based on the model name and model version
|
|
@@ -576,9 +577,9 @@ def _init_endpoint_record(
|
|
|
576
577
|
)
|
|
577
578
|
|
|
578
579
|
except Exception as e:
|
|
579
|
-
logger.error("Failed to create endpoint record", exc=e)
|
|
580
|
+
logger.error("Failed to create endpoint record", exc=err_to_str(e))
|
|
580
581
|
|
|
581
582
|
except Exception as e:
|
|
582
|
-
logger.error("Failed to retrieve model endpoint object", exc=e)
|
|
583
|
+
logger.error("Failed to retrieve model endpoint object", exc=err_to_str(e))
|
|
583
584
|
|
|
584
585
|
return uid
|
mlrun/track/tracker.py
CHANGED
|
@@ -31,8 +31,9 @@ class Tracker(ABC):
|
|
|
31
31
|
* Offline: Manually importing models and artifacts into an MLRun project using the `import_x` methods.
|
|
32
32
|
"""
|
|
33
33
|
|
|
34
|
+
@staticmethod
|
|
34
35
|
@abstractmethod
|
|
35
|
-
def is_enabled(
|
|
36
|
+
def is_enabled() -> bool:
|
|
36
37
|
"""
|
|
37
38
|
Checks if tracker is enabled.
|
|
38
39
|
|
mlrun/utils/async_http.py
CHANGED
|
@@ -24,7 +24,7 @@ from aiohttp_retry import ExponentialRetry, RequestParams, RetryClient, RetryOpt
|
|
|
24
24
|
from aiohttp_retry.client import _RequestContext
|
|
25
25
|
|
|
26
26
|
from mlrun.config import config
|
|
27
|
-
from mlrun.errors import err_to_str
|
|
27
|
+
from mlrun.errors import err_to_str, raise_for_status
|
|
28
28
|
|
|
29
29
|
from .helpers import logger as mlrun_logger
|
|
30
30
|
|
|
@@ -46,12 +46,21 @@ class AsyncClientWithRetry(RetryClient):
|
|
|
46
46
|
*args,
|
|
47
47
|
**kwargs,
|
|
48
48
|
):
|
|
49
|
+
# do not retry on PUT / PATCH as they might have side effects (not truly idempotent)
|
|
50
|
+
blacklisted_methods = (
|
|
51
|
+
blacklisted_methods
|
|
52
|
+
if blacklisted_methods is not None
|
|
53
|
+
else [
|
|
54
|
+
"POST",
|
|
55
|
+
"PUT",
|
|
56
|
+
"PATCH",
|
|
57
|
+
]
|
|
58
|
+
)
|
|
49
59
|
super().__init__(
|
|
50
60
|
*args,
|
|
51
61
|
retry_options=ExponentialRetryOverride(
|
|
52
62
|
retry_on_exception=retry_on_exception,
|
|
53
|
-
|
|
54
|
-
blacklisted_methods=blacklisted_methods or ["POST", "PUT", "PATCH"],
|
|
63
|
+
blacklisted_methods=blacklisted_methods,
|
|
55
64
|
attempts=max_retries,
|
|
56
65
|
statuses=retry_on_status_codes,
|
|
57
66
|
factor=retry_backoff_factor,
|
|
@@ -63,6 +72,12 @@ class AsyncClientWithRetry(RetryClient):
|
|
|
63
72
|
**kwargs,
|
|
64
73
|
)
|
|
65
74
|
|
|
75
|
+
def methods_blacklist_update_required(self, new_blacklist: str):
|
|
76
|
+
self._retry_options: ExponentialRetryOverride
|
|
77
|
+
return set(self._retry_options.blacklisted_methods).difference(
|
|
78
|
+
set(new_blacklist)
|
|
79
|
+
)
|
|
80
|
+
|
|
66
81
|
def _make_requests(
|
|
67
82
|
self,
|
|
68
83
|
params_list: list[RequestParams],
|
|
@@ -173,7 +188,7 @@ class _CustomRequestContext(_RequestContext):
|
|
|
173
188
|
last_attempt = current_attempt == self._retry_options.attempts
|
|
174
189
|
if self._is_status_code_ok(response.status) or last_attempt:
|
|
175
190
|
if self._raise_for_status:
|
|
176
|
-
|
|
191
|
+
raise_for_status(response)
|
|
177
192
|
|
|
178
193
|
self._response = response
|
|
179
194
|
return response
|
|
@@ -275,6 +290,11 @@ class _CustomRequestContext(_RequestContext):
|
|
|
275
290
|
if isinstance(exc.os_error, exc_type):
|
|
276
291
|
return
|
|
277
292
|
if exc.__cause__:
|
|
278
|
-
return
|
|
293
|
+
# If the cause exception is retriable, return, otherwise, raise the original exception
|
|
294
|
+
try:
|
|
295
|
+
self.verify_exception_type(exc.__cause__)
|
|
296
|
+
except Exception:
|
|
297
|
+
raise exc
|
|
298
|
+
return
|
|
279
299
|
else:
|
|
280
300
|
raise exc
|
mlrun/utils/helpers.py
CHANGED
|
@@ -39,7 +39,7 @@ import pandas
|
|
|
39
39
|
import semver
|
|
40
40
|
import yaml
|
|
41
41
|
from dateutil import parser
|
|
42
|
-
from
|
|
42
|
+
from mlrun_pipelines.models import PipelineRun
|
|
43
43
|
from pandas._libs.tslibs.timestamps import Timedelta, Timestamp
|
|
44
44
|
from yaml.representer import RepresenterError
|
|
45
45
|
|
|
@@ -76,19 +76,6 @@ class OverwriteBuildParamsWarning(FutureWarning):
|
|
|
76
76
|
pass
|
|
77
77
|
|
|
78
78
|
|
|
79
|
-
# TODO: remove in 1.7.0
|
|
80
|
-
@deprecated(
|
|
81
|
-
version="1.5.0",
|
|
82
|
-
reason="'parse_versioned_object_uri' will be removed from this file in 1.7.0, use "
|
|
83
|
-
"'mlrun.common.helpers.parse_versioned_object_uri' instead",
|
|
84
|
-
category=FutureWarning,
|
|
85
|
-
)
|
|
86
|
-
def parse_versioned_object_uri(uri: str, default_project: str = ""):
|
|
87
|
-
return mlrun.common.helpers.parse_versioned_object_uri(
|
|
88
|
-
uri=uri, default_project=default_project
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
|
|
92
79
|
class StorePrefix:
|
|
93
80
|
"""map mlrun store objects to prefixes"""
|
|
94
81
|
|
|
@@ -119,14 +106,9 @@ class StorePrefix:
|
|
|
119
106
|
|
|
120
107
|
|
|
121
108
|
def get_artifact_target(item: dict, project=None):
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
tree = item.get("tree")
|
|
126
|
-
else:
|
|
127
|
-
db_key = item["spec"].get("db_key")
|
|
128
|
-
project_str = project or item["metadata"].get("project")
|
|
129
|
-
tree = item["metadata"].get("tree")
|
|
109
|
+
db_key = item["spec"].get("db_key")
|
|
110
|
+
project_str = project or item["metadata"].get("project")
|
|
111
|
+
tree = item["metadata"].get("tree")
|
|
130
112
|
|
|
131
113
|
kind = item.get("kind")
|
|
132
114
|
if kind in ["dataset", "model", "artifact"] and db_key:
|
|
@@ -135,11 +117,15 @@ def get_artifact_target(item: dict, project=None):
|
|
|
135
117
|
target = f"{target}@{tree}"
|
|
136
118
|
return target
|
|
137
119
|
|
|
138
|
-
return (
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
120
|
+
return item["spec"].get("target_path")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# TODO: left for migrations testing purposes. Remove in 1.8.0.
|
|
124
|
+
def is_legacy_artifact(artifact):
|
|
125
|
+
if isinstance(artifact, dict):
|
|
126
|
+
return "metadata" not in artifact
|
|
127
|
+
else:
|
|
128
|
+
return not hasattr(artifact, "metadata")
|
|
143
129
|
|
|
144
130
|
|
|
145
131
|
logger = create_logger(config.log_level, config.log_formatter, "mlrun", sys.stdout)
|
|
@@ -195,8 +181,12 @@ def verify_field_regex(
|
|
|
195
181
|
)
|
|
196
182
|
if mode == mlrun.common.schemas.RegexMatchModes.all:
|
|
197
183
|
if raise_on_failure:
|
|
184
|
+
if len(field_name) > max_chars:
|
|
185
|
+
field_name = field_name[:max_chars] + "...truncated"
|
|
186
|
+
if len(field_value) > max_chars:
|
|
187
|
+
field_value = field_value[:max_chars] + "...truncated"
|
|
198
188
|
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
199
|
-
f"Field '{field_name
|
|
189
|
+
f"Field '{field_name}' is malformed. '{field_value}' "
|
|
200
190
|
f"does not match required pattern: {pattern}"
|
|
201
191
|
)
|
|
202
192
|
return False
|
|
@@ -437,7 +427,7 @@ class LogBatchWriter:
|
|
|
437
427
|
|
|
438
428
|
def get_in(obj, keys, default=None):
|
|
439
429
|
"""
|
|
440
|
-
>>> get_in({
|
|
430
|
+
>>> get_in({"a": {"b": 1}}, "a.b")
|
|
441
431
|
1
|
|
442
432
|
"""
|
|
443
433
|
if isinstance(keys, str):
|
|
@@ -801,34 +791,6 @@ def gen_html_table(header, rows=None):
|
|
|
801
791
|
return style + '<table class="tg">\n' + out + "</table>\n\n"
|
|
802
792
|
|
|
803
793
|
|
|
804
|
-
def new_pipe_metadata(
|
|
805
|
-
artifact_path: str = None,
|
|
806
|
-
cleanup_ttl: int = None,
|
|
807
|
-
op_transformers: list[typing.Callable] = None,
|
|
808
|
-
):
|
|
809
|
-
from kfp.dsl import PipelineConf
|
|
810
|
-
|
|
811
|
-
def _set_artifact_path(task):
|
|
812
|
-
from kubernetes import client as k8s_client
|
|
813
|
-
|
|
814
|
-
task.add_env_variable(
|
|
815
|
-
k8s_client.V1EnvVar(name="MLRUN_ARTIFACT_PATH", value=artifact_path)
|
|
816
|
-
)
|
|
817
|
-
return task
|
|
818
|
-
|
|
819
|
-
conf = PipelineConf()
|
|
820
|
-
cleanup_ttl = cleanup_ttl or int(config.kfp_ttl)
|
|
821
|
-
|
|
822
|
-
if cleanup_ttl:
|
|
823
|
-
conf.set_ttl_seconds_after_finished(cleanup_ttl)
|
|
824
|
-
if artifact_path:
|
|
825
|
-
conf.add_op_transformer(_set_artifact_path)
|
|
826
|
-
if op_transformers:
|
|
827
|
-
for op_transformer in op_transformers:
|
|
828
|
-
conf.add_op_transformer(op_transformer)
|
|
829
|
-
return conf
|
|
830
|
-
|
|
831
|
-
|
|
832
794
|
def _convert_python_package_version_to_image_tag(version: typing.Optional[str]):
|
|
833
795
|
return (
|
|
834
796
|
version.replace("+", "-").replace("0.0.0-", "") if version is not None else None
|
|
@@ -1015,17 +977,27 @@ def get_ui_url(project, uid=None):
|
|
|
1015
977
|
return url
|
|
1016
978
|
|
|
1017
979
|
|
|
980
|
+
def get_model_endpoint_url(project, model_name, model_endpoint_id):
|
|
981
|
+
url = ""
|
|
982
|
+
if mlrun.mlconf.resolve_ui_url():
|
|
983
|
+
url = f"{mlrun.mlconf.resolve_ui_url()}/{mlrun.mlconf.ui.projects_prefix}/{project}/models"
|
|
984
|
+
if model_name:
|
|
985
|
+
url += f"/model-endpoints/{model_name}/{model_endpoint_id}/overview"
|
|
986
|
+
return url
|
|
987
|
+
|
|
988
|
+
|
|
1018
989
|
def get_workflow_url(project, id=None):
|
|
1019
990
|
url = ""
|
|
1020
991
|
if mlrun.mlconf.resolve_ui_url():
|
|
1021
|
-
url =
|
|
1022
|
-
mlrun.mlconf.resolve_ui_url()
|
|
992
|
+
url = (
|
|
993
|
+
f"{mlrun.mlconf.resolve_ui_url()}/{mlrun.mlconf.ui.projects_prefix}"
|
|
994
|
+
f"/{project}/jobs/monitor-workflows/workflow/{id}"
|
|
1023
995
|
)
|
|
1024
996
|
return url
|
|
1025
997
|
|
|
1026
998
|
|
|
1027
999
|
def are_strings_in_exception_chain_messages(
|
|
1028
|
-
exception: Exception, strings_list
|
|
1000
|
+
exception: Exception, strings_list: list[str]
|
|
1029
1001
|
) -> bool:
|
|
1030
1002
|
while exception is not None:
|
|
1031
1003
|
if any([string in str(exception) for string in strings_list]):
|
|
@@ -1138,7 +1110,7 @@ def get_function(function, namespace):
|
|
|
1138
1110
|
|
|
1139
1111
|
|
|
1140
1112
|
def get_handler_extended(
|
|
1141
|
-
handler_path: str, context=None, class_args: dict =
|
|
1113
|
+
handler_path: str, context=None, class_args: dict = None, namespaces=None
|
|
1142
1114
|
):
|
|
1143
1115
|
"""get function handler from [class_name::]handler string
|
|
1144
1116
|
|
|
@@ -1148,6 +1120,7 @@ def get_handler_extended(
|
|
|
1148
1120
|
:param namespaces: one or list of namespaces/modules to search the handler in
|
|
1149
1121
|
:return: function handler (callable)
|
|
1150
1122
|
"""
|
|
1123
|
+
class_args = class_args or {}
|
|
1151
1124
|
if "::" not in handler_path:
|
|
1152
1125
|
return get_function(handler_path, namespaces)
|
|
1153
1126
|
|
|
@@ -1224,7 +1197,7 @@ def calculate_dataframe_hash(dataframe: pandas.DataFrame):
|
|
|
1224
1197
|
return hashlib.sha1(pandas.util.hash_pandas_object(dataframe).values).hexdigest()
|
|
1225
1198
|
|
|
1226
1199
|
|
|
1227
|
-
def template_artifact_path(artifact_path, project, run_uid=
|
|
1200
|
+
def template_artifact_path(artifact_path, project, run_uid=None):
|
|
1228
1201
|
"""
|
|
1229
1202
|
Replace {{run.uid}} with the run uid and {{project}} with the project name in the artifact path.
|
|
1230
1203
|
If no run uid is provided, the word `project` will be used instead as it is assumed to be a project
|
|
@@ -1232,6 +1205,7 @@ def template_artifact_path(artifact_path, project, run_uid="project"):
|
|
|
1232
1205
|
"""
|
|
1233
1206
|
if not artifact_path:
|
|
1234
1207
|
return artifact_path
|
|
1208
|
+
run_uid = run_uid or "project"
|
|
1235
1209
|
artifact_path = artifact_path.replace("{{run.uid}}", run_uid)
|
|
1236
1210
|
artifact_path = _fill_project_path_template(artifact_path, project)
|
|
1237
1211
|
return artifact_path
|
|
@@ -1291,13 +1265,6 @@ def str_to_timestamp(time_str: str, now_time: Timestamp = None):
|
|
|
1291
1265
|
return Timestamp(time_str)
|
|
1292
1266
|
|
|
1293
1267
|
|
|
1294
|
-
def is_legacy_artifact(artifact):
|
|
1295
|
-
if isinstance(artifact, dict):
|
|
1296
|
-
return "metadata" not in artifact
|
|
1297
|
-
else:
|
|
1298
|
-
return not hasattr(artifact, "metadata")
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
1268
|
def is_link_artifact(artifact):
|
|
1302
1269
|
if isinstance(artifact, dict):
|
|
1303
1270
|
return (
|
|
@@ -1307,7 +1274,7 @@ def is_link_artifact(artifact):
|
|
|
1307
1274
|
return artifact.kind == mlrun.common.schemas.ArtifactCategories.link.value
|
|
1308
1275
|
|
|
1309
1276
|
|
|
1310
|
-
def format_run(run:
|
|
1277
|
+
def format_run(run: PipelineRun, with_project=False) -> dict:
|
|
1311
1278
|
fields = [
|
|
1312
1279
|
"id",
|
|
1313
1280
|
"name",
|
|
@@ -1344,17 +1311,17 @@ def format_run(run: dict, with_project=False) -> dict:
|
|
|
1344
1311
|
# pipelines are yet to populate the status or workflow has failed
|
|
1345
1312
|
# as observed https://jira.iguazeng.com/browse/ML-5195
|
|
1346
1313
|
# set to unknown to ensure a status is returned
|
|
1347
|
-
if run
|
|
1348
|
-
run["status"] = inflection.titleize(
|
|
1314
|
+
if run.get("status", None) is None:
|
|
1315
|
+
run["status"] = inflection.titleize(
|
|
1316
|
+
mlrun.common.runtimes.constants.RunStates.unknown
|
|
1317
|
+
)
|
|
1349
1318
|
|
|
1350
1319
|
return run
|
|
1351
1320
|
|
|
1352
1321
|
|
|
1353
1322
|
def get_in_artifact(artifact: dict, key, default=None, raise_on_missing=False):
|
|
1354
1323
|
"""artifact can be dict or Artifact object"""
|
|
1355
|
-
if
|
|
1356
|
-
return artifact.get(key, default)
|
|
1357
|
-
elif key == "kind":
|
|
1324
|
+
if key == "kind":
|
|
1358
1325
|
return artifact.get(key, default)
|
|
1359
1326
|
else:
|
|
1360
1327
|
for block in ["metadata", "spec", "status"]:
|
|
@@ -1405,6 +1372,18 @@ def as_number(field_name, field_value):
|
|
|
1405
1372
|
|
|
1406
1373
|
|
|
1407
1374
|
def filter_warnings(action, category):
|
|
1375
|
+
"""
|
|
1376
|
+
Decorator to filter warnings
|
|
1377
|
+
|
|
1378
|
+
Example::
|
|
1379
|
+
@filter_warnings("ignore", FutureWarning)
|
|
1380
|
+
def my_function():
|
|
1381
|
+
pass
|
|
1382
|
+
|
|
1383
|
+
:param action: one of "error", "ignore", "always", "default", "module", or "once"
|
|
1384
|
+
:param category: a class that the warning must be a subclass of
|
|
1385
|
+
"""
|
|
1386
|
+
|
|
1408
1387
|
def decorator(function):
|
|
1409
1388
|
def wrapper(*args, **kwargs):
|
|
1410
1389
|
# context manager that copies and, upon exit, restores the warnings filter and the showwarning() function.
|
|
@@ -1562,3 +1541,90 @@ def is_safe_path(base, filepath, is_symlink=False):
|
|
|
1562
1541
|
os.path.abspath(filepath) if not is_symlink else os.path.realpath(filepath)
|
|
1563
1542
|
)
|
|
1564
1543
|
return base == os.path.commonpath((base, resolved_filepath))
|
|
1544
|
+
|
|
1545
|
+
|
|
1546
|
+
def get_serving_spec():
|
|
1547
|
+
data = None
|
|
1548
|
+
|
|
1549
|
+
# we will have the serving spec in either mounted config map
|
|
1550
|
+
# or env depending on the size of the spec and configuration
|
|
1551
|
+
|
|
1552
|
+
try:
|
|
1553
|
+
with open(mlrun.common.constants.MLRUN_SERVING_SPEC_PATH) as f:
|
|
1554
|
+
data = f.read()
|
|
1555
|
+
except FileNotFoundError:
|
|
1556
|
+
pass
|
|
1557
|
+
|
|
1558
|
+
if data is None:
|
|
1559
|
+
data = os.environ.get("SERVING_SPEC_ENV", "")
|
|
1560
|
+
if not data:
|
|
1561
|
+
raise mlrun.errors.MLRunInvalidArgumentError(
|
|
1562
|
+
"Failed to find serving spec in env var or config file"
|
|
1563
|
+
)
|
|
1564
|
+
spec = json.loads(data)
|
|
1565
|
+
return spec
|
|
1566
|
+
|
|
1567
|
+
|
|
1568
|
+
def additional_filters_warning(additional_filters, class_name):
|
|
1569
|
+
if additional_filters and any(additional_filters):
|
|
1570
|
+
mlrun.utils.logger.warn(
|
|
1571
|
+
f"additional_filters parameter is not supported in {class_name},"
|
|
1572
|
+
f" parameter has been ignored."
|
|
1573
|
+
)
|
|
1574
|
+
|
|
1575
|
+
|
|
1576
|
+
def validate_component_version_compatibility(
|
|
1577
|
+
component_name: typing.Literal["iguazio", "nuclio"], *min_versions: str
|
|
1578
|
+
):
|
|
1579
|
+
"""
|
|
1580
|
+
:param component_name: Name of the component to validate compatibility for.
|
|
1581
|
+
:param min_versions: Valid minimum version(s) required, assuming no 2 versions has equal major and minor.
|
|
1582
|
+
"""
|
|
1583
|
+
parsed_min_versions = [
|
|
1584
|
+
semver.VersionInfo.parse(min_version) for min_version in min_versions
|
|
1585
|
+
]
|
|
1586
|
+
parsed_current_version = None
|
|
1587
|
+
component_current_version = None
|
|
1588
|
+
try:
|
|
1589
|
+
if component_name == "iguazio":
|
|
1590
|
+
component_current_version = mlrun.mlconf.igz_version
|
|
1591
|
+
parsed_current_version = mlrun.mlconf.get_parsed_igz_version()
|
|
1592
|
+
|
|
1593
|
+
if parsed_current_version:
|
|
1594
|
+
# ignore pre-release and build metadata, as iguazio version always has them, and we only care about the
|
|
1595
|
+
# major, minor, and patch versions
|
|
1596
|
+
parsed_current_version = semver.VersionInfo.parse(
|
|
1597
|
+
f"{parsed_current_version.major}.{parsed_current_version.minor}.{parsed_current_version.patch}"
|
|
1598
|
+
)
|
|
1599
|
+
if component_name == "nuclio":
|
|
1600
|
+
component_current_version = mlrun.mlconf.nuclio_version
|
|
1601
|
+
parsed_current_version = semver.VersionInfo.parse(
|
|
1602
|
+
mlrun.mlconf.nuclio_version
|
|
1603
|
+
)
|
|
1604
|
+
if not parsed_current_version:
|
|
1605
|
+
return True
|
|
1606
|
+
except ValueError:
|
|
1607
|
+
# only log when version is set but invalid
|
|
1608
|
+
if component_current_version:
|
|
1609
|
+
logger.warning(
|
|
1610
|
+
"Unable to parse current version, assuming compatibility",
|
|
1611
|
+
component_name=component_name,
|
|
1612
|
+
current_version=component_current_version,
|
|
1613
|
+
min_versions=min_versions,
|
|
1614
|
+
)
|
|
1615
|
+
return True
|
|
1616
|
+
|
|
1617
|
+
parsed_min_versions.sort(reverse=True)
|
|
1618
|
+
for parsed_min_version in parsed_min_versions:
|
|
1619
|
+
if parsed_current_version < parsed_min_version:
|
|
1620
|
+
return False
|
|
1621
|
+
return True
|
|
1622
|
+
|
|
1623
|
+
|
|
1624
|
+
def format_alert_summary(
|
|
1625
|
+
alert: mlrun.common.schemas.AlertConfig, event_data: mlrun.common.schemas.Event
|
|
1626
|
+
) -> str:
|
|
1627
|
+
result = alert.summary.replace("{{project}}", alert.project)
|
|
1628
|
+
result = result.replace("{{name}}", alert.name)
|
|
1629
|
+
result = result.replace("{{entity}}", event_data.entity.ids[0])
|
|
1630
|
+
return result
|
mlrun/utils/http.py
CHANGED