mlrun 1.7.0rc26__py3-none-any.whl → 1.7.0rc29__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 (66) hide show
  1. mlrun/__main__.py +7 -7
  2. mlrun/alerts/alert.py +13 -1
  3. mlrun/artifacts/manager.py +5 -0
  4. mlrun/common/constants.py +2 -2
  5. mlrun/common/formatters/base.py +9 -9
  6. mlrun/common/schemas/alert.py +4 -8
  7. mlrun/common/schemas/api_gateway.py +7 -0
  8. mlrun/common/schemas/constants.py +3 -0
  9. mlrun/common/schemas/model_monitoring/__init__.py +1 -0
  10. mlrun/common/schemas/model_monitoring/constants.py +27 -12
  11. mlrun/common/schemas/model_monitoring/model_endpoints.py +0 -12
  12. mlrun/common/schemas/schedule.py +1 -1
  13. mlrun/config.py +16 -9
  14. mlrun/datastore/azure_blob.py +2 -1
  15. mlrun/datastore/base.py +1 -5
  16. mlrun/datastore/datastore.py +3 -3
  17. mlrun/datastore/inmem.py +1 -1
  18. mlrun/datastore/snowflake_utils.py +3 -1
  19. mlrun/datastore/sources.py +26 -11
  20. mlrun/datastore/store_resources.py +2 -0
  21. mlrun/datastore/targets.py +60 -25
  22. mlrun/db/base.py +10 -0
  23. mlrun/db/httpdb.py +41 -30
  24. mlrun/db/nopdb.py +10 -1
  25. mlrun/errors.py +4 -0
  26. mlrun/execution.py +18 -10
  27. mlrun/feature_store/retrieval/spark_merger.py +2 -1
  28. mlrun/launcher/local.py +2 -2
  29. mlrun/model.py +30 -0
  30. mlrun/model_monitoring/api.py +6 -52
  31. mlrun/model_monitoring/applications/histogram_data_drift.py +4 -1
  32. mlrun/model_monitoring/db/stores/__init__.py +21 -9
  33. mlrun/model_monitoring/db/stores/base/store.py +39 -1
  34. mlrun/model_monitoring/db/stores/sqldb/models/base.py +9 -7
  35. mlrun/model_monitoring/db/stores/sqldb/models/mysql.py +4 -2
  36. mlrun/model_monitoring/db/stores/sqldb/sql_store.py +34 -79
  37. mlrun/model_monitoring/db/stores/v3io_kv/kv_store.py +19 -27
  38. mlrun/model_monitoring/db/tsdb/__init__.py +19 -14
  39. mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +4 -2
  40. mlrun/model_monitoring/helpers.py +9 -5
  41. mlrun/model_monitoring/writer.py +1 -5
  42. mlrun/projects/operations.py +1 -0
  43. mlrun/projects/project.py +71 -75
  44. mlrun/render.py +10 -5
  45. mlrun/run.py +2 -2
  46. mlrun/runtimes/daskjob.py +7 -1
  47. mlrun/runtimes/local.py +24 -7
  48. mlrun/runtimes/nuclio/function.py +20 -0
  49. mlrun/runtimes/pod.py +5 -29
  50. mlrun/serving/routers.py +75 -59
  51. mlrun/serving/server.py +1 -0
  52. mlrun/serving/v2_serving.py +8 -1
  53. mlrun/utils/helpers.py +46 -2
  54. mlrun/utils/logger.py +36 -2
  55. mlrun/utils/notifications/notification/base.py +4 -0
  56. mlrun/utils/notifications/notification/git.py +21 -0
  57. mlrun/utils/notifications/notification/slack.py +8 -0
  58. mlrun/utils/notifications/notification/webhook.py +41 -1
  59. mlrun/utils/notifications/notification_pusher.py +2 -2
  60. mlrun/utils/version/version.json +2 -2
  61. {mlrun-1.7.0rc26.dist-info → mlrun-1.7.0rc29.dist-info}/METADATA +9 -4
  62. {mlrun-1.7.0rc26.dist-info → mlrun-1.7.0rc29.dist-info}/RECORD +66 -66
  63. {mlrun-1.7.0rc26.dist-info → mlrun-1.7.0rc29.dist-info}/WHEEL +1 -1
  64. {mlrun-1.7.0rc26.dist-info → mlrun-1.7.0rc29.dist-info}/LICENSE +0 -0
  65. {mlrun-1.7.0rc26.dist-info → mlrun-1.7.0rc29.dist-info}/entry_points.txt +0 -0
  66. {mlrun-1.7.0rc26.dist-info → mlrun-1.7.0rc29.dist-info}/top_level.txt +0 -0
@@ -195,7 +195,10 @@ class HistogramDataDriftApplication(ModelMonitoringApplicationBaseV2):
195
195
  EventFieldType.CURRENT_STATS: json.dumps(
196
196
  monitoring_context.sample_df_stats
197
197
  ),
198
- EventFieldType.DRIFT_MEASURES: metrics_per_feature.T.to_json(),
198
+ EventFieldType.DRIFT_MEASURES: json.dumps(
199
+ metrics_per_feature.T.to_dict()
200
+ | {metric.name: metric.value for metric in metrics}
201
+ ),
199
202
  EventFieldType.DRIFT_STATUS: status.value,
200
203
  },
201
204
  )
@@ -63,7 +63,7 @@ class ObjectStoreFactory(enum.Enum):
63
63
  :param value: Provided enum (invalid) value.
64
64
  """
65
65
  valid_values = list(cls.__members__.keys())
66
- raise mlrun.errors.MLRunInvalidArgumentError(
66
+ raise mlrun.errors.MLRunInvalidMMStoreType(
67
67
  f"{value} is not a valid endpoint store, please choose a valid value: %{valid_values}."
68
68
  )
69
69
 
@@ -88,21 +88,28 @@ def get_model_endpoint_store(
88
88
  def get_store_object(
89
89
  project: str,
90
90
  secret_provider: typing.Optional[typing.Callable[[str], str]] = None,
91
+ store_connection_string: typing.Optional[str] = None,
91
92
  **kwargs,
92
93
  ) -> StoreBase:
93
94
  """
94
95
  Generate a store object. If a connection string is provided, the store type will be updated according to the
95
96
  connection string. Currently, the supported store types are SQL and v3io-nosql.
96
97
 
97
- :param project: The name of the project.
98
- :param secret_provider: An optional secret provider to get the connection string secret.
98
+ :param project: The name of the project.
99
+ :param secret_provider: An optional secret provider to get the connection string secret.
100
+ :param store_connection_string: Optional explicit connection string of the store.
99
101
 
100
102
  :return: `StoreBase` object. Using this object, the user can apply different operations such as write, update, get
101
- and delete a model endpoint record.
103
+ and delete a model endpoint record.
104
+ :raise: `MLRunInvalidMMStoreType` if the user didn't provide store connection
105
+ or the provided store connection is invalid.
102
106
  """
103
107
 
104
- store_connection_string = mlrun.model_monitoring.helpers.get_connection_string(
105
- secret_provider=secret_provider
108
+ store_connection_string = (
109
+ store_connection_string
110
+ or mlrun.model_monitoring.helpers.get_connection_string(
111
+ secret_provider=secret_provider
112
+ )
106
113
  )
107
114
 
108
115
  if store_connection_string and (
@@ -111,10 +118,15 @@ def get_store_object(
111
118
  ):
112
119
  store_type = mlrun.common.schemas.model_monitoring.ModelEndpointTarget.SQL
113
120
  kwargs["store_connection_string"] = store_connection_string
121
+ elif store_connection_string and store_connection_string == "v3io":
122
+ store_type = (
123
+ mlrun.common.schemas.model_monitoring.ModelEndpointTarget.V3IO_NOSQL
124
+ )
114
125
  else:
115
- # Set the default store type if no connection has been set
116
- store_type = mlrun.mlconf.model_endpoint_monitoring.store_type
117
-
126
+ raise mlrun.errors.MLRunInvalidMMStoreType(
127
+ "You must provide a valid store connection by using "
128
+ "set_model_monitoring_credentials API."
129
+ )
118
130
  # Get store type value from ObjectStoreFactory enum class
119
131
  store_type_fact = ObjectStoreFactory(store_type)
120
132
 
@@ -11,7 +11,7 @@
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
-
14
+ import json
15
15
  import typing
16
16
  from abc import ABC, abstractmethod
17
17
 
@@ -170,3 +170,41 @@ class StoreBase(ABC):
170
170
 
171
171
  :return: A list of the available metrics.
172
172
  """
173
+
174
+ @staticmethod
175
+ def _validate_labels(
176
+ endpoint_dict: dict,
177
+ labels: list,
178
+ ) -> bool:
179
+ """Validate that the model endpoint dictionary has the provided labels. There are 2 possible cases:
180
+ 1 - Labels were provided as a list of key-values pairs (e.g. ['label_1=value_1', 'label_2=value_2']): Validate
181
+ that each pair exist in the endpoint dictionary.
182
+ 2 - Labels were provided as a list of key labels (e.g. ['label_1', 'label_2']): Validate that each key exist in
183
+ the endpoint labels dictionary.
184
+
185
+ :param endpoint_dict: Dictionary of the model endpoint records.
186
+ :param labels: List of dictionary of required labels.
187
+
188
+ :return: True if the labels exist in the endpoint labels dictionary, otherwise False.
189
+ """
190
+
191
+ # Convert endpoint labels into dictionary
192
+ endpoint_labels = json.loads(
193
+ endpoint_dict.get(mm_schemas.EventFieldType.LABELS)
194
+ )
195
+
196
+ for label in labels:
197
+ # Case 1 - label is a key=value pair
198
+ if "=" in label:
199
+ lbl, value = list(map(lambda x: x.strip(), label.split("=")))
200
+ if lbl not in endpoint_labels or str(endpoint_labels[lbl]) != value:
201
+ return False
202
+ # Case 2 - label is just a key
203
+ else:
204
+ if label not in endpoint_labels:
205
+ return False
206
+
207
+ return True
208
+
209
+ def create_tables(self):
210
+ pass
@@ -11,8 +11,10 @@
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
+
14
15
  from sqlalchemy import (
15
- TIMESTAMP,
16
+ DATETIME,
17
+ TIMESTAMP, # TODO: migrate to DATETIME, see ML-6921
16
18
  Boolean,
17
19
  Column,
18
20
  Float,
@@ -90,11 +92,11 @@ class ModelEndpointsBaseTable(BaseModel):
90
92
  metrics = Column(EventFieldType.METRICS, Text)
91
93
  first_request = Column(
92
94
  EventFieldType.FIRST_REQUEST,
93
- TIMESTAMP(timezone=True),
95
+ TIMESTAMP(timezone=True), # TODO: migrate to DATETIME, see ML-6921
94
96
  )
95
97
  last_request = Column(
96
98
  EventFieldType.LAST_REQUEST,
97
- TIMESTAMP(timezone=True),
99
+ TIMESTAMP(timezone=True), # TODO: migrate to DATETIME, see ML-6921
98
100
  )
99
101
 
100
102
 
@@ -122,11 +124,11 @@ class ApplicationResultBaseTable(BaseModel):
122
124
 
123
125
  start_infer_time = Column(
124
126
  WriterEvent.START_INFER_TIME,
125
- TIMESTAMP(timezone=True),
127
+ DATETIME(timezone=True),
126
128
  )
127
129
  end_infer_time = Column(
128
130
  WriterEvent.END_INFER_TIME,
129
- TIMESTAMP(timezone=True),
131
+ DATETIME(timezone=True),
130
132
  )
131
133
 
132
134
  result_status = Column(ResultData.RESULT_STATUS, String(10))
@@ -152,11 +154,11 @@ class ApplicationMetricsBaseTable(BaseModel):
152
154
  )
153
155
  start_infer_time = Column(
154
156
  WriterEvent.START_INFER_TIME,
155
- TIMESTAMP(timezone=True),
157
+ DATETIME(timezone=True),
156
158
  )
157
159
  end_infer_time = Column(
158
160
  WriterEvent.END_INFER_TIME,
159
- TIMESTAMP(timezone=True),
161
+ DATETIME(timezone=True),
160
162
  )
161
163
  metric_name = Column(
162
164
  MetricData.METRIC_NAME,
@@ -34,10 +34,12 @@ Base = declarative_base()
34
34
  class ModelEndpointsTable(Base, ModelEndpointsBaseTable):
35
35
  first_request = Column(
36
36
  EventFieldType.FIRST_REQUEST,
37
+ # TODO: migrate to DATETIME, see ML-6921
37
38
  sqlalchemy.dialects.mysql.TIMESTAMP(fsp=3, timezone=True),
38
39
  )
39
40
  last_request = Column(
40
41
  EventFieldType.LAST_REQUEST,
42
+ # TODO: migrate to DATETIME, see ML-6921
41
43
  sqlalchemy.dialects.mysql.TIMESTAMP(fsp=3, timezone=True),
42
44
  )
43
45
 
@@ -52,11 +54,11 @@ class _ApplicationResultOrMetric:
52
54
 
53
55
  start_infer_time = Column(
54
56
  WriterEvent.START_INFER_TIME,
55
- sqlalchemy.dialects.mysql.TIMESTAMP(fsp=3, timezone=True),
57
+ sqlalchemy.dialects.mysql.DATETIME(fsp=3, timezone=True),
56
58
  )
57
59
  end_infer_time = Column(
58
60
  WriterEvent.END_INFER_TIME,
59
- sqlalchemy.dialects.mysql.TIMESTAMP(fsp=3, timezone=True),
61
+ sqlalchemy.dialects.mysql.DATETIME(fsp=3, timezone=True),
60
62
  )
61
63
 
62
64
  @declared_attr
@@ -13,7 +13,6 @@
13
13
  # limitations under the License.
14
14
 
15
15
  import datetime
16
- import json
17
16
  import typing
18
17
  import uuid
19
18
 
@@ -21,6 +20,7 @@ import pandas as pd
21
20
  import sqlalchemy
22
21
  import sqlalchemy.exc
23
22
  import sqlalchemy.orm
23
+ from sqlalchemy.engine import make_url
24
24
  from sqlalchemy.sql.elements import BinaryExpression
25
25
 
26
26
  import mlrun.common.model_monitoring.helpers
@@ -62,6 +62,10 @@ class SQLStoreBase(StoreBase):
62
62
 
63
63
  self._sql_connection_string = kwargs.get("store_connection_string")
64
64
  self._engine = get_engine(dsn=self._sql_connection_string)
65
+ self._init_tables()
66
+
67
+ def create_tables(self):
68
+ self._create_tables_if_not_exist()
65
69
 
66
70
  def _init_tables(self):
67
71
  self._init_model_endpoints_table()
@@ -70,13 +74,13 @@ class SQLStoreBase(StoreBase):
70
74
  self._init_monitoring_schedules_table()
71
75
 
72
76
  def _init_model_endpoints_table(self):
73
- self.ModelEndpointsTable = (
77
+ self.model_endpoints_table = (
74
78
  mlrun.model_monitoring.db.stores.sqldb.models._get_model_endpoints_table(
75
79
  connection_string=self._sql_connection_string
76
80
  )
77
81
  )
78
82
  self._tables[mm_schemas.EventFieldType.MODEL_ENDPOINTS] = (
79
- self.ModelEndpointsTable
83
+ self.model_endpoints_table
80
84
  )
81
85
 
82
86
  def _init_application_results_table(self):
@@ -150,22 +154,17 @@ class SQLStoreBase(StoreBase):
150
154
  :param criteria: A list of binary expressions that filter the query.
151
155
  """
152
156
  with create_session(dsn=self._sql_connection_string) as session:
153
- try:
154
- logger.debug(
155
- "Querying the DB",
156
- table=table.__name__,
157
- criteria=[str(criterion) for criterion in criteria],
158
- )
159
- # Generate the get query
160
- return (
161
- session.query(table) # pyright: ignore[reportOptionalCall]
162
- .filter(*criteria)
163
- .one_or_none()
164
- )
165
- except sqlalchemy.exc.ProgrammingError:
166
- # Probably table doesn't exist, try to create tables
167
- self._create_tables_if_not_exist()
168
- return
157
+ logger.debug(
158
+ "Querying the DB",
159
+ table=table.__name__,
160
+ criteria=[str(criterion) for criterion in criteria],
161
+ )
162
+ # Generate the get query
163
+ return (
164
+ session.query(table) # pyright: ignore[reportOptionalCall]
165
+ .filter(*criteria)
166
+ .one_or_none()
167
+ )
169
168
 
170
169
  def _delete(
171
170
  self,
@@ -213,14 +212,13 @@ class SQLStoreBase(StoreBase):
213
212
  of the attributes dictionary should exist in the SQL table.
214
213
 
215
214
  """
216
- self._init_model_endpoints_table()
217
215
 
218
216
  attributes.pop(mm_schemas.EventFieldType.ENDPOINT_ID, None)
219
217
 
220
218
  self._update(
221
219
  attributes=attributes,
222
- table=self.ModelEndpointsTable,
223
- criteria=[self.ModelEndpointsTable.uid == endpoint_id],
220
+ table=self.model_endpoints_table,
221
+ criteria=[self.model_endpoints_table.uid == endpoint_id],
224
222
  )
225
223
 
226
224
  def delete_model_endpoint(self, endpoint_id: str) -> None:
@@ -229,11 +227,10 @@ class SQLStoreBase(StoreBase):
229
227
 
230
228
  :param endpoint_id: The unique id of the model endpoint.
231
229
  """
232
- self._init_model_endpoints_table()
233
230
  # Delete the model endpoint record using sqlalchemy ORM
234
231
  self._delete(
235
- table=self.ModelEndpointsTable,
236
- criteria=[self.ModelEndpointsTable.uid == endpoint_id],
232
+ table=self.model_endpoints_table,
233
+ criteria=[self.model_endpoints_table.uid == endpoint_id],
237
234
  )
238
235
 
239
236
  def get_model_endpoint(
@@ -249,12 +246,11 @@ class SQLStoreBase(StoreBase):
249
246
 
250
247
  :raise MLRunNotFoundError: If the model endpoints table was not found or the model endpoint id was not found.
251
248
  """
252
- self._init_model_endpoints_table()
253
249
 
254
250
  # Get the model endpoint record
255
251
  endpoint_record = self._get(
256
- table=self.ModelEndpointsTable,
257
- criteria=[self.ModelEndpointsTable.uid == endpoint_id],
252
+ table=self.model_endpoints_table,
253
+ criteria=[self.model_endpoints_table.uid == endpoint_id],
258
254
  )
259
255
 
260
256
  if not endpoint_record:
@@ -286,23 +282,22 @@ class SQLStoreBase(StoreBase):
286
282
 
287
283
  :return: A list of model endpoint dictionaries.
288
284
  """
289
- self._init_model_endpoints_table()
290
285
  # Generate an empty model endpoints that will be filled afterwards with model endpoint dictionaries
291
286
  endpoint_list = []
292
287
 
293
288
  model_endpoints_table = (
294
- self.ModelEndpointsTable.__table__ # pyright: ignore[reportAttributeAccessIssue]
289
+ self.model_endpoints_table.__table__ # pyright: ignore[reportAttributeAccessIssue]
295
290
  )
296
-
297
291
  # Get the model endpoints records using sqlalchemy ORM
298
292
  with create_session(dsn=self._sql_connection_string) as session:
299
293
  # Generate the list query
300
- query = session.query(self.ModelEndpointsTable).filter_by(
294
+ query = session.query(self.model_endpoints_table).filter_by(
301
295
  project=self.project
302
296
  )
303
297
 
304
298
  # Apply filters
305
299
  if model:
300
+ model = model if ":" in model else f"{model}:latest"
306
301
  query = self._filter_values(
307
302
  query=query,
308
303
  model_endpoints_table=model_endpoints_table,
@@ -310,11 +305,12 @@ class SQLStoreBase(StoreBase):
310
305
  filtered_values=[model],
311
306
  )
312
307
  if function:
308
+ function_uri = f"{self.project}/{function}"
313
309
  query = self._filter_values(
314
310
  query=query,
315
311
  model_endpoints_table=model_endpoints_table,
316
- key_filter=mm_schemas.EventFieldType.FUNCTION,
317
- filtered_values=[function],
312
+ key_filter=mm_schemas.EventFieldType.FUNCTION_URI,
313
+ filtered_values=[function_uri],
318
314
  )
319
315
  if uids:
320
316
  query = self._filter_values(
@@ -364,11 +360,9 @@ class SQLStoreBase(StoreBase):
364
360
  """
365
361
 
366
362
  if kind == mm_schemas.WriterEventKind.METRIC:
367
- self._init_application_metrics_table()
368
363
  table = self.application_metrics_table
369
364
  table_name = mm_schemas.FileTargetKind.APP_METRICS
370
365
  elif kind == mm_schemas.WriterEventKind.RESULT:
371
- self._init_application_results_table()
372
366
  table = self.application_results_table
373
367
  table_name = mm_schemas.FileTargetKind.APP_RESULTS
374
368
  else:
@@ -442,7 +436,6 @@ class SQLStoreBase(StoreBase):
442
436
  :return: Timestamp as a Unix time.
443
437
  :raise: MLRunNotFoundError if last analyzed value is not found.
444
438
  """
445
- self._init_monitoring_schedules_table()
446
439
  monitoring_schedule_record = self._get(
447
440
  table=self.MonitoringSchedulesTable,
448
441
  criteria=self._get_filter_criteria(
@@ -469,8 +462,6 @@ class SQLStoreBase(StoreBase):
469
462
  :param last_analyzed: Timestamp as a Unix time that represents the last analyzed time of a certain
470
463
  application and model endpoint.
471
464
  """
472
- self._init_monitoring_schedules_table()
473
-
474
465
  criteria = self._get_filter_criteria(
475
466
  table=self.MonitoringSchedulesTable,
476
467
  endpoint_id=endpoint_id,
@@ -500,7 +491,6 @@ class SQLStoreBase(StoreBase):
500
491
  def _delete_last_analyzed(
501
492
  self, endpoint_id: str, application_name: typing.Optional[str] = None
502
493
  ) -> None:
503
- self._init_monitoring_schedules_table()
504
494
  criteria = self._get_filter_criteria(
505
495
  table=self.MonitoringSchedulesTable,
506
496
  endpoint_id=endpoint_id,
@@ -512,7 +502,6 @@ class SQLStoreBase(StoreBase):
512
502
  def _delete_application_result(
513
503
  self, endpoint_id: str, application_name: typing.Optional[str] = None
514
504
  ) -> None:
515
- self._init_application_results_table()
516
505
  criteria = self._get_filter_criteria(
517
506
  table=self.application_results_table,
518
507
  endpoint_id=endpoint_id,
@@ -524,7 +513,6 @@ class SQLStoreBase(StoreBase):
524
513
  def _delete_application_metrics(
525
514
  self, endpoint_id: str, application_name: typing.Optional[str] = None
526
515
  ) -> None:
527
- self._init_application_metrics_table()
528
516
  criteria = self._get_filter_criteria(
529
517
  table=self.application_metrics_table,
530
518
  endpoint_id=endpoint_id,
@@ -538,8 +526,12 @@ class SQLStoreBase(StoreBase):
538
526
 
539
527
  for table in self._tables:
540
528
  # Create table if not exist. The `metadata` contains the `ModelEndpointsTable`
529
+ db_name = make_url(self._sql_connection_string).database
541
530
  if not self._engine.has_table(table):
531
+ logger.info(f"Creating table {table} on {db_name} db.")
542
532
  self._tables[table].metadata.create_all(bind=self._engine)
533
+ else:
534
+ logger.info(f"Table {table} already exists on {db_name} db.")
543
535
 
544
536
  @staticmethod
545
537
  def _filter_values(
@@ -581,41 +573,6 @@ class SQLStoreBase(StoreBase):
581
573
  # Apply AND operator on the SQL query object with the filters tuple
582
574
  return query.filter(sqlalchemy.and_(*filter_query))
583
575
 
584
- @staticmethod
585
- def _validate_labels(
586
- endpoint_dict: dict,
587
- labels: list,
588
- ) -> bool:
589
- """Validate that the model endpoint dictionary has the provided labels. There are 2 possible cases:
590
- 1 - Labels were provided as a list of key-values pairs (e.g. ['label_1=value_1', 'label_2=value_2']): Validate
591
- that each pair exist in the endpoint dictionary.
592
- 2 - Labels were provided as a list of key labels (e.g. ['label_1', 'label_2']): Validate that each key exist in
593
- the endpoint labels dictionary.
594
-
595
- :param endpoint_dict: Dictionary of the model endpoint records.
596
- :param labels: List of dictionary of required labels.
597
-
598
- :return: True if the labels exist in the endpoint labels dictionary, otherwise False.
599
- """
600
-
601
- # Convert endpoint labels into dictionary
602
- endpoint_labels = json.loads(
603
- endpoint_dict.get(mm_schemas.EventFieldType.LABELS)
604
- )
605
-
606
- for label in labels:
607
- # Case 1 - label is a key=value pair
608
- if "=" in label:
609
- lbl, value = list(map(lambda x: x.strip(), label.split("=")))
610
- if lbl not in endpoint_labels or str(endpoint_labels[lbl]) != value:
611
- return False
612
- # Case 2 - label is just a key
613
- else:
614
- if label not in endpoint_labels:
615
- return False
616
-
617
- return True
618
-
619
576
  def delete_model_endpoints_resources(self) -> None:
620
577
  """
621
578
  Delete all the model monitoring resources of the project in the SQL tables.
@@ -650,11 +607,9 @@ class SQLStoreBase(StoreBase):
650
607
  type=type,
651
608
  )
652
609
  if type == mm_schemas.ModelEndpointMonitoringMetricType.METRIC:
653
- self._init_application_metrics_table()
654
610
  table = self.application_metrics_table
655
611
  name_col = mm_schemas.MetricData.METRIC_NAME
656
612
  else:
657
- self._init_application_results_table()
658
613
  table = self.application_results_table
659
614
  name_col = mm_schemas.ResultData.RESULT_NAME
660
615
 
@@ -256,7 +256,6 @@ class KVStoreBase(StoreBase):
256
256
  self.project,
257
257
  function,
258
258
  model,
259
- labels,
260
259
  top_level,
261
260
  ),
262
261
  raise_for_status=v3io.dataplane.RaiseForStatus.never,
@@ -269,7 +268,6 @@ class KVStoreBase(StoreBase):
269
268
  exc=mlrun.errors.err_to_str(exc),
270
269
  )
271
270
  return endpoint_list
272
-
273
271
  # Create a list of model endpoints unique ids
274
272
  if uids is None:
275
273
  uids = []
@@ -282,10 +280,16 @@ class KVStoreBase(StoreBase):
282
280
 
283
281
  # Add each relevant model endpoint to the model endpoints list
284
282
  for endpoint_id in uids:
285
- endpoint = self.get_model_endpoint(
283
+ endpoint_dict = self.get_model_endpoint(
286
284
  endpoint_id=endpoint_id,
287
285
  )
288
- endpoint_list.append(endpoint)
286
+
287
+ if labels and not self._validate_labels(
288
+ endpoint_dict=endpoint_dict, labels=labels
289
+ ):
290
+ continue
291
+
292
+ endpoint_list.append(endpoint_dict)
289
293
 
290
294
  return endpoint_list
291
295
 
@@ -509,20 +513,16 @@ class KVStoreBase(StoreBase):
509
513
  project: str,
510
514
  function: str = None,
511
515
  model: str = None,
512
- labels: list[str] = None,
513
516
  top_level: bool = False,
514
517
  ) -> str:
515
518
  """
516
519
  Convert the provided filters into a valid filter expression. The expected filter expression includes different
517
520
  conditions, divided by ' AND '.
518
521
 
519
- :param project: The name of the project.
520
- :param model: The name of the model to filter by.
521
- :param function: The name of the function to filter by.
522
- :param labels: A list of labels to filter by. Label filters work by either filtering a specific value of
523
- a label (i.e. list("key=value")) or by looking for the existence of a given
524
- key (i.e. "key").
525
- :param top_level: If True will return only routers and endpoint that are NOT children of any router.
522
+ :param project: The name of the project.
523
+ :param model: The name of the model to filter by.
524
+ :param function: The name of the function to filter by.
525
+ :param top_level: If True will return only routers and endpoint that are NOT children of any router.
526
526
 
527
527
  :return: A valid filter expression as a string.
528
528
 
@@ -533,25 +533,17 @@ class KVStoreBase(StoreBase):
533
533
  raise mlrun.errors.MLRunInvalidArgumentError("project can't be empty")
534
534
 
535
535
  # Add project filter
536
- filter_expression = [f"project=='{project}'"]
536
+ filter_expression = [f"{mm_schemas.EventFieldType.PROJECT}=='{project}'"]
537
537
 
538
538
  # Add function and model filters
539
539
  if function:
540
- filter_expression.append(f"function=='{function}'")
540
+ function_uri = f"{project}/{function}" if function else None
541
+ filter_expression.append(
542
+ f"{mm_schemas.EventFieldType.FUNCTION_URI}=='{function_uri}'"
543
+ )
541
544
  if model:
542
- filter_expression.append(f"model=='{model}'")
543
-
544
- # Add labels filters
545
- if labels:
546
- for label in labels:
547
- if not label.startswith("_"):
548
- label = f"_{label}"
549
-
550
- if "=" in label:
551
- lbl, value = list(map(lambda x: x.strip(), label.split("=")))
552
- filter_expression.append(f"{lbl}=='{value}'")
553
- else:
554
- filter_expression.append(f"exists({label})")
545
+ model = model if ":" in model else f"{model}:latest"
546
+ filter_expression.append(f"{mm_schemas.EventFieldType.MODEL}=='{model}'")
555
547
 
556
548
  # Apply top_level filter (remove endpoints that considered a child of a router)
557
549
  if top_level:
@@ -57,41 +57,46 @@ class ObjectTSDBFactory(enum.Enum):
57
57
  :param value: Provided enum (invalid) value.
58
58
  """
59
59
  valid_values = list(cls.__members__.keys())
60
- raise mlrun.errors.MLRunInvalidArgumentError(
60
+ raise mlrun.errors.MLRunInvalidMMStoreType(
61
61
  f"{value} is not a valid tsdb, please choose a valid value: %{valid_values}."
62
62
  )
63
63
 
64
64
 
65
65
  def get_tsdb_connector(
66
66
  project: str,
67
- tsdb_connector_type: str = "",
68
67
  secret_provider: typing.Optional[typing.Callable[[str], str]] = None,
68
+ tsdb_connection_string: typing.Optional[str] = None,
69
69
  **kwargs,
70
70
  ) -> TSDBConnector:
71
71
  """
72
72
  Get TSDB connector object.
73
- :param project: The name of the project.
74
- :param tsdb_connector_type: The type of the TSDB connector. See mlrun.model_monitoring.db.tsdb.ObjectTSDBFactory
75
- for available options.
76
- :param secret_provider: An optional secret provider to get the connection string secret.
73
+ :param project: The name of the project.
74
+ :param secret_provider: An optional secret provider to get the connection string secret.
75
+ :param tsdb_connection_string: An optional explicit connection string to the TSDB.
77
76
 
78
77
  :return: `TSDBConnector` object. The main goal of this object is to handle different operations on the
79
78
  TSDB connector such as updating drift metrics or write application record result.
79
+ :raise: `MLRunInvalidMMStoreType` if the user didn't provide TSDB connection
80
+ or the provided TSDB connection is invalid.
80
81
  """
81
82
 
82
- tsdb_connection_string = mlrun.model_monitoring.helpers.get_tsdb_connection_string(
83
- secret_provider=secret_provider
83
+ tsdb_connection_string = (
84
+ tsdb_connection_string
85
+ or mlrun.model_monitoring.helpers.get_tsdb_connection_string(
86
+ secret_provider=secret_provider
87
+ )
84
88
  )
85
89
 
86
90
  if tsdb_connection_string and tsdb_connection_string.startswith("taosws"):
87
91
  tsdb_connector_type = mlrun.common.schemas.model_monitoring.TSDBTarget.TDEngine
88
92
  kwargs["connection_string"] = tsdb_connection_string
89
-
90
- # Set the default TSDB connector type if no connection has been set
91
- tsdb_connector_type = (
92
- tsdb_connector_type
93
- or mlrun.mlconf.model_endpoint_monitoring.tsdb_connector_type
94
- )
93
+ elif tsdb_connection_string and tsdb_connection_string == "v3io":
94
+ tsdb_connector_type = mlrun.common.schemas.model_monitoring.TSDBTarget.V3IO_TSDB
95
+ else:
96
+ raise mlrun.errors.MLRunInvalidMMStoreType(
97
+ "You must provide a valid tsdb store connection by using "
98
+ "set_model_monitoring_credentials API."
99
+ )
95
100
 
96
101
  # Get connector type value from ObjectTSDBFactory enum class
97
102
  tsdb_connector_factory = ObjectTSDBFactory(tsdb_connector_type)
@@ -38,7 +38,10 @@ def _is_no_schema_error(exc: v3io_frames.ReadError) -> bool:
38
38
  In case of a nonexistent TSDB table - a `v3io_frames.ReadError` error is raised.
39
39
  Check if the error message contains the relevant string to verify the cause.
40
40
  """
41
- return "No TSDB schema file found" in str(exc)
41
+ msg = str(exc)
42
+ # https://github.com/v3io/v3io-tsdb/blob/v0.14.1/pkg/tsdb/v3iotsdb.go#L205
43
+ # https://github.com/v3io/v3io-tsdb/blob/v0.14.1/pkg/partmgr/partmgr.go#L238
44
+ return "No TSDB schema file found" in msg or "Failed to read schema at path" in msg
42
45
 
43
46
 
44
47
  class V3IOTSDBConnector(TSDBConnector):
@@ -596,7 +599,6 @@ class V3IOTSDBConnector(TSDBConnector):
596
599
  end=end,
597
600
  columns=[mm_schemas.EventFieldType.LATENCY],
598
601
  filter_query=f"endpoint_id=='{endpoint_id}'",
599
- interval=aggregation_window,
600
602
  agg_funcs=agg_funcs,
601
603
  sliding_window_step=aggregation_window,
602
604
  )