mlrun 1.10.0rc14__py3-none-any.whl → 1.10.0rc16__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 (48) hide show
  1. mlrun/artifacts/base.py +0 -31
  2. mlrun/artifacts/llm_prompt.py +6 -0
  3. mlrun/artifacts/manager.py +0 -5
  4. mlrun/common/constants.py +1 -0
  5. mlrun/common/schemas/__init__.py +1 -0
  6. mlrun/common/schemas/model_monitoring/__init__.py +1 -0
  7. mlrun/common/schemas/model_monitoring/functions.py +1 -1
  8. mlrun/common/schemas/model_monitoring/model_endpoints.py +10 -0
  9. mlrun/common/schemas/workflow.py +2 -0
  10. mlrun/config.py +1 -1
  11. mlrun/datastore/model_provider/model_provider.py +42 -14
  12. mlrun/datastore/model_provider/openai_provider.py +96 -15
  13. mlrun/db/base.py +20 -0
  14. mlrun/db/httpdb.py +64 -9
  15. mlrun/db/nopdb.py +13 -0
  16. mlrun/launcher/local.py +13 -0
  17. mlrun/model_monitoring/__init__.py +1 -0
  18. mlrun/model_monitoring/applications/base.py +176 -20
  19. mlrun/model_monitoring/db/_schedules.py +84 -24
  20. mlrun/model_monitoring/db/tsdb/base.py +72 -1
  21. mlrun/model_monitoring/db/tsdb/tdengine/schemas.py +7 -1
  22. mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py +37 -0
  23. mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +25 -0
  24. mlrun/model_monitoring/helpers.py +26 -4
  25. mlrun/projects/pipelines.py +44 -24
  26. mlrun/projects/project.py +26 -7
  27. mlrun/runtimes/daskjob.py +6 -0
  28. mlrun/runtimes/mpijob/abstract.py +6 -0
  29. mlrun/runtimes/mpijob/v1.py +6 -0
  30. mlrun/runtimes/nuclio/application/application.py +2 -0
  31. mlrun/runtimes/nuclio/function.py +6 -0
  32. mlrun/runtimes/nuclio/serving.py +12 -11
  33. mlrun/runtimes/pod.py +21 -0
  34. mlrun/runtimes/remotesparkjob.py +6 -0
  35. mlrun/runtimes/sparkjob/spark3job.py +6 -0
  36. mlrun/runtimes/utils.py +0 -2
  37. mlrun/serving/server.py +122 -53
  38. mlrun/serving/states.py +128 -44
  39. mlrun/serving/system_steps.py +84 -58
  40. mlrun/utils/helpers.py +82 -12
  41. mlrun/utils/retryer.py +15 -2
  42. mlrun/utils/version/version.json +2 -2
  43. {mlrun-1.10.0rc14.dist-info → mlrun-1.10.0rc16.dist-info}/METADATA +2 -7
  44. {mlrun-1.10.0rc14.dist-info → mlrun-1.10.0rc16.dist-info}/RECORD +48 -48
  45. {mlrun-1.10.0rc14.dist-info → mlrun-1.10.0rc16.dist-info}/WHEEL +0 -0
  46. {mlrun-1.10.0rc14.dist-info → mlrun-1.10.0rc16.dist-info}/entry_points.txt +0 -0
  47. {mlrun-1.10.0rc14.dist-info → mlrun-1.10.0rc16.dist-info}/licenses/LICENSE +0 -0
  48. {mlrun-1.10.0rc14.dist-info → mlrun-1.10.0rc16.dist-info}/top_level.txt +0 -0
@@ -13,10 +13,10 @@
13
13
  # limitations under the License.
14
14
 
15
15
  import random
16
- from copy import deepcopy
17
16
  from datetime import timedelta
18
17
  from typing import Any, Optional, Union
19
18
 
19
+ import numpy as np
20
20
  import storey
21
21
 
22
22
  import mlrun
@@ -24,7 +24,7 @@ import mlrun.artifacts
24
24
  import mlrun.common.schemas.model_monitoring as mm_schemas
25
25
  import mlrun.serving
26
26
  from mlrun.common.schemas import MonitoringData
27
- from mlrun.utils import logger
27
+ from mlrun.utils import get_data_from_path, logger
28
28
 
29
29
 
30
30
  class MonitoringPreProcessor(storey.MapClass):
@@ -45,24 +45,13 @@ class MonitoringPreProcessor(storey.MapClass):
45
45
  result_path = model_monitoring_data.get(MonitoringData.RESULT_PATH)
46
46
  input_path = model_monitoring_data.get(MonitoringData.INPUT_PATH)
47
47
 
48
- result = self._get_data_from_path(
49
- result_path, event.body.get(model, event.body)
50
- )
48
+ result = get_data_from_path(result_path, event.body.get(model, event.body))
51
49
  output_schema = model_monitoring_data.get(MonitoringData.OUTPUTS)
52
50
  input_schema = model_monitoring_data.get(MonitoringData.INPUTS)
53
51
  logger.debug("output schema retrieved", output_schema=output_schema)
54
52
  if isinstance(result, dict):
55
- if len(result) > 1:
56
- # transpose by key the outputs:
57
- outputs = self.transpose_by_key(result, output_schema)
58
- elif len(result) == 1:
59
- outputs = (
60
- result[output_schema[0]]
61
- if output_schema
62
- else list(result.values())[0]
63
- )
64
- else:
65
- outputs = []
53
+ # transpose by key the outputs:
54
+ outputs = self.transpose_by_key(result, output_schema)
66
55
  if not output_schema:
67
56
  logger.warn(
68
57
  "Output schema was not provided using Project:log_model or by ModelRunnerStep:add_model order "
@@ -72,16 +61,14 @@ class MonitoringPreProcessor(storey.MapClass):
72
61
  outputs = result
73
62
 
74
63
  event_inputs = event._metadata.get("inputs", {})
75
- event_inputs = self._get_data_from_path(input_path, event_inputs)
64
+ event_inputs = get_data_from_path(input_path, event_inputs)
76
65
  if isinstance(event_inputs, dict):
77
- if len(event_inputs) > 1:
78
- # transpose by key the inputs:
79
- inputs = self.transpose_by_key(event_inputs, input_schema)
80
- else:
81
- inputs = (
82
- event_inputs[input_schema[0]]
83
- if input_schema
84
- else list(result.values())[0]
66
+ # transpose by key the inputs:
67
+ inputs = self.transpose_by_key(event_inputs, input_schema)
68
+ if not input_schema:
69
+ logger.warn(
70
+ "Input schema was not provided using by ModelRunnerStep:add_model, order "
71
+ "may not preserved"
85
72
  )
86
73
  else:
87
74
  inputs = event_inputs
@@ -104,6 +91,11 @@ class MonitoringPreProcessor(storey.MapClass):
104
91
  output_len=len(outputs),
105
92
  schema_len=len(output_schema),
106
93
  )
94
+ if len(inputs) != len(outputs):
95
+ logger.warn(
96
+ "outputs and inputs are not in the same length check 'input_path' and "
97
+ "'output_path' was specified if needed"
98
+ )
107
99
  request = {"inputs": inputs, "id": getattr(event, "id", None)}
108
100
  resp = {"outputs": outputs}
109
101
 
@@ -111,41 +103,73 @@ class MonitoringPreProcessor(storey.MapClass):
111
103
 
112
104
  @staticmethod
113
105
  def transpose_by_key(
114
- data_to_transpose, schema: Optional[list[str]] = None
115
- ) -> list[list[float]]:
116
- values = (
117
- list(data_to_transpose.values())
118
- if not schema
119
- else [data_to_transpose[key] for key in schema]
120
- )
121
- if values and not isinstance(values[0], list):
122
- values = [values]
123
- transposed = (
124
- list(map(list, zip(*values)))
125
- if all(isinstance(v, list) for v in values) and len(values) > 1
126
- else values
127
- )
128
- return transposed
106
+ data: dict, schema: Optional[Union[str, list[str]]] = None
107
+ ) -> Union[list[float], list[list[float]]]:
108
+ """
109
+ Transpose values from a dictionary by keys.
129
110
 
130
- @staticmethod
131
- def _get_data_from_path(
132
- path: Union[str, list[str], None], data: dict
133
- ) -> dict[str, Any]:
134
- if isinstance(path, str):
135
- output_data = data.get(path)
136
- elif isinstance(path, list):
137
- output_data = deepcopy(data)
138
- for key in path:
139
- output_data = output_data.get(key, {})
140
- elif path is None:
141
- output_data = data
111
+ Given a dictionary and an optional schema (a key or list of keys), this function:
112
+ - Extracts the values for the specified keys (or all keys if no schema is provided).
113
+ - Ensures the data is represented as a list of rows, then transposes it (i.e., switches rows to columns).
114
+ - Handles edge cases:
115
+ * If a single scalar or single-element list is provided, returns a flat list.
116
+ * If a single key is provided (as a string or a list with one element), handles it properly.
117
+ * If only one row with len of one remains after transposition, unwraps it to avoid nested list-of-one.
118
+
119
+ Example::
120
+
121
+ transpose_by_key({"a": 1})
122
+ # returns: [1]
123
+
124
+ transpose_by_key({"a": [1, 2]})
125
+ # returns: [1 ,2]
126
+
127
+ transpose_by_key({"a": [1, 2], "b": [3, 4]})
128
+ # returns: [[1, 3], [2, 4]]
129
+
130
+ :param data: Dictionary with values that are either scalars or lists.
131
+ :param schema: Optional key or list of keys to extract. If not provided, all keys are used.
132
+ Can be a string (single key) or a list of strings.
133
+
134
+ :return: Transposed values:
135
+ * If result is a single column or row, returns a flat list.
136
+ * If result is a matrix, returns a list of lists.
137
+
138
+ :raises ValueError: If the values include a mix of scalars and lists, or if the list lengths do not match.
139
+ """
140
+
141
+ # Normalize schema to list
142
+ if not schema:
143
+ keys = list(data.keys())
144
+ elif isinstance(schema, str):
145
+ keys = [schema]
142
146
  else:
143
- raise mlrun.errors.MLRunInvalidArgumentError(
144
- "Expected path be of type str or list of str or None"
147
+ keys = schema
148
+
149
+ values = [data[key] for key in keys]
150
+
151
+ # Detect if all are scalars ie: int,float,str
152
+ all_scalars = all(not isinstance(v, (list, tuple, np.ndarray)) for v in values)
153
+ all_lists = all(isinstance(v, (list, tuple, np.ndarray)) for v in values)
154
+
155
+ if not (all_scalars or all_lists):
156
+ raise ValueError(
157
+ "All values must be either scalars or lists of equal length."
145
158
  )
146
- if isinstance(output_data, (int, float)):
147
- output_data = [output_data]
148
- return output_data
159
+
160
+ if all_scalars:
161
+ transposed = np.array([values])
162
+ elif all_lists and len(keys) > 1:
163
+ arrays = [np.array(v) for v in values]
164
+ mat = np.stack(arrays, axis=0)
165
+ transposed = mat.T
166
+ else:
167
+ return values[0]
168
+
169
+ if transposed.shape[1] == 1 and transposed.shape[0] == 1:
170
+ # Transform [[0]] -> [0]:
171
+ return transposed[:, 0].tolist()
172
+ return transposed.tolist()
149
173
 
150
174
  def do(self, event):
151
175
  monitoring_event_list = []
@@ -337,7 +361,9 @@ class SamplingStep(storey.MapClass):
337
361
  event=event,
338
362
  sampling_percentage=self.sampling_percentage,
339
363
  )
340
- if self.sampling_percentage != 100:
364
+ if self.sampling_percentage != 100 and not event.get(
365
+ mm_schemas.StreamProcessingEvent.ERROR
366
+ ):
341
367
  request = event[mm_schemas.StreamProcessingEvent.REQUEST]
342
368
  num_of_inputs = len(request["inputs"])
343
369
  sampled_requests_indices = self._pick_random_requests(
mlrun/utils/helpers.py CHANGED
@@ -29,6 +29,7 @@ import traceback
29
29
  import typing
30
30
  import uuid
31
31
  import warnings
32
+ from copy import deepcopy
32
33
  from datetime import datetime, timedelta, timezone
33
34
  from importlib import import_module, reload
34
35
  from os import path
@@ -162,14 +163,6 @@ def get_artifact_target(item: dict, project=None):
162
163
  return item["spec"].get("target_path")
163
164
 
164
165
 
165
- # TODO: Remove once data migration v5 is obsolete
166
- def is_legacy_artifact(artifact):
167
- if isinstance(artifact, dict):
168
- return "metadata" not in artifact
169
- else:
170
- return not hasattr(artifact, "metadata")
171
-
172
-
173
166
  logger = create_logger(config.log_level, config.log_formatter, "mlrun", sys.stdout)
174
167
  missing = object()
175
168
 
@@ -794,6 +787,22 @@ def generate_artifact_uri(
794
787
  return artifact_uri
795
788
 
796
789
 
790
+ def remove_tag_from_artifact_uri(uri: str) -> Optional[str]:
791
+ """
792
+ Remove the `:<tag>` part from a URI with pattern:
793
+ [store://][<project>/]<key>[#<iter>][:<tag>][@<tree>][^<uid>]
794
+
795
+ Returns the URI without the tag section.
796
+
797
+ Examples:
798
+ "store://proj/key:latest" => "store://proj/key"
799
+ "key#1:dev@tree^uid" => "key#1@tree^uid"
800
+ "store://key:tag" => "store://key"
801
+ "store://models/remote-model-project/my_model#0@tree" => unchanged (no tag)
802
+ """
803
+ return re.sub(r"(?<=/[^/:]\+):[^@^:\s]+(?=(@|\^|$))", "", uri)
804
+
805
+
797
806
  def extend_hub_uri_if_needed(uri) -> tuple[str, bool]:
798
807
  """
799
808
  Retrieve the full uri of the item's yaml in the hub.
@@ -1050,7 +1059,14 @@ def fill_function_hash(function_dict, tag=""):
1050
1059
 
1051
1060
 
1052
1061
  def retry_until_successful(
1053
- backoff: int, timeout: int, logger, verbose: bool, _function, *args, **kwargs
1062
+ backoff: int,
1063
+ timeout: int,
1064
+ logger,
1065
+ verbose: bool,
1066
+ _function,
1067
+ *args,
1068
+ fatal_exceptions=(),
1069
+ **kwargs,
1054
1070
  ):
1055
1071
  """
1056
1072
  Runs function with given *args and **kwargs.
@@ -1063,14 +1079,31 @@ def retry_until_successful(
1063
1079
  :param verbose: whether to log the failure on each retry
1064
1080
  :param _function: function to run
1065
1081
  :param args: functions args
1082
+ :param fatal_exceptions: exception types that should not be retried
1066
1083
  :param kwargs: functions kwargs
1067
1084
  :return: function result
1068
1085
  """
1069
- return Retryer(backoff, timeout, logger, verbose, _function, *args, **kwargs).run()
1086
+ return Retryer(
1087
+ backoff,
1088
+ timeout,
1089
+ logger,
1090
+ verbose,
1091
+ _function,
1092
+ *args,
1093
+ fatal_exceptions=fatal_exceptions,
1094
+ **kwargs,
1095
+ ).run()
1070
1096
 
1071
1097
 
1072
1098
  async def retry_until_successful_async(
1073
- backoff: int, timeout: int, logger, verbose: bool, _function, *args, **kwargs
1099
+ backoff: int,
1100
+ timeout: int,
1101
+ logger,
1102
+ verbose: bool,
1103
+ _function,
1104
+ *args,
1105
+ fatal_exceptions=(),
1106
+ **kwargs,
1074
1107
  ):
1075
1108
  """
1076
1109
  Runs function with given *args and **kwargs.
@@ -1082,12 +1115,20 @@ async def retry_until_successful_async(
1082
1115
  :param logger: a logger so we can log the failures
1083
1116
  :param verbose: whether to log the failure on each retry
1084
1117
  :param _function: function to run
1118
+ :param fatal_exceptions: exception types that should not be retried
1085
1119
  :param args: functions args
1086
1120
  :param kwargs: functions kwargs
1087
1121
  :return: function result
1088
1122
  """
1089
1123
  return await AsyncRetryer(
1090
- backoff, timeout, logger, verbose, _function, *args, **kwargs
1124
+ backoff,
1125
+ timeout,
1126
+ logger,
1127
+ verbose,
1128
+ _function,
1129
+ *args,
1130
+ fatal_exceptions=fatal_exceptions,
1131
+ **kwargs,
1091
1132
  ).run()
1092
1133
 
1093
1134
 
@@ -2352,3 +2393,32 @@ def encode_user_code(
2352
2393
  "Consider using `with_source_archive` to add user code as a remote source to the function."
2353
2394
  )
2354
2395
  return encoded
2396
+
2397
+
2398
+ def split_path(path: str) -> typing.Union[str, list[str], None]:
2399
+ if path is not None:
2400
+ parsed_path = path.split(".")
2401
+ if len(parsed_path) == 1:
2402
+ parsed_path = parsed_path[0]
2403
+ return parsed_path
2404
+ return path
2405
+
2406
+
2407
+ def get_data_from_path(
2408
+ path: typing.Union[str, list[str], None], data: dict
2409
+ ) -> dict[str, Any]:
2410
+ if isinstance(path, str):
2411
+ output_data = data.get(path)
2412
+ elif isinstance(path, list):
2413
+ output_data = deepcopy(data)
2414
+ for key in path:
2415
+ output_data = output_data.get(key, {})
2416
+ elif path is None:
2417
+ output_data = data
2418
+ else:
2419
+ raise mlrun.errors.MLRunInvalidArgumentError(
2420
+ "Expected path be of type str or list of str or None"
2421
+ )
2422
+ if isinstance(output_data, (int, float)):
2423
+ output_data = [output_data]
2424
+ return output_data
mlrun/utils/retryer.py CHANGED
@@ -77,7 +77,17 @@ def create_exponential_backoff(base=2, max_value=120, scale_factor=1):
77
77
 
78
78
 
79
79
  class Retryer:
80
- def __init__(self, backoff, timeout, logger, verbose, function, *args, **kwargs):
80
+ def __init__(
81
+ self,
82
+ backoff,
83
+ timeout,
84
+ logger,
85
+ verbose,
86
+ function,
87
+ *args,
88
+ fatal_exceptions=(),
89
+ **kwargs,
90
+ ):
81
91
  """
82
92
  Initialize function retryer with given *args and **kwargs.
83
93
  Tries to run it until success or timeout reached (timeout is optional)
@@ -89,6 +99,7 @@ class Retryer:
89
99
  :param verbose: whether to log the failure on each retry
90
100
  :param _function: function to run
91
101
  :param args: functions args
102
+ :param fatal_exceptions: exception types that should not be retried
92
103
  :param kwargs: functions kwargs
93
104
  """
94
105
  self.backoff = backoff
@@ -96,6 +107,7 @@ class Retryer:
96
107
  self.logger = logger
97
108
  self.verbose = verbose
98
109
  self.function = function
110
+ self.fatal_exceptions = tuple(fatal_exceptions or ())
99
111
  self.args = args
100
112
  self.kwargs = kwargs
101
113
  self.start_time = None
@@ -107,7 +119,8 @@ class Retryer:
107
119
  while not self._timeout_exceeded():
108
120
  next_interval = self.first_interval or next(self.backoff)
109
121
  result, exc, retry = self._perform_call(next_interval)
110
- if retry:
122
+
123
+ if retry and type(exc) not in self.fatal_exceptions:
111
124
  time.sleep(next_interval)
112
125
  elif not exc:
113
126
  return result
@@ -1,4 +1,4 @@
1
1
  {
2
- "git_commit": "5f421886e871ccc04e021cd67fc4597e39ab890c",
3
- "version": "1.10.0-rc14"
2
+ "git_commit": "78045e1e85e7c81eee93682240c4ebe7b22fa67c",
3
+ "version": "1.10.0-rc16"
4
4
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mlrun
3
- Version: 1.10.0rc14
3
+ Version: 1.10.0rc16
4
4
  Summary: Tracking and config of machine learning runs
5
5
  Home-page: https://github.com/mlrun/mlrun
6
6
  Author: Yaron Haviv
@@ -28,7 +28,7 @@ Requires-Dist: aiohttp-retry~=2.9
28
28
  Requires-Dist: click~=8.1
29
29
  Requires-Dist: nest-asyncio~=1.0
30
30
  Requires-Dist: ipython~=8.10
31
- Requires-Dist: nuclio-jupyter~=0.11.1
31
+ Requires-Dist: nuclio-jupyter~=0.11.2
32
32
  Requires-Dist: numpy<1.27.0,>=1.26.4
33
33
  Requires-Dist: pandas<2.2,>=1.2
34
34
  Requires-Dist: pyarrow<18,>=10.0
@@ -101,8 +101,6 @@ Provides-Extra: tdengine
101
101
  Requires-Dist: taos-ws-py==0.3.2; extra == "tdengine"
102
102
  Provides-Extra: snowflake
103
103
  Requires-Dist: snowflake-connector-python~=3.7; extra == "snowflake"
104
- Provides-Extra: openai
105
- Requires-Dist: openai~=1.88; extra == "openai"
106
104
  Provides-Extra: dev-postgres
107
105
  Requires-Dist: pytest-mock-resources[postgres]~=2.12; extra == "dev-postgres"
108
106
  Provides-Extra: kfp18
@@ -148,7 +146,6 @@ Requires-Dist: graphviz~=0.20.0; extra == "all"
148
146
  Requires-Dist: kafka-python~=2.1.0; extra == "all"
149
147
  Requires-Dist: mlflow~=2.22; extra == "all"
150
148
  Requires-Dist: msrest~=0.6.21; extra == "all"
151
- Requires-Dist: openai~=1.88; extra == "all"
152
149
  Requires-Dist: oss2==2.18.1; extra == "all"
153
150
  Requires-Dist: ossfs==2023.12.0; extra == "all"
154
151
  Requires-Dist: plotly~=5.23; extra == "all"
@@ -180,7 +177,6 @@ Requires-Dist: graphviz~=0.20.0; extra == "complete"
180
177
  Requires-Dist: kafka-python~=2.1.0; extra == "complete"
181
178
  Requires-Dist: mlflow~=2.22; extra == "complete"
182
179
  Requires-Dist: msrest~=0.6.21; extra == "complete"
183
- Requires-Dist: openai~=1.88; extra == "complete"
184
180
  Requires-Dist: oss2==2.18.1; extra == "complete"
185
181
  Requires-Dist: ossfs==2023.12.0; extra == "complete"
186
182
  Requires-Dist: plotly~=5.23; extra == "complete"
@@ -223,7 +219,6 @@ Requires-Dist: mlflow~=2.22; extra == "complete-api"
223
219
  Requires-Dist: mlrun-pipelines-kfp-v1-8~=0.5.7; extra == "complete-api"
224
220
  Requires-Dist: msrest~=0.6.21; extra == "complete-api"
225
221
  Requires-Dist: objgraph~=3.6; extra == "complete-api"
226
- Requires-Dist: openai~=1.88; extra == "complete-api"
227
222
  Requires-Dist: oss2==2.18.1; extra == "complete-api"
228
223
  Requires-Dist: ossfs==2023.12.0; extra == "complete-api"
229
224
  Requires-Dist: plotly~=5.23; extra == "complete-api"