mlrun 1.10.0rc7__py3-none-any.whl → 1.10.0rc9__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 (53) hide show
  1. mlrun/__init__.py +3 -1
  2. mlrun/common/db/dialects.py +25 -0
  3. mlrun/common/schemas/background_task.py +5 -0
  4. mlrun/common/schemas/function.py +1 -0
  5. mlrun/common/schemas/model_monitoring/__init__.py +2 -0
  6. mlrun/common/schemas/model_monitoring/constants.py +16 -0
  7. mlrun/common/schemas/model_monitoring/model_endpoints.py +8 -0
  8. mlrun/common/schemas/partition.py +13 -3
  9. mlrun/common/schemas/project.py +4 -0
  10. mlrun/common/schemas/serving.py +2 -0
  11. mlrun/config.py +11 -22
  12. mlrun/datastore/utils.py +3 -2
  13. mlrun/db/__init__.py +1 -0
  14. mlrun/db/base.py +11 -10
  15. mlrun/db/httpdb.py +97 -25
  16. mlrun/db/nopdb.py +5 -4
  17. mlrun/db/sql_types.py +160 -0
  18. mlrun/frameworks/tf_keras/__init__.py +4 -4
  19. mlrun/frameworks/tf_keras/callbacks/logging_callback.py +23 -20
  20. mlrun/frameworks/tf_keras/mlrun_interface.py +4 -1
  21. mlrun/frameworks/tf_keras/model_handler.py +80 -9
  22. mlrun/frameworks/tf_keras/utils.py +12 -1
  23. mlrun/launcher/base.py +6 -1
  24. mlrun/launcher/client.py +1 -22
  25. mlrun/launcher/local.py +0 -4
  26. mlrun/model_monitoring/applications/base.py +21 -1
  27. mlrun/model_monitoring/applications/context.py +2 -1
  28. mlrun/projects/pipelines.py +35 -3
  29. mlrun/projects/project.py +13 -29
  30. mlrun/run.py +37 -5
  31. mlrun/runtimes/daskjob.py +0 -2
  32. mlrun/runtimes/kubejob.py +0 -4
  33. mlrun/runtimes/mpijob/abstract.py +0 -2
  34. mlrun/runtimes/mpijob/v1.py +0 -2
  35. mlrun/runtimes/nuclio/function.py +0 -2
  36. mlrun/runtimes/nuclio/serving.py +14 -51
  37. mlrun/runtimes/pod.py +0 -3
  38. mlrun/runtimes/remotesparkjob.py +0 -2
  39. mlrun/runtimes/sparkjob/spark3job.py +0 -2
  40. mlrun/serving/__init__.py +2 -0
  41. mlrun/serving/server.py +159 -123
  42. mlrun/serving/states.py +215 -18
  43. mlrun/serving/system_steps.py +391 -0
  44. mlrun/serving/v2_serving.py +9 -8
  45. mlrun/utils/helpers.py +19 -1
  46. mlrun/utils/version/version.json +2 -2
  47. {mlrun-1.10.0rc7.dist-info → mlrun-1.10.0rc9.dist-info}/METADATA +22 -18
  48. {mlrun-1.10.0rc7.dist-info → mlrun-1.10.0rc9.dist-info}/RECORD +52 -50
  49. mlrun/common/db/sql_session.py +0 -79
  50. {mlrun-1.10.0rc7.dist-info → mlrun-1.10.0rc9.dist-info}/WHEEL +0 -0
  51. {mlrun-1.10.0rc7.dist-info → mlrun-1.10.0rc9.dist-info}/entry_points.txt +0 -0
  52. {mlrun-1.10.0rc7.dist-info → mlrun-1.10.0rc9.dist-info}/licenses/LICENSE +0 -0
  53. {mlrun-1.10.0rc7.dist-info → mlrun-1.10.0rc9.dist-info}/top_level.txt +0 -0
mlrun/db/sql_types.py ADDED
@@ -0,0 +1,160 @@
1
+ # Copyright 2025 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
+ This module provides SQLAlchemy TypeDecorator subclasses that are aware of
16
+ database dialects (MySQL, PostgreSQL, SQLite) and automatically select
17
+ appropriate native types (e.g., UUID, BLOB, TIMESTAMP with precision) or
18
+ fallbacks (e.g., hex-string storage) to ensure consistent behavior across
19
+ different database backends.
20
+ """
21
+
22
+ import uuid
23
+ from typing import Any, Optional, Union
24
+
25
+ import sqlalchemy.types
26
+ from sqlalchemy import CHAR, Text
27
+ from sqlalchemy.dialects.mysql import DATETIME as MYSQL_DATETIME
28
+ from sqlalchemy.dialects.mysql import MEDIUMBLOB
29
+ from sqlalchemy.dialects.postgresql import BYTEA
30
+ from sqlalchemy.dialects.postgresql import TIMESTAMP as PG_TIMESTAMP
31
+ from sqlalchemy.dialects.postgresql import UUID as PG_UUID
32
+ from sqlalchemy.engine.interfaces import Dialect
33
+ from sqlalchemy.types import TypeDecorator
34
+
35
+ import mlrun.common.db.dialects
36
+
37
+
38
+ class DateTime(TypeDecorator):
39
+ impl = sqlalchemy.types.DateTime
40
+ cache_ok = True
41
+ precision: int = 3
42
+
43
+ def load_dialect_impl(
44
+ self,
45
+ dialect: Dialect,
46
+ ) -> sqlalchemy.types.TypeEngine:
47
+ if dialect.name == mlrun.common.db.dialects.Dialects.MYSQL:
48
+ return dialect.type_descriptor(
49
+ MYSQL_DATETIME(
50
+ fsp=self.precision,
51
+ timezone=True,
52
+ )
53
+ )
54
+ if dialect.name == mlrun.common.db.dialects.Dialects.POSTGRESQL:
55
+ return dialect.type_descriptor(
56
+ PG_TIMESTAMP(
57
+ precision=self.precision,
58
+ timezone=True,
59
+ )
60
+ )
61
+ return dialect.type_descriptor(sqlalchemy.types.DateTime)
62
+
63
+
64
+ class MicroSecondDateTime(DateTime):
65
+ cache_ok = True
66
+ precision: int = 6
67
+
68
+
69
+ class Blob(TypeDecorator):
70
+ impl = sqlalchemy.types.LargeBinary
71
+ cache_ok = True
72
+
73
+ def load_dialect_impl(
74
+ self,
75
+ dialect: Dialect,
76
+ ) -> sqlalchemy.types.TypeEngine:
77
+ if dialect.name == mlrun.common.db.dialects.Dialects.MYSQL:
78
+ return dialect.type_descriptor(MEDIUMBLOB)
79
+ if dialect.name == mlrun.common.db.dialects.Dialects.POSTGRESQL:
80
+ return dialect.type_descriptor(BYTEA)
81
+ return dialect.type_descriptor(self.impl)
82
+
83
+
84
+ class Utf8BinText(TypeDecorator):
85
+ impl = Text
86
+ cache_ok = True
87
+
88
+ def load_dialect_impl(
89
+ self,
90
+ dialect: Dialect,
91
+ ) -> sqlalchemy.types.TypeEngine:
92
+ if dialect.name == mlrun.common.db.dialects.Dialects.MYSQL:
93
+ return dialect.type_descriptor(
94
+ sqlalchemy.dialects.mysql.VARCHAR(
95
+ collation="utf8_bin",
96
+ length=255,
97
+ )
98
+ )
99
+ if dialect.name == mlrun.common.db.dialects.Dialects.POSTGRESQL:
100
+ # This collation is created as part of the database creation
101
+ return dialect.type_descriptor(
102
+ Text(
103
+ collation="utf8_bin",
104
+ )
105
+ )
106
+ if dialect.name == mlrun.common.db.dialects.Dialects.SQLITE:
107
+ return dialect.type_descriptor(
108
+ Text(
109
+ collation="BINARY",
110
+ )
111
+ )
112
+ return dialect.type_descriptor(self.impl)
113
+
114
+
115
+ class UuidType(TypeDecorator):
116
+ """
117
+ A UUID type which stores as native UUID on Postgres (as_uuid=True)
118
+ and as 32-char hex strings on other dialects.
119
+ """
120
+
121
+ impl = CHAR(32)
122
+ cache_ok = True
123
+
124
+ def load_dialect_impl(self, dialect: Dialect) -> sqlalchemy.types.TypeEngine:
125
+ if dialect.name == mlrun.common.db.dialects.Dialects.POSTGRESQL:
126
+ return dialect.type_descriptor(PG_UUID(as_uuid=True))
127
+ return dialect.type_descriptor(CHAR(32))
128
+
129
+ def process_bind_param(
130
+ self,
131
+ value: Optional[Union[uuid.UUID, str]],
132
+ dialect: Dialect,
133
+ ) -> Optional[Union[uuid.UUID, str]]:
134
+ if value is None:
135
+ return None
136
+ if isinstance(value, uuid.UUID):
137
+ return (
138
+ value
139
+ if dialect.name == mlrun.common.db.dialects.Dialects.POSTGRESQL
140
+ else value.hex
141
+ )
142
+ if isinstance(value, str):
143
+ u = uuid.UUID(value)
144
+ return (
145
+ u
146
+ if dialect.name == mlrun.common.db.dialects.Dialects.POSTGRESQL
147
+ else u.hex
148
+ )
149
+ raise ValueError(f"Cannot bind UUID value {value!r}")
150
+
151
+ def process_result_value(
152
+ self, value: Optional[Union[uuid.UUID, bytes, str]], dialect: Dialect
153
+ ) -> Optional[uuid.UUID]:
154
+ if value is None:
155
+ return None
156
+ return value if isinstance(value, uuid.UUID) else uuid.UUID(value)
157
+
158
+ def coerce_compared_value(self, op: Any, value: Any) -> TypeDecorator:
159
+ # ensure STR comparisons are coerced through this type
160
+ return self
@@ -14,7 +14,7 @@
14
14
 
15
15
  from typing import Any, Optional, Union
16
16
 
17
- from tensorflow import keras
17
+ import tensorflow as tf
18
18
 
19
19
  import mlrun
20
20
  import mlrun.common.constants as mlrun_constants
@@ -27,11 +27,11 @@ from .utils import TFKerasTypes, TFKerasUtils
27
27
 
28
28
 
29
29
  def apply_mlrun(
30
- model: keras.Model = None,
30
+ model: tf.keras.Model = None,
31
31
  model_name: Optional[str] = None,
32
32
  tag: str = "",
33
33
  model_path: Optional[str] = None,
34
- model_format: str = TFKerasModelHandler.ModelFormats.SAVED_MODEL,
34
+ model_format: Optional[str] = None,
35
35
  save_traces: bool = False,
36
36
  modules_map: Optional[Union[dict[str, Union[None, str, list[str]]], str]] = None,
37
37
  custom_objects_map: Optional[Union[dict[str, Union[str, list[str]]], str]] = None,
@@ -54,7 +54,7 @@ def apply_mlrun(
54
54
  :param model_path: The model's store object path. Mandatory for evaluation (to know which model to
55
55
  update). If model is not provided, it will be loaded from this path.
56
56
  :param model_format: The format to use for saving and loading the model. Should be passed as a
57
- member of the class 'ModelFormats'. Default: 'ModelFormats.SAVED_MODEL'.
57
+ member of the class 'ModelFormats'.
58
58
  :param save_traces: Whether or not to use functions saving (only available for the 'SavedModel'
59
59
  format) for loading the model later without the custom objects dictionary. Only
60
60
  from tensorflow version >= 2.4.0. Using this setting will increase the model
@@ -16,14 +16,14 @@ from typing import Callable, Optional, Union
16
16
 
17
17
  import numpy as np
18
18
  import tensorflow as tf
19
- from tensorflow import Tensor, Variable
19
+ from tensorflow import keras
20
20
  from tensorflow.python.keras.callbacks import Callback
21
21
 
22
22
  import mlrun
23
23
 
24
24
  from ..._common import LoggingMode
25
25
  from ..._dl_common.loggers import Logger
26
- from ..utils import TFKerasTypes
26
+ from ..utils import TFKerasTypes, is_keras_3
27
27
 
28
28
 
29
29
  class LoggingCallback(Callback):
@@ -70,7 +70,7 @@ class LoggingCallback(Callback):
70
70
  {
71
71
  "epochs": 7
72
72
  }
73
- :param auto_log: Whether or not to enable auto logging, trying to track common static and dynamic
73
+ :param auto_log: Whether to enable auto logging, trying to track common static and dynamic
74
74
  hyperparameters.
75
75
  """
76
76
  super().__init__()
@@ -385,18 +385,24 @@ class LoggingCallback(Callback):
385
385
  self._logger.log_context_parameters()
386
386
 
387
387
  # Add learning rate:
388
- learning_rate_key = "lr"
389
- learning_rate_key_chain = ["optimizer", "lr"]
390
- if learning_rate_key not in self._dynamic_hyperparameters_keys and hasattr(
391
- self.model, "optimizer"
392
- ):
393
- try:
394
- self._get_hyperparameter(key_chain=learning_rate_key_chain)
388
+ learning_rate_keys = [
389
+ "learning_rate",
390
+ "lr",
391
+ ] # "lr" is for backward compatibility in older keras versions.
392
+ if all(
393
+ learning_rate_key not in self._dynamic_hyperparameters_keys
394
+ for learning_rate_key in learning_rate_keys
395
+ ) and hasattr(self.model, "optimizer"):
396
+ for learning_rate_key in learning_rate_keys:
397
+ learning_rate_key_chain = ["optimizer", learning_rate_key]
398
+ try:
399
+ self._get_hyperparameter(key_chain=learning_rate_key_chain)
400
+ except (KeyError, IndexError, AttributeError, ValueError):
401
+ continue
395
402
  self._dynamic_hyperparameters_keys[learning_rate_key] = (
396
403
  learning_rate_key_chain
397
404
  )
398
- except (KeyError, IndexError, ValueError):
399
- pass
405
+ break
400
406
 
401
407
  def _get_hyperparameter(
402
408
  self,
@@ -427,7 +433,7 @@ class LoggingCallback(Callback):
427
433
  value = value[key]
428
434
  else:
429
435
  value = getattr(value, key)
430
- except KeyError or IndexError as KeyChainError:
436
+ except KeyError or IndexError or AttributeError as KeyChainError:
431
437
  raise KeyChainError(
432
438
  f"Error during getting a hyperparameter value with the key chain {key_chain}. "
433
439
  f"The {value.__class__} in it does not have the following key/index from the key provided: "
@@ -435,7 +441,9 @@ class LoggingCallback(Callback):
435
441
  )
436
442
 
437
443
  # Parse the value:
438
- if isinstance(value, Tensor) or isinstance(value, Variable):
444
+ if isinstance(value, (tf.Tensor, tf.Variable)) or (
445
+ is_keras_3() and isinstance(value, (keras.KerasTensor, keras.Variable))
446
+ ):
439
447
  if int(tf.size(value)) == 1:
440
448
  value = float(value)
441
449
  else:
@@ -451,12 +459,7 @@ class LoggingCallback(Callback):
451
459
  f"The parameter with the following key chain: {key_chain} is a numpy.ndarray with {value.size} "
452
460
  f"elements. numpy arrays are trackable only if they have 1 element."
453
461
  )
454
- elif not (
455
- isinstance(value, float)
456
- or isinstance(value, int)
457
- or isinstance(value, str)
458
- or isinstance(value, bool)
459
- ):
462
+ elif not (isinstance(value, (float, int, str, bool))):
460
463
  raise mlrun.errors.MLRunInvalidArgumentError(
461
464
  f"The parameter with the following key chain: {key_chain} is of type '{type(value)}'. The only "
462
465
  f"trackable types are: float, int, str and bool."
@@ -280,7 +280,10 @@ class TFKerasMLRunInterface(MLRunInterface, ABC):
280
280
  print(f"Horovod worker #{self._hvd.rank()} is using CPU")
281
281
 
282
282
  # Adjust learning rate based on the number of GPUs:
283
- optimizer.lr = optimizer.lr * self._hvd.size()
283
+ if hasattr(self.optimizer, "lr"):
284
+ optimizer.lr *= self._hvd.size()
285
+ else:
286
+ optimizer.learning_rate *= self._hvd.size()
284
287
 
285
288
  # Wrap the optimizer in horovod's distributed optimizer: 'hvd.DistributedOptimizer'.
286
289
  optimizer = self._hvd.DistributedOptimizer(optimizer)
@@ -29,7 +29,7 @@ from mlrun.features import Feature
29
29
  from .._common import without_mlrun_interface
30
30
  from .._dl_common import DLModelHandler
31
31
  from .mlrun_interface import TFKerasMLRunInterface
32
- from .utils import TFKerasUtils
32
+ from .utils import TFKerasUtils, is_keras_3
33
33
 
34
34
 
35
35
  class TFKerasModelHandler(DLModelHandler):
@@ -40,8 +40,8 @@ class TFKerasModelHandler(DLModelHandler):
40
40
  # Framework name:
41
41
  FRAMEWORK_NAME = "tensorflow.keras"
42
42
 
43
- # Declare a type of an input sample:
44
- IOSample = Union[tf.Tensor, tf.TensorSpec, np.ndarray]
43
+ # Declare a type of input sample (only from keras v3 there is a KerasTensor type):
44
+ IOSample = Union[tf.Tensor, tf.TensorSpec, "keras.KerasTensor", np.ndarray]
45
45
 
46
46
  class ModelFormats:
47
47
  """
@@ -49,9 +49,19 @@ class TFKerasModelHandler(DLModelHandler):
49
49
  """
50
50
 
51
51
  SAVED_MODEL = "SavedModel"
52
+ KERAS = "keras"
52
53
  H5 = "h5"
53
54
  JSON_ARCHITECTURE_H5_WEIGHTS = "json_h5"
54
55
 
56
+ @classmethod
57
+ def default(cls) -> str:
58
+ """
59
+ Get the default model format to use for saving and loading the model based on the keras version.
60
+
61
+ :return: The default model format to use.
62
+ """
63
+ return cls.KERAS if is_keras_3() else cls.SAVED_MODEL
64
+
55
65
  class _LabelKeys:
56
66
  """
57
67
  Required labels keys to log with the model.
@@ -65,7 +75,7 @@ class TFKerasModelHandler(DLModelHandler):
65
75
  model: keras.Model = None,
66
76
  model_path: Optional[str] = None,
67
77
  model_name: Optional[str] = None,
68
- model_format: str = ModelFormats.SAVED_MODEL,
78
+ model_format: Optional[str] = None,
69
79
  context: mlrun.MLClientCtx = None,
70
80
  modules_map: Optional[
71
81
  Union[dict[str, Union[None, str, list[str]]], str]
@@ -98,7 +108,7 @@ class TFKerasModelHandler(DLModelHandler):
98
108
  * If given a loaded model object and the model name is None, the name will be
99
109
  set to the model's object name / class.
100
110
  :param model_format: The format to use for saving and loading the model. Should be passed as a
101
- member of the class 'ModelFormats'. Default: 'ModelFormats.SAVED_MODEL'.
111
+ member of the class 'ModelFormats'.
102
112
  :param context: MLRun context to work with for logging the model.
103
113
  :param modules_map: A dictionary of all the modules required for loading the model. Each key
104
114
  is a path to a module and its value is the object name to import from it. All
@@ -144,8 +154,11 @@ class TFKerasModelHandler(DLModelHandler):
144
154
  * 'save_traces' parameter was miss-used.
145
155
  """
146
156
  # Validate given format:
157
+ if not model_format:
158
+ model_format = TFKerasModelHandler.ModelFormats.default()
147
159
  if model_format not in [
148
160
  TFKerasModelHandler.ModelFormats.SAVED_MODEL,
161
+ TFKerasModelHandler.ModelFormats.KERAS,
149
162
  TFKerasModelHandler.ModelFormats.H5,
150
163
  TFKerasModelHandler.ModelFormats.JSON_ARCHITECTURE_H5_WEIGHTS,
151
164
  ]:
@@ -153,6 +166,22 @@ class TFKerasModelHandler(DLModelHandler):
153
166
  f"Unrecognized model format: '{model_format}'. Please use one of the class members of "
154
167
  "'TFKerasModelHandler.ModelFormats'"
155
168
  )
169
+ if not is_keras_3():
170
+ if model_format == TFKerasModelHandler.ModelFormats.KERAS:
171
+ raise mlrun.errors.MLRunInvalidArgumentError(
172
+ "The 'keras' model format is only supported in Keras 3.0.0 and above. "
173
+ f"Current version is {keras.__version__}."
174
+ )
175
+ else:
176
+ if (
177
+ model_format == TFKerasModelHandler.ModelFormats.SAVED_MODEL
178
+ or model_format
179
+ == TFKerasModelHandler.ModelFormats.JSON_ARCHITECTURE_H5_WEIGHTS
180
+ ):
181
+ raise mlrun.errors.MLRunInvalidArgumentError(
182
+ f"The '{model_format}' model format is not supported in Keras 3.0.0 and above. "
183
+ f"Current version is {keras.__version__}."
184
+ )
156
185
 
157
186
  # Validate 'save_traces':
158
187
  if save_traces:
@@ -239,11 +268,19 @@ class TFKerasModelHandler(DLModelHandler):
239
268
  self._model_file = f"{self._model_name}.h5"
240
269
  self._model.save(self._model_file)
241
270
 
271
+ # ModelFormats.keras - Save as a keras file:
272
+ elif self._model_format == self.ModelFormats.KERAS:
273
+ self._model_file = f"{self._model_name}.keras"
274
+ self._model.save(self._model_file)
275
+
242
276
  # ModelFormats.SAVED_MODEL - Save as a SavedModel directory and zip its file:
243
277
  elif self._model_format == TFKerasModelHandler.ModelFormats.SAVED_MODEL:
244
278
  # Save it in a SavedModel format directory:
279
+ # Note: Using keras>=3.0.0 can save in this format via `model.export` but then it won't be able to load it
280
+ # back, only for inference. So, we use the `save` method instead for keras 2 and validate the user won't use
281
+ # keras 3 and this model format.
245
282
  if self._save_traces is True:
246
- # Save traces can only be used in versions >= 2.4, so only if its true we use it in the call:
283
+ # Save traces can only be used in versions >= 2.4, so only if it's true, we use it in the call:
247
284
  self._model.save(self._model_name, save_traces=self._save_traces)
248
285
  else:
249
286
  self._model.save(self._model_name)
@@ -303,6 +340,12 @@ class TFKerasModelHandler(DLModelHandler):
303
340
  self._model_file, custom_objects=self._custom_objects
304
341
  )
305
342
 
343
+ # ModelFormats.KERAS - Load from a keras file:
344
+ elif self._model_format == TFKerasModelHandler.ModelFormats.KERAS:
345
+ self._model = keras.models.load_model(
346
+ self._model_file, custom_objects=self._custom_objects
347
+ )
348
+
306
349
  # ModelFormats.SAVED_MODEL - Load from a SavedModel directory:
307
350
  elif self._model_format == TFKerasModelHandler.ModelFormats.SAVED_MODEL:
308
351
  self._model = keras.models.load_model(
@@ -434,7 +477,10 @@ class TFKerasModelHandler(DLModelHandler):
434
477
  )
435
478
 
436
479
  # Read the inputs:
437
- input_signature = [input_layer.type_spec for input_layer in self._model.inputs]
480
+ input_signature = [
481
+ getattr(input_layer, "type_spec", input_layer)
482
+ for input_layer in self._model.inputs
483
+ ]
438
484
 
439
485
  # Set the inputs:
440
486
  self.set_inputs(from_sample=input_signature)
@@ -453,7 +499,8 @@ class TFKerasModelHandler(DLModelHandler):
453
499
 
454
500
  # Read the outputs:
455
501
  output_signature = [
456
- output_layer.type_spec for output_layer in self._model.outputs
502
+ getattr(output_layer, "type_spec", output_layer)
503
+ for output_layer in self._model.outputs
457
504
  ]
458
505
 
459
506
  # Set the outputs:
@@ -480,11 +527,22 @@ class TFKerasModelHandler(DLModelHandler):
480
527
  self._model_file = os.path.join(
481
528
  os.path.dirname(self._model_file), self._model_name
482
529
  )
530
+ elif self._model_format == TFKerasModelHandler.ModelFormats.KERAS:
531
+ # When keras tried to load it, it validates the suffix. The `artifacts.model.get_model` function is
532
+ # downloading the keras file to a temp file with a `pkl` suffix, so it needs to be replaced:
533
+ self._model_file = self._model_file.rsplit(".pkl", 1)[0] + ".keras"
534
+ elif self._model_format == TFKerasModelHandler.ModelFormats.H5:
535
+ # When keras tried to load it, it validates the suffix. The `artifacts.model.get_model` function is
536
+ # downloading the keras file to a temp file with a `pkl` suffix, so it needs to be replaced:
537
+ self._model_file = self._model_file.rsplit(".pkl", 1)[0] + ".h5"
483
538
  # # ModelFormats.JSON_ARCHITECTURE_H5_WEIGHTS - Get the weights file:
484
539
  elif (
485
540
  self._model_format
486
541
  == TFKerasModelHandler.ModelFormats.JSON_ARCHITECTURE_H5_WEIGHTS
487
542
  ):
543
+ # When keras tried to load it, it validates the suffix. The `artifacts.model.get_model` function is
544
+ # downloading the keras file to a temp file with a `pkl` suffix, so it needs to be replaced:
545
+ self._model_file = self._model_file.rsplit(".pkl", 1)[0] + ".json"
488
546
  # Get the weights file:
489
547
  self._weights_file = self._extra_data[
490
548
  self._get_weights_file_artifact_name()
@@ -509,6 +567,17 @@ class TFKerasModelHandler(DLModelHandler):
509
567
  f"'{self._model_path}'"
510
568
  )
511
569
 
570
+ # ModelFormats.KERAS - Get the keras model file:
571
+ elif self._model_format == TFKerasModelHandler.ModelFormats.KERAS:
572
+ self._model_file = os.path.join(
573
+ self._model_path, f"{self._model_name}.keras"
574
+ )
575
+ if not os.path.exists(self._model_file):
576
+ raise mlrun.errors.MLRunNotFoundError(
577
+ f"The model file '{self._model_name}.keras' was not found within the given 'model_path': "
578
+ f"'{self._model_path}'"
579
+ )
580
+
512
581
  # ModelFormats.SAVED_MODEL - Get the zip file and extract it, or simply locate the directory:
513
582
  elif self._model_format == TFKerasModelHandler.ModelFormats.SAVED_MODEL:
514
583
  self._model_file = os.path.join(self._model_path, f"{self._model_name}.zip")
@@ -559,7 +628,9 @@ class TFKerasModelHandler(DLModelHandler):
559
628
  # Supported types:
560
629
  if isinstance(sample, np.ndarray):
561
630
  return super()._read_sample(sample=sample)
562
- elif isinstance(sample, tf.TensorSpec):
631
+ elif isinstance(sample, tf.TensorSpec) or (
632
+ is_keras_3() and isinstance(sample, keras.KerasTensor)
633
+ ):
563
634
  return Feature(
564
635
  name=sample.name,
565
636
  value_type=TFKerasUtils.convert_tf_dtype_to_value_type(
@@ -11,8 +11,8 @@
11
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
-
15
14
  import tensorflow as tf
15
+ from packaging import version
16
16
  from tensorflow import keras
17
17
 
18
18
  import mlrun
@@ -117,3 +117,14 @@ class TFKerasUtils(DLUtils):
117
117
  raise mlrun.errors.MLRunInvalidArgumentError(
118
118
  f"MLRun value type is not supporting the given tensorflow data type: '{tf_dtype}'."
119
119
  )
120
+
121
+
122
+ def is_keras_3() -> bool:
123
+ """
124
+ Check if the current Keras version is 3.x.
125
+
126
+ :return: True if Keras version is 3.x, False otherwise.
127
+ """
128
+ return hasattr(keras, "__version__") and version.parse(
129
+ keras.__version__
130
+ ) >= version.parse("3.0.0")
mlrun/launcher/base.py CHANGED
@@ -82,7 +82,6 @@ class BaseLauncher(abc.ABC):
82
82
  runtime: "mlrun.runtimes.base.BaseRuntime",
83
83
  project_name: Optional[str] = "",
84
84
  full: bool = True,
85
- client_version: str = "",
86
85
  ):
87
86
  pass
88
87
 
@@ -148,6 +147,12 @@ class BaseLauncher(abc.ABC):
148
147
  self._validate_run_params(run.spec.parameters)
149
148
  self._validate_output_path(runtime, run)
150
149
 
150
+ for image in [
151
+ runtime.spec.image,
152
+ getattr(runtime.spec.build, "base_image", None),
153
+ ]:
154
+ mlrun.utils.helpers.warn_on_deprecated_image(image)
155
+
151
156
  @staticmethod
152
157
  def _validate_output_path(
153
158
  runtime: "mlrun.runtimes.BaseRuntime",
mlrun/launcher/client.py CHANGED
@@ -12,7 +12,6 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
  import abc
15
- import warnings
16
15
  from typing import Optional
17
16
 
18
17
  import IPython.display
@@ -37,7 +36,6 @@ class ClientBaseLauncher(launcher.BaseLauncher, abc.ABC):
37
36
  runtime: "mlrun.runtimes.base.BaseRuntime",
38
37
  project_name: Optional[str] = "",
39
38
  full: bool = True,
40
- client_version: str = "",
41
39
  ):
42
40
  runtime.try_auto_mount_based_on_config()
43
41
  runtime._fill_credentials()
@@ -63,26 +61,7 @@ class ClientBaseLauncher(launcher.BaseLauncher, abc.ABC):
63
61
  ):
64
62
  image = mlrun.mlconf.function_defaults.image_by_kind.to_dict()[runtime.kind]
65
63
 
66
- # Warn if user explicitly set the deprecated mlrun/ml-base image
67
- if image and "mlrun/ml-base" in image:
68
- client_version = mlrun.utils.version.Version().get()["version"]
69
- auto_replaced = mlrun.utils.validate_component_version_compatibility(
70
- "mlrun-client", "1.10.0", mlrun_client_version=client_version
71
- )
72
- message = (
73
- "'mlrun/ml-base' image is deprecated in 1.10.0 and will be removed in 1.12.0, "
74
- "use 'mlrun/mlrun' instead."
75
- )
76
- if auto_replaced:
77
- message += (
78
- " Since your client version is >= 1.10.0, the image will be automatically "
79
- "replaced with mlrun/mlrun."
80
- )
81
- warnings.warn(
82
- message,
83
- # TODO: Remove this in 1.12.0
84
- FutureWarning,
85
- )
64
+ mlrun.utils.helpers.warn_on_deprecated_image(image)
86
65
 
87
66
  # TODO: need a better way to decide whether a function requires a build
88
67
  if require_build and image and not runtime.spec.build.base_image:
mlrun/launcher/local.py CHANGED
@@ -13,7 +13,6 @@
13
13
  # limitations under the License.
14
14
  import os
15
15
  import pathlib
16
- from os import environ
17
16
  from typing import Callable, Optional, Union
18
17
 
19
18
  import mlrun.common.constants as mlrun_constants
@@ -252,9 +251,6 @@ class ClientLocalLauncher(launcher.ClientBaseLauncher):
252
251
  # copy the code/base-spec to the local function (for the UI and code logging)
253
252
  fn.spec.description = runtime.spec.description
254
253
  fn.spec.build = runtime.spec.build
255
- serving_spec = getattr(runtime.spec, "serving_spec", None)
256
- if serving_spec:
257
- environ["SERVING_SPEC_ENV"] = serving_spec
258
254
 
259
255
  run.spec.handler = handler
260
256
  run.spec.reset_on_run = reset_on_run
@@ -166,13 +166,29 @@ class ModelMonitoringApplicationBase(MonitoringApplicationToDict, ABC):
166
166
  return result
167
167
 
168
168
  @staticmethod
169
+ def _check_writer_is_up(project: "mlrun.MlrunProject") -> None:
170
+ try:
171
+ project.get_function(
172
+ mm_constants.MonitoringFunctionNames.WRITER, ignore_cache=True
173
+ )
174
+ except mlrun.errors.MLRunNotFoundError:
175
+ raise mlrun.errors.MLRunValueError(
176
+ "Writing outputs to the databases is blocked as the model monitoring infrastructure is disabled.\n"
177
+ "To unblock, enable model monitoring with `project.enable_model_monitoring()`."
178
+ )
179
+
180
+ @classmethod
169
181
  @contextmanager
170
182
  def _push_to_writer(
183
+ cls,
171
184
  *,
172
185
  write_output: bool,
173
186
  stream_profile: Optional[ds_profile.DatastoreProfile],
187
+ project: "mlrun.MlrunProject",
174
188
  ) -> Iterator[dict[str, list[tuple]]]:
175
189
  endpoints_output: dict[str, list[tuple]] = defaultdict(list)
190
+ if write_output:
191
+ cls._check_writer_is_up(project)
176
192
  try:
177
193
  yield endpoints_output
178
194
  finally:
@@ -220,6 +236,9 @@ class ModelMonitoringApplicationBase(MonitoringApplicationToDict, ABC):
220
236
  for an MLRun job.
221
237
  This method should not be called directly.
222
238
  """
239
+ project = context.get_project_object()
240
+ if not project:
241
+ raise mlrun.errors.MLRunValueError("Could not load project from context")
223
242
 
224
243
  if write_output and (
225
244
  not endpoints or sample_data is not None or reference_data is not None
@@ -236,7 +255,7 @@ class ModelMonitoringApplicationBase(MonitoringApplicationToDict, ABC):
236
255
  )
237
256
 
238
257
  with self._push_to_writer(
239
- write_output=write_output, stream_profile=stream_profile
258
+ write_output=write_output, stream_profile=stream_profile, project=project
240
259
  ) as endpoints_output:
241
260
 
242
261
  def call_do_tracking(event: Optional[dict] = None):
@@ -249,6 +268,7 @@ class ModelMonitoringApplicationBase(MonitoringApplicationToDict, ABC):
249
268
  event=event,
250
269
  application_name=self.__class__.__name__,
251
270
  context=context,
271
+ project=project,
252
272
  sample_df=sample_data,
253
273
  feature_stats=feature_stats,
254
274
  )
@@ -137,13 +137,14 @@ class MonitoringApplicationContext:
137
137
  cls,
138
138
  context: "mlrun.MLClientCtx",
139
139
  *,
140
+ project: Optional["mlrun.MlrunProject"] = None,
140
141
  application_name: str,
141
142
  event: dict[str, Any],
142
143
  model_endpoint_dict: Optional[dict[str, ModelEndpoint]] = None,
143
144
  sample_df: Optional[pd.DataFrame] = None,
144
145
  feature_stats: Optional[FeatureStats] = None,
145
146
  ) -> "MonitoringApplicationContext":
146
- project = context.get_project_object()
147
+ project = project or context.get_project_object()
147
148
  if not project:
148
149
  raise mlrun.errors.MLRunValueError("Could not load project from context")
149
150
  logger = context.logger