mlrun 1.7.0rc26__py3-none-any.whl → 1.7.0rc31__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 (78) 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 +3 -3
  5. mlrun/common/formatters/artifact.py +1 -0
  6. mlrun/common/formatters/base.py +9 -9
  7. mlrun/common/schemas/alert.py +4 -8
  8. mlrun/common/schemas/api_gateway.py +7 -0
  9. mlrun/common/schemas/constants.py +3 -0
  10. mlrun/common/schemas/model_monitoring/__init__.py +1 -0
  11. mlrun/common/schemas/model_monitoring/constants.py +32 -13
  12. mlrun/common/schemas/model_monitoring/model_endpoints.py +0 -12
  13. mlrun/common/schemas/project.py +10 -9
  14. mlrun/common/schemas/schedule.py +1 -1
  15. mlrun/config.py +37 -11
  16. mlrun/data_types/spark.py +2 -2
  17. mlrun/data_types/to_pandas.py +48 -16
  18. mlrun/datastore/__init__.py +1 -0
  19. mlrun/datastore/azure_blob.py +2 -1
  20. mlrun/datastore/base.py +21 -13
  21. mlrun/datastore/datastore.py +7 -5
  22. mlrun/datastore/datastore_profile.py +1 -1
  23. mlrun/datastore/google_cloud_storage.py +1 -0
  24. mlrun/datastore/inmem.py +4 -1
  25. mlrun/datastore/s3.py +2 -0
  26. mlrun/datastore/snowflake_utils.py +3 -1
  27. mlrun/datastore/sources.py +40 -11
  28. mlrun/datastore/store_resources.py +2 -0
  29. mlrun/datastore/targets.py +71 -26
  30. mlrun/db/base.py +11 -0
  31. mlrun/db/httpdb.py +50 -31
  32. mlrun/db/nopdb.py +11 -1
  33. mlrun/errors.py +4 -0
  34. mlrun/execution.py +18 -10
  35. mlrun/feature_store/retrieval/spark_merger.py +4 -32
  36. mlrun/launcher/local.py +2 -2
  37. mlrun/model.py +27 -1
  38. mlrun/model_monitoring/api.py +9 -55
  39. mlrun/model_monitoring/applications/histogram_data_drift.py +4 -1
  40. mlrun/model_monitoring/controller.py +57 -73
  41. mlrun/model_monitoring/db/stores/__init__.py +21 -9
  42. mlrun/model_monitoring/db/stores/base/store.py +39 -1
  43. mlrun/model_monitoring/db/stores/sqldb/models/base.py +9 -7
  44. mlrun/model_monitoring/db/stores/sqldb/models/mysql.py +4 -2
  45. mlrun/model_monitoring/db/stores/sqldb/sql_store.py +41 -80
  46. mlrun/model_monitoring/db/stores/v3io_kv/kv_store.py +22 -27
  47. mlrun/model_monitoring/db/tsdb/__init__.py +19 -14
  48. mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +4 -2
  49. mlrun/model_monitoring/helpers.py +15 -17
  50. mlrun/model_monitoring/writer.py +2 -7
  51. mlrun/projects/operations.py +1 -0
  52. mlrun/projects/project.py +87 -75
  53. mlrun/render.py +10 -5
  54. mlrun/run.py +7 -7
  55. mlrun/runtimes/base.py +1 -1
  56. mlrun/runtimes/daskjob.py +7 -1
  57. mlrun/runtimes/local.py +24 -7
  58. mlrun/runtimes/nuclio/function.py +20 -0
  59. mlrun/runtimes/pod.py +5 -29
  60. mlrun/serving/routers.py +75 -59
  61. mlrun/serving/server.py +1 -0
  62. mlrun/serving/v2_serving.py +8 -1
  63. mlrun/utils/helpers.py +46 -2
  64. mlrun/utils/logger.py +36 -2
  65. mlrun/utils/notifications/notification/base.py +4 -0
  66. mlrun/utils/notifications/notification/git.py +21 -0
  67. mlrun/utils/notifications/notification/slack.py +8 -0
  68. mlrun/utils/notifications/notification/webhook.py +41 -1
  69. mlrun/utils/notifications/notification_pusher.py +2 -2
  70. mlrun/utils/version/version.json +2 -2
  71. {mlrun-1.7.0rc26.dist-info → mlrun-1.7.0rc31.dist-info}/METADATA +13 -8
  72. {mlrun-1.7.0rc26.dist-info → mlrun-1.7.0rc31.dist-info}/RECORD +76 -78
  73. {mlrun-1.7.0rc26.dist-info → mlrun-1.7.0rc31.dist-info}/WHEEL +1 -1
  74. mlrun/feature_store/retrieval/conversion.py +0 -271
  75. mlrun/model_monitoring/controller_handler.py +0 -37
  76. {mlrun-1.7.0rc26.dist-info → mlrun-1.7.0rc31.dist-info}/LICENSE +0 -0
  77. {mlrun-1.7.0rc26.dist-info → mlrun-1.7.0rc31.dist-info}/entry_points.txt +0 -0
  78. {mlrun-1.7.0rc26.dist-info → mlrun-1.7.0rc31.dist-info}/top_level.txt +0 -0
@@ -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,28 @@ 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(
301
- project=self.project
294
+ query = (
295
+ session.query(self.model_endpoints_table)
296
+ .options(
297
+ # Exclude these fields when listing model endpoints to avoid returning too much data (ML-6594)
298
+ sqlalchemy.orm.defer(mm_schemas.EventFieldType.FEATURE_STATS),
299
+ sqlalchemy.orm.defer(mm_schemas.EventFieldType.CURRENT_STATS),
300
+ )
301
+ .filter_by(project=self.project)
302
302
  )
303
303
 
304
304
  # Apply filters
305
305
  if model:
306
+ model = model if ":" in model else f"{model}:latest"
306
307
  query = self._filter_values(
307
308
  query=query,
308
309
  model_endpoints_table=model_endpoints_table,
@@ -310,11 +311,12 @@ class SQLStoreBase(StoreBase):
310
311
  filtered_values=[model],
311
312
  )
312
313
  if function:
314
+ function_uri = f"{self.project}/{function}"
313
315
  query = self._filter_values(
314
316
  query=query,
315
317
  model_endpoints_table=model_endpoints_table,
316
- key_filter=mm_schemas.EventFieldType.FUNCTION,
317
- filtered_values=[function],
318
+ key_filter=mm_schemas.EventFieldType.FUNCTION_URI,
319
+ filtered_values=[function_uri],
318
320
  )
319
321
  if uids:
320
322
  query = self._filter_values(
@@ -364,11 +366,9 @@ class SQLStoreBase(StoreBase):
364
366
  """
365
367
 
366
368
  if kind == mm_schemas.WriterEventKind.METRIC:
367
- self._init_application_metrics_table()
368
369
  table = self.application_metrics_table
369
370
  table_name = mm_schemas.FileTargetKind.APP_METRICS
370
371
  elif kind == mm_schemas.WriterEventKind.RESULT:
371
- self._init_application_results_table()
372
372
  table = self.application_results_table
373
373
  table_name = mm_schemas.FileTargetKind.APP_RESULTS
374
374
  else:
@@ -442,7 +442,6 @@ class SQLStoreBase(StoreBase):
442
442
  :return: Timestamp as a Unix time.
443
443
  :raise: MLRunNotFoundError if last analyzed value is not found.
444
444
  """
445
- self._init_monitoring_schedules_table()
446
445
  monitoring_schedule_record = self._get(
447
446
  table=self.MonitoringSchedulesTable,
448
447
  criteria=self._get_filter_criteria(
@@ -469,8 +468,6 @@ class SQLStoreBase(StoreBase):
469
468
  :param last_analyzed: Timestamp as a Unix time that represents the last analyzed time of a certain
470
469
  application and model endpoint.
471
470
  """
472
- self._init_monitoring_schedules_table()
473
-
474
471
  criteria = self._get_filter_criteria(
475
472
  table=self.MonitoringSchedulesTable,
476
473
  endpoint_id=endpoint_id,
@@ -500,7 +497,6 @@ class SQLStoreBase(StoreBase):
500
497
  def _delete_last_analyzed(
501
498
  self, endpoint_id: str, application_name: typing.Optional[str] = None
502
499
  ) -> None:
503
- self._init_monitoring_schedules_table()
504
500
  criteria = self._get_filter_criteria(
505
501
  table=self.MonitoringSchedulesTable,
506
502
  endpoint_id=endpoint_id,
@@ -512,7 +508,6 @@ class SQLStoreBase(StoreBase):
512
508
  def _delete_application_result(
513
509
  self, endpoint_id: str, application_name: typing.Optional[str] = None
514
510
  ) -> None:
515
- self._init_application_results_table()
516
511
  criteria = self._get_filter_criteria(
517
512
  table=self.application_results_table,
518
513
  endpoint_id=endpoint_id,
@@ -524,7 +519,6 @@ class SQLStoreBase(StoreBase):
524
519
  def _delete_application_metrics(
525
520
  self, endpoint_id: str, application_name: typing.Optional[str] = None
526
521
  ) -> None:
527
- self._init_application_metrics_table()
528
522
  criteria = self._get_filter_criteria(
529
523
  table=self.application_metrics_table,
530
524
  endpoint_id=endpoint_id,
@@ -538,8 +532,12 @@ class SQLStoreBase(StoreBase):
538
532
 
539
533
  for table in self._tables:
540
534
  # Create table if not exist. The `metadata` contains the `ModelEndpointsTable`
535
+ db_name = make_url(self._sql_connection_string).database
541
536
  if not self._engine.has_table(table):
537
+ logger.info(f"Creating table {table} on {db_name} db.")
542
538
  self._tables[table].metadata.create_all(bind=self._engine)
539
+ else:
540
+ logger.info(f"Table {table} already exists on {db_name} db.")
543
541
 
544
542
  @staticmethod
545
543
  def _filter_values(
@@ -581,41 +579,6 @@ class SQLStoreBase(StoreBase):
581
579
  # Apply AND operator on the SQL query object with the filters tuple
582
580
  return query.filter(sqlalchemy.and_(*filter_query))
583
581
 
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
582
  def delete_model_endpoints_resources(self) -> None:
620
583
  """
621
584
  Delete all the model monitoring resources of the project in the SQL tables.
@@ -650,11 +613,9 @@ class SQLStoreBase(StoreBase):
650
613
  type=type,
651
614
  )
652
615
  if type == mm_schemas.ModelEndpointMonitoringMetricType.METRIC:
653
- self._init_application_metrics_table()
654
616
  table = self.application_metrics_table
655
617
  name_col = mm_schemas.MetricData.METRIC_NAME
656
618
  else:
657
- self._init_application_results_table()
658
619
  table = self.application_results_table
659
620
  name_col = mm_schemas.ResultData.RESULT_NAME
660
621
 
@@ -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,19 @@ 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
+ # Exclude these fields when listing model endpoints to avoid returning too much data (ML-6594)
287
+ endpoint_dict.pop(mm_schemas.EventFieldType.FEATURE_STATS)
288
+ endpoint_dict.pop(mm_schemas.EventFieldType.CURRENT_STATS)
289
+
290
+ if labels and not self._validate_labels(
291
+ endpoint_dict=endpoint_dict, labels=labels
292
+ ):
293
+ continue
294
+
295
+ endpoint_list.append(endpoint_dict)
289
296
 
290
297
  return endpoint_list
291
298
 
@@ -509,20 +516,16 @@ class KVStoreBase(StoreBase):
509
516
  project: str,
510
517
  function: str = None,
511
518
  model: str = None,
512
- labels: list[str] = None,
513
519
  top_level: bool = False,
514
520
  ) -> str:
515
521
  """
516
522
  Convert the provided filters into a valid filter expression. The expected filter expression includes different
517
523
  conditions, divided by ' AND '.
518
524
 
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.
525
+ :param project: The name of the project.
526
+ :param model: The name of the model to filter by.
527
+ :param function: The name of the function to filter by.
528
+ :param top_level: If True will return only routers and endpoint that are NOT children of any router.
526
529
 
527
530
  :return: A valid filter expression as a string.
528
531
 
@@ -533,25 +536,17 @@ class KVStoreBase(StoreBase):
533
536
  raise mlrun.errors.MLRunInvalidArgumentError("project can't be empty")
534
537
 
535
538
  # Add project filter
536
- filter_expression = [f"project=='{project}'"]
539
+ filter_expression = [f"{mm_schemas.EventFieldType.PROJECT}=='{project}'"]
537
540
 
538
541
  # Add function and model filters
539
542
  if function:
540
- filter_expression.append(f"function=='{function}'")
543
+ function_uri = f"{project}/{function}" if function else None
544
+ filter_expression.append(
545
+ f"{mm_schemas.EventFieldType.FUNCTION_URI}=='{function_uri}'"
546
+ )
541
547
  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})")
548
+ model = model if ":" in model else f"{model}:latest"
549
+ filter_expression.append(f"{mm_schemas.EventFieldType.MODEL}=='{model}'")
555
550
 
556
551
  # Apply top_level filter (remove endpoints that considered a child of a router)
557
552
  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
  )