mlrun 1.6.4rc2__py3-none-any.whl → 1.7.0rc20__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mlrun might be problematic. Click here for more details.

Files changed (291) hide show
  1. mlrun/__init__.py +11 -1
  2. mlrun/__main__.py +26 -112
  3. mlrun/alerts/__init__.py +15 -0
  4. mlrun/alerts/alert.py +144 -0
  5. mlrun/api/schemas/__init__.py +5 -4
  6. mlrun/artifacts/__init__.py +8 -3
  7. mlrun/artifacts/base.py +46 -257
  8. mlrun/artifacts/dataset.py +11 -192
  9. mlrun/artifacts/manager.py +47 -48
  10. mlrun/artifacts/model.py +31 -159
  11. mlrun/artifacts/plots.py +23 -380
  12. mlrun/common/constants.py +69 -0
  13. mlrun/common/db/sql_session.py +2 -3
  14. mlrun/common/formatters/__init__.py +19 -0
  15. mlrun/common/formatters/artifact.py +21 -0
  16. mlrun/common/formatters/base.py +78 -0
  17. mlrun/common/formatters/function.py +41 -0
  18. mlrun/common/formatters/pipeline.py +53 -0
  19. mlrun/common/formatters/project.py +51 -0
  20. mlrun/common/helpers.py +1 -2
  21. mlrun/common/model_monitoring/helpers.py +9 -5
  22. mlrun/{runtimes → common/runtimes}/constants.py +37 -9
  23. mlrun/common/schemas/__init__.py +24 -4
  24. mlrun/common/schemas/alert.py +203 -0
  25. mlrun/common/schemas/api_gateway.py +148 -0
  26. mlrun/common/schemas/artifact.py +18 -8
  27. mlrun/common/schemas/auth.py +11 -5
  28. mlrun/common/schemas/background_task.py +1 -1
  29. mlrun/common/schemas/client_spec.py +4 -1
  30. mlrun/common/schemas/feature_store.py +16 -16
  31. mlrun/common/schemas/frontend_spec.py +8 -7
  32. mlrun/common/schemas/function.py +5 -1
  33. mlrun/common/schemas/hub.py +11 -18
  34. mlrun/common/schemas/memory_reports.py +2 -2
  35. mlrun/common/schemas/model_monitoring/__init__.py +18 -3
  36. mlrun/common/schemas/model_monitoring/constants.py +83 -26
  37. mlrun/common/schemas/model_monitoring/grafana.py +13 -9
  38. mlrun/common/schemas/model_monitoring/model_endpoints.py +99 -16
  39. mlrun/common/schemas/notification.py +4 -4
  40. mlrun/common/schemas/object.py +2 -2
  41. mlrun/{runtimes/mpijob/v1alpha1.py → common/schemas/pagination.py} +10 -13
  42. mlrun/common/schemas/pipeline.py +1 -10
  43. mlrun/common/schemas/project.py +24 -23
  44. mlrun/common/schemas/runtime_resource.py +8 -12
  45. mlrun/common/schemas/schedule.py +3 -3
  46. mlrun/common/schemas/tag.py +1 -2
  47. mlrun/common/schemas/workflow.py +2 -2
  48. mlrun/common/types.py +7 -1
  49. mlrun/config.py +54 -17
  50. mlrun/data_types/to_pandas.py +10 -12
  51. mlrun/datastore/__init__.py +5 -8
  52. mlrun/datastore/alibaba_oss.py +130 -0
  53. mlrun/datastore/azure_blob.py +17 -5
  54. mlrun/datastore/base.py +62 -39
  55. mlrun/datastore/datastore.py +28 -9
  56. mlrun/datastore/datastore_profile.py +146 -20
  57. mlrun/datastore/filestore.py +0 -1
  58. mlrun/datastore/google_cloud_storage.py +6 -2
  59. mlrun/datastore/hdfs.py +56 -0
  60. mlrun/datastore/inmem.py +2 -2
  61. mlrun/datastore/redis.py +6 -2
  62. mlrun/datastore/s3.py +9 -0
  63. mlrun/datastore/snowflake_utils.py +43 -0
  64. mlrun/datastore/sources.py +201 -96
  65. mlrun/datastore/spark_utils.py +1 -2
  66. mlrun/datastore/store_resources.py +7 -7
  67. mlrun/datastore/targets.py +358 -104
  68. mlrun/datastore/utils.py +72 -58
  69. mlrun/datastore/v3io.py +5 -1
  70. mlrun/db/base.py +185 -35
  71. mlrun/db/factory.py +1 -1
  72. mlrun/db/httpdb.py +614 -179
  73. mlrun/db/nopdb.py +210 -26
  74. mlrun/errors.py +12 -1
  75. mlrun/execution.py +41 -24
  76. mlrun/feature_store/__init__.py +0 -2
  77. mlrun/feature_store/api.py +40 -72
  78. mlrun/feature_store/common.py +1 -1
  79. mlrun/feature_store/feature_set.py +76 -55
  80. mlrun/feature_store/feature_vector.py +28 -30
  81. mlrun/feature_store/ingestion.py +7 -6
  82. mlrun/feature_store/retrieval/base.py +16 -11
  83. mlrun/feature_store/retrieval/conversion.py +11 -13
  84. mlrun/feature_store/retrieval/dask_merger.py +2 -0
  85. mlrun/feature_store/retrieval/job.py +9 -3
  86. mlrun/feature_store/retrieval/local_merger.py +2 -0
  87. mlrun/feature_store/retrieval/spark_merger.py +34 -24
  88. mlrun/feature_store/steps.py +37 -34
  89. mlrun/features.py +9 -20
  90. mlrun/frameworks/_common/artifacts_library.py +9 -9
  91. mlrun/frameworks/_common/mlrun_interface.py +5 -5
  92. mlrun/frameworks/_common/model_handler.py +48 -48
  93. mlrun/frameworks/_common/plan.py +2 -3
  94. mlrun/frameworks/_common/producer.py +3 -4
  95. mlrun/frameworks/_common/utils.py +5 -5
  96. mlrun/frameworks/_dl_common/loggers/logger.py +6 -7
  97. mlrun/frameworks/_dl_common/loggers/mlrun_logger.py +9 -9
  98. mlrun/frameworks/_dl_common/loggers/tensorboard_logger.py +23 -47
  99. mlrun/frameworks/_ml_common/artifacts_library.py +1 -2
  100. mlrun/frameworks/_ml_common/loggers/logger.py +3 -4
  101. mlrun/frameworks/_ml_common/loggers/mlrun_logger.py +4 -5
  102. mlrun/frameworks/_ml_common/model_handler.py +24 -24
  103. mlrun/frameworks/_ml_common/pkl_model_server.py +2 -2
  104. mlrun/frameworks/_ml_common/plan.py +1 -1
  105. mlrun/frameworks/_ml_common/plans/calibration_curve_plan.py +2 -3
  106. mlrun/frameworks/_ml_common/plans/confusion_matrix_plan.py +2 -3
  107. mlrun/frameworks/_ml_common/plans/dataset_plan.py +3 -3
  108. mlrun/frameworks/_ml_common/plans/feature_importance_plan.py +3 -3
  109. mlrun/frameworks/_ml_common/plans/roc_curve_plan.py +4 -4
  110. mlrun/frameworks/_ml_common/utils.py +4 -4
  111. mlrun/frameworks/auto_mlrun/auto_mlrun.py +9 -9
  112. mlrun/frameworks/huggingface/model_server.py +4 -4
  113. mlrun/frameworks/lgbm/__init__.py +33 -33
  114. mlrun/frameworks/lgbm/callbacks/callback.py +2 -4
  115. mlrun/frameworks/lgbm/callbacks/logging_callback.py +4 -5
  116. mlrun/frameworks/lgbm/callbacks/mlrun_logging_callback.py +4 -5
  117. mlrun/frameworks/lgbm/mlrun_interfaces/booster_mlrun_interface.py +1 -3
  118. mlrun/frameworks/lgbm/mlrun_interfaces/mlrun_interface.py +6 -6
  119. mlrun/frameworks/lgbm/model_handler.py +10 -10
  120. mlrun/frameworks/lgbm/model_server.py +6 -6
  121. mlrun/frameworks/lgbm/utils.py +5 -5
  122. mlrun/frameworks/onnx/dataset.py +8 -8
  123. mlrun/frameworks/onnx/mlrun_interface.py +3 -3
  124. mlrun/frameworks/onnx/model_handler.py +6 -6
  125. mlrun/frameworks/onnx/model_server.py +7 -7
  126. mlrun/frameworks/parallel_coordinates.py +4 -3
  127. mlrun/frameworks/pytorch/__init__.py +18 -18
  128. mlrun/frameworks/pytorch/callbacks/callback.py +4 -5
  129. mlrun/frameworks/pytorch/callbacks/logging_callback.py +17 -17
  130. mlrun/frameworks/pytorch/callbacks/mlrun_logging_callback.py +11 -11
  131. mlrun/frameworks/pytorch/callbacks/tensorboard_logging_callback.py +23 -29
  132. mlrun/frameworks/pytorch/callbacks_handler.py +38 -38
  133. mlrun/frameworks/pytorch/mlrun_interface.py +20 -20
  134. mlrun/frameworks/pytorch/model_handler.py +17 -17
  135. mlrun/frameworks/pytorch/model_server.py +7 -7
  136. mlrun/frameworks/sklearn/__init__.py +13 -13
  137. mlrun/frameworks/sklearn/estimator.py +4 -4
  138. mlrun/frameworks/sklearn/metrics_library.py +14 -14
  139. mlrun/frameworks/sklearn/mlrun_interface.py +3 -6
  140. mlrun/frameworks/sklearn/model_handler.py +2 -2
  141. mlrun/frameworks/tf_keras/__init__.py +10 -7
  142. mlrun/frameworks/tf_keras/callbacks/logging_callback.py +15 -15
  143. mlrun/frameworks/tf_keras/callbacks/mlrun_logging_callback.py +11 -11
  144. mlrun/frameworks/tf_keras/callbacks/tensorboard_logging_callback.py +19 -23
  145. mlrun/frameworks/tf_keras/mlrun_interface.py +9 -11
  146. mlrun/frameworks/tf_keras/model_handler.py +14 -14
  147. mlrun/frameworks/tf_keras/model_server.py +6 -6
  148. mlrun/frameworks/xgboost/__init__.py +13 -13
  149. mlrun/frameworks/xgboost/model_handler.py +6 -6
  150. mlrun/k8s_utils.py +14 -16
  151. mlrun/launcher/__init__.py +1 -1
  152. mlrun/launcher/base.py +16 -15
  153. mlrun/launcher/client.py +8 -6
  154. mlrun/launcher/factory.py +1 -1
  155. mlrun/launcher/local.py +17 -11
  156. mlrun/launcher/remote.py +16 -10
  157. mlrun/lists.py +7 -6
  158. mlrun/model.py +238 -73
  159. mlrun/model_monitoring/__init__.py +1 -1
  160. mlrun/model_monitoring/api.py +138 -315
  161. mlrun/model_monitoring/application.py +5 -296
  162. mlrun/model_monitoring/applications/__init__.py +24 -0
  163. mlrun/model_monitoring/applications/_application_steps.py +157 -0
  164. mlrun/model_monitoring/applications/base.py +282 -0
  165. mlrun/model_monitoring/applications/context.py +214 -0
  166. mlrun/model_monitoring/applications/evidently_base.py +211 -0
  167. mlrun/model_monitoring/applications/histogram_data_drift.py +349 -0
  168. mlrun/model_monitoring/applications/results.py +99 -0
  169. mlrun/model_monitoring/controller.py +104 -84
  170. mlrun/model_monitoring/controller_handler.py +13 -5
  171. mlrun/model_monitoring/db/__init__.py +18 -0
  172. mlrun/model_monitoring/{stores → db/stores}/__init__.py +43 -36
  173. mlrun/model_monitoring/db/stores/base/__init__.py +15 -0
  174. mlrun/model_monitoring/{stores/model_endpoint_store.py → db/stores/base/store.py} +64 -40
  175. mlrun/model_monitoring/db/stores/sqldb/__init__.py +13 -0
  176. mlrun/model_monitoring/db/stores/sqldb/models/__init__.py +71 -0
  177. mlrun/model_monitoring/{stores → db/stores/sqldb}/models/base.py +109 -5
  178. mlrun/model_monitoring/db/stores/sqldb/models/mysql.py +88 -0
  179. mlrun/model_monitoring/{stores/models/mysql.py → db/stores/sqldb/models/sqlite.py} +19 -13
  180. mlrun/model_monitoring/db/stores/sqldb/sql_store.py +684 -0
  181. mlrun/model_monitoring/db/stores/v3io_kv/__init__.py +13 -0
  182. mlrun/model_monitoring/{stores/kv_model_endpoint_store.py → db/stores/v3io_kv/kv_store.py} +310 -165
  183. mlrun/model_monitoring/db/tsdb/__init__.py +100 -0
  184. mlrun/model_monitoring/db/tsdb/base.py +329 -0
  185. mlrun/model_monitoring/db/tsdb/helpers.py +30 -0
  186. mlrun/model_monitoring/db/tsdb/tdengine/__init__.py +15 -0
  187. mlrun/model_monitoring/db/tsdb/tdengine/schemas.py +240 -0
  188. mlrun/model_monitoring/db/tsdb/tdengine/stream_graph_steps.py +45 -0
  189. mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py +397 -0
  190. mlrun/model_monitoring/db/tsdb/v3io/__init__.py +15 -0
  191. mlrun/model_monitoring/db/tsdb/v3io/stream_graph_steps.py +117 -0
  192. mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +630 -0
  193. mlrun/model_monitoring/evidently_application.py +6 -118
  194. mlrun/model_monitoring/features_drift_table.py +134 -106
  195. mlrun/model_monitoring/helpers.py +127 -28
  196. mlrun/model_monitoring/metrics/__init__.py +13 -0
  197. mlrun/model_monitoring/metrics/histogram_distance.py +127 -0
  198. mlrun/model_monitoring/model_endpoint.py +3 -2
  199. mlrun/model_monitoring/prometheus.py +1 -4
  200. mlrun/model_monitoring/stream_processing.py +62 -231
  201. mlrun/model_monitoring/tracking_policy.py +9 -2
  202. mlrun/model_monitoring/writer.py +152 -124
  203. mlrun/package/__init__.py +6 -6
  204. mlrun/package/context_handler.py +5 -5
  205. mlrun/package/packager.py +7 -7
  206. mlrun/package/packagers/default_packager.py +6 -6
  207. mlrun/package/packagers/numpy_packagers.py +15 -15
  208. mlrun/package/packagers/pandas_packagers.py +5 -5
  209. mlrun/package/packagers/python_standard_library_packagers.py +10 -10
  210. mlrun/package/packagers_manager.py +19 -23
  211. mlrun/package/utils/_formatter.py +6 -6
  212. mlrun/package/utils/_pickler.py +2 -2
  213. mlrun/package/utils/_supported_format.py +4 -4
  214. mlrun/package/utils/log_hint_utils.py +2 -2
  215. mlrun/package/utils/type_hint_utils.py +4 -9
  216. mlrun/platforms/__init__.py +11 -10
  217. mlrun/platforms/iguazio.py +24 -203
  218. mlrun/projects/operations.py +35 -21
  219. mlrun/projects/pipelines.py +68 -99
  220. mlrun/projects/project.py +830 -266
  221. mlrun/render.py +3 -11
  222. mlrun/run.py +162 -166
  223. mlrun/runtimes/__init__.py +62 -7
  224. mlrun/runtimes/base.py +39 -32
  225. mlrun/runtimes/daskjob.py +8 -8
  226. mlrun/runtimes/databricks_job/databricks_cancel_task.py +1 -1
  227. mlrun/runtimes/databricks_job/databricks_runtime.py +7 -7
  228. mlrun/runtimes/databricks_job/databricks_wrapper.py +1 -1
  229. mlrun/runtimes/funcdoc.py +0 -28
  230. mlrun/runtimes/function_reference.py +1 -1
  231. mlrun/runtimes/kubejob.py +28 -122
  232. mlrun/runtimes/local.py +6 -3
  233. mlrun/runtimes/mpijob/__init__.py +0 -20
  234. mlrun/runtimes/mpijob/abstract.py +9 -10
  235. mlrun/runtimes/mpijob/v1.py +1 -1
  236. mlrun/{model_monitoring/stores/models/sqlite.py → runtimes/nuclio/__init__.py} +7 -9
  237. mlrun/runtimes/nuclio/api_gateway.py +709 -0
  238. mlrun/runtimes/nuclio/application/__init__.py +15 -0
  239. mlrun/runtimes/nuclio/application/application.py +523 -0
  240. mlrun/runtimes/nuclio/application/reverse_proxy.go +95 -0
  241. mlrun/runtimes/{function.py → nuclio/function.py} +112 -73
  242. mlrun/runtimes/{nuclio.py → nuclio/nuclio.py} +6 -6
  243. mlrun/runtimes/{serving.py → nuclio/serving.py} +45 -51
  244. mlrun/runtimes/pod.py +286 -88
  245. mlrun/runtimes/remotesparkjob.py +2 -2
  246. mlrun/runtimes/sparkjob/spark3job.py +51 -34
  247. mlrun/runtimes/utils.py +7 -75
  248. mlrun/secrets.py +9 -5
  249. mlrun/serving/remote.py +2 -7
  250. mlrun/serving/routers.py +13 -10
  251. mlrun/serving/server.py +22 -26
  252. mlrun/serving/states.py +99 -25
  253. mlrun/serving/utils.py +3 -3
  254. mlrun/serving/v1_serving.py +6 -7
  255. mlrun/serving/v2_serving.py +59 -20
  256. mlrun/track/tracker.py +2 -1
  257. mlrun/track/tracker_manager.py +3 -3
  258. mlrun/track/trackers/mlflow_tracker.py +1 -2
  259. mlrun/utils/async_http.py +5 -7
  260. mlrun/utils/azure_vault.py +1 -1
  261. mlrun/utils/clones.py +1 -2
  262. mlrun/utils/condition_evaluator.py +3 -3
  263. mlrun/utils/db.py +3 -3
  264. mlrun/utils/helpers.py +183 -197
  265. mlrun/utils/http.py +2 -5
  266. mlrun/utils/logger.py +76 -14
  267. mlrun/utils/notifications/notification/__init__.py +17 -12
  268. mlrun/utils/notifications/notification/base.py +14 -2
  269. mlrun/utils/notifications/notification/console.py +2 -0
  270. mlrun/utils/notifications/notification/git.py +3 -1
  271. mlrun/utils/notifications/notification/ipython.py +3 -1
  272. mlrun/utils/notifications/notification/slack.py +101 -21
  273. mlrun/utils/notifications/notification/webhook.py +11 -1
  274. mlrun/utils/notifications/notification_pusher.py +155 -30
  275. mlrun/utils/retryer.py +208 -0
  276. mlrun/utils/singleton.py +1 -1
  277. mlrun/utils/v3io_clients.py +2 -4
  278. mlrun/utils/version/version.json +2 -2
  279. mlrun/utils/version/version.py +2 -6
  280. {mlrun-1.6.4rc2.dist-info → mlrun-1.7.0rc20.dist-info}/METADATA +31 -19
  281. mlrun-1.7.0rc20.dist-info/RECORD +353 -0
  282. mlrun/kfpops.py +0 -868
  283. mlrun/model_monitoring/batch.py +0 -1095
  284. mlrun/model_monitoring/stores/models/__init__.py +0 -27
  285. mlrun/model_monitoring/stores/sql_model_endpoint_store.py +0 -384
  286. mlrun/platforms/other.py +0 -306
  287. mlrun-1.6.4rc2.dist-info/RECORD +0 -314
  288. {mlrun-1.6.4rc2.dist-info → mlrun-1.7.0rc20.dist-info}/LICENSE +0 -0
  289. {mlrun-1.6.4rc2.dist-info → mlrun-1.7.0rc20.dist-info}/WHEEL +0 -0
  290. {mlrun-1.6.4rc2.dist-info → mlrun-1.7.0rc20.dist-info}/entry_points.txt +0 -0
  291. {mlrun-1.6.4rc2.dist-info → mlrun-1.7.0rc20.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,684 @@
1
+ # Copyright 2024 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
+ import datetime
16
+ import json
17
+ import typing
18
+ import uuid
19
+
20
+ import pandas as pd
21
+ import sqlalchemy
22
+ import sqlalchemy.exc
23
+ import sqlalchemy.orm
24
+ from sqlalchemy.sql.elements import BinaryExpression
25
+
26
+ import mlrun.common.model_monitoring.helpers
27
+ import mlrun.common.schemas.model_monitoring as mm_schemas
28
+ import mlrun.model_monitoring.db
29
+ import mlrun.model_monitoring.db.stores.sqldb.models
30
+ import mlrun.model_monitoring.helpers
31
+ from mlrun.common.db.sql_session import create_session, get_engine
32
+ from mlrun.utils import datetime_now, logger
33
+
34
+
35
+ class SQLStoreBase(mlrun.model_monitoring.db.StoreBase):
36
+ """
37
+ Handles the DB operations when the DB target is from type SQL. For the SQL operations, we use SQLAlchemy, a Python
38
+ SQL toolkit that handles the communication with the database. When using SQL for storing the model monitoring
39
+ data, the user needs to provide a valid connection string for the database.
40
+ """
41
+
42
+ _tables = {}
43
+
44
+ def __init__(
45
+ self,
46
+ project: str,
47
+ secret_provider: typing.Callable = None,
48
+ ):
49
+ """
50
+ Initialize SQL store target object.
51
+
52
+ :param project: The name of the project.
53
+ :param secret_provider: An optional secret provider to get the connection string secret.
54
+ """
55
+
56
+ super().__init__(project=project)
57
+
58
+ self._sql_connection_string = (
59
+ mlrun.model_monitoring.helpers.get_connection_string(
60
+ secret_provider=secret_provider
61
+ )
62
+ )
63
+
64
+ self._engine = get_engine(dsn=self._sql_connection_string)
65
+
66
+ def _init_tables(self):
67
+ self._init_model_endpoints_table()
68
+ self._init_application_results_table()
69
+ self._init_application_metrics_table()
70
+ self._init_monitoring_schedules_table()
71
+
72
+ def _init_model_endpoints_table(self):
73
+ self.ModelEndpointsTable = (
74
+ mlrun.model_monitoring.db.stores.sqldb.models._get_model_endpoints_table(
75
+ connection_string=self._sql_connection_string
76
+ )
77
+ )
78
+ self._tables[mm_schemas.EventFieldType.MODEL_ENDPOINTS] = (
79
+ self.ModelEndpointsTable
80
+ )
81
+
82
+ def _init_application_results_table(self):
83
+ self.application_results_table = (
84
+ mlrun.model_monitoring.db.stores.sqldb.models._get_application_result_table(
85
+ connection_string=self._sql_connection_string
86
+ )
87
+ )
88
+ self._tables[mm_schemas.FileTargetKind.APP_RESULTS] = (
89
+ self.application_results_table
90
+ )
91
+
92
+ def _init_application_metrics_table(self) -> None:
93
+ self.application_metrics_table = mlrun.model_monitoring.db.stores.sqldb.models._get_application_metrics_table(
94
+ connection_string=self._sql_connection_string
95
+ )
96
+ self._tables[mm_schemas.FileTargetKind.APP_METRICS] = (
97
+ self.application_metrics_table
98
+ )
99
+
100
+ def _init_monitoring_schedules_table(self):
101
+ self.MonitoringSchedulesTable = mlrun.model_monitoring.db.stores.sqldb.models._get_monitoring_schedules_table(
102
+ connection_string=self._sql_connection_string
103
+ )
104
+ self._tables[mm_schemas.FileTargetKind.MONITORING_SCHEDULES] = (
105
+ self.MonitoringSchedulesTable
106
+ )
107
+
108
+ def _write(self, table_name: str, event: dict[str, typing.Any]) -> None:
109
+ """
110
+ Create a new record in the SQL table.
111
+
112
+ :param table_name: Target table name.
113
+ :param event: Event dictionary that will be written into the DB.
114
+ """
115
+ with self._engine.connect() as connection:
116
+ # Convert the result into a pandas Dataframe and write it into the database
117
+ event_df = pd.DataFrame([event])
118
+ event_df.to_sql(table_name, con=connection, index=False, if_exists="append")
119
+
120
+ def _update(
121
+ self,
122
+ attributes: dict[str, typing.Any],
123
+ table: sqlalchemy.orm.decl_api.DeclarativeMeta,
124
+ criteria: list[BinaryExpression],
125
+ ) -> None:
126
+ """
127
+ Update a record in the SQL table.
128
+
129
+ :param attributes: Dictionary of attributes that will be used for update the record. Note that the keys
130
+ of the attributes dictionary should exist in the SQL table.
131
+ :param table: SQLAlchemy declarative table.
132
+ :param criteria: A list of binary expressions that filter the query.
133
+ """
134
+ with create_session(dsn=self._sql_connection_string) as session:
135
+ # Generate and commit the update session query
136
+ session.query(
137
+ table # pyright: ignore[reportOptionalCall]
138
+ ).filter(*criteria).update(attributes, synchronize_session=False)
139
+ session.commit()
140
+
141
+ def _get(
142
+ self,
143
+ table: sqlalchemy.orm.decl_api.DeclarativeMeta,
144
+ criteria: list[BinaryExpression],
145
+ ):
146
+ """
147
+ Get a record from the SQL table.
148
+
149
+ param table: SQLAlchemy declarative table.
150
+ :param criteria: A list of binary expressions that filter the query.
151
+ """
152
+ 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
169
+
170
+ def _delete(
171
+ self,
172
+ table: sqlalchemy.orm.decl_api.DeclarativeMeta,
173
+ criteria: list[BinaryExpression],
174
+ ) -> None:
175
+ """
176
+ Delete records from the SQL table.
177
+
178
+ param table: SQLAlchemy declarative table.
179
+ :param criteria: A list of binary expressions that filter the query.
180
+ """
181
+ with create_session(dsn=self._sql_connection_string) as session:
182
+ # Generate and commit the delete query
183
+ session.query(
184
+ table # pyright: ignore[reportOptionalCall]
185
+ ).filter(*criteria).delete(synchronize_session=False)
186
+ session.commit()
187
+
188
+ def write_model_endpoint(self, endpoint: dict[str, typing.Any]):
189
+ """
190
+ Create a new endpoint record in the SQL table. This method also creates the model endpoints table within the
191
+ SQL database if not exist.
192
+
193
+ :param endpoint: model endpoint dictionary that will be written into the DB.
194
+ """
195
+
196
+ # Adjust timestamps fields
197
+ endpoint[mm_schemas.EventFieldType.FIRST_REQUEST] = (endpoint)[
198
+ mm_schemas.EventFieldType.LAST_REQUEST
199
+ ] = datetime_now()
200
+
201
+ self._write(
202
+ table_name=mm_schemas.EventFieldType.MODEL_ENDPOINTS, event=endpoint
203
+ )
204
+
205
+ def update_model_endpoint(
206
+ self, endpoint_id: str, attributes: dict[str, typing.Any]
207
+ ):
208
+ """
209
+ Update a model endpoint record with a given attributes.
210
+
211
+ :param endpoint_id: The unique id of the model endpoint.
212
+ :param attributes: Dictionary of attributes that will be used for update the model endpoint. Note that the keys
213
+ of the attributes dictionary should exist in the SQL table.
214
+
215
+ """
216
+ self._init_model_endpoints_table()
217
+
218
+ attributes.pop(mm_schemas.EventFieldType.ENDPOINT_ID, None)
219
+
220
+ self._update(
221
+ attributes=attributes,
222
+ table=self.ModelEndpointsTable,
223
+ criteria=[self.ModelEndpointsTable.uid == endpoint_id],
224
+ )
225
+
226
+ def delete_model_endpoint(self, endpoint_id: str) -> None:
227
+ """
228
+ Deletes the SQL record of a given model endpoint id.
229
+
230
+ :param endpoint_id: The unique id of the model endpoint.
231
+ """
232
+ self._init_model_endpoints_table()
233
+ # Delete the model endpoint record using sqlalchemy ORM
234
+ self._delete(
235
+ table=self.ModelEndpointsTable,
236
+ criteria=[self.ModelEndpointsTable.uid == endpoint_id],
237
+ )
238
+
239
+ def get_model_endpoint(
240
+ self,
241
+ endpoint_id: str,
242
+ ) -> dict[str, typing.Any]:
243
+ """
244
+ Get a single model endpoint record.
245
+
246
+ :param endpoint_id: The unique id of the model endpoint.
247
+
248
+ :return: A model endpoint record as a dictionary.
249
+
250
+ :raise MLRunNotFoundError: If the model endpoints table was not found or the model endpoint id was not found.
251
+ """
252
+ self._init_model_endpoints_table()
253
+
254
+ # Get the model endpoint record
255
+ endpoint_record = self._get(
256
+ table=self.ModelEndpointsTable,
257
+ criteria=[self.ModelEndpointsTable.uid == endpoint_id],
258
+ )
259
+
260
+ if not endpoint_record:
261
+ raise mlrun.errors.MLRunNotFoundError(f"Endpoint {endpoint_id} not found")
262
+
263
+ # Convert the database values and the table columns into a python dictionary
264
+ return endpoint_record.to_dict()
265
+
266
+ def list_model_endpoints(
267
+ self,
268
+ model: str = None,
269
+ function: str = None,
270
+ labels: list[str] = None,
271
+ top_level: bool = None,
272
+ uids: list = None,
273
+ ) -> list[dict[str, typing.Any]]:
274
+ """
275
+ Returns a list of model endpoint dictionaries, supports filtering by model, function, labels or top level.
276
+ By default, when no filters are applied, all available model endpoints for the given project will
277
+ be listed.
278
+
279
+ :param model: The name of the model to filter by.
280
+ :param function: The name of the function to filter by.
281
+ :param labels: A list of labels to filter by. Label filters work by either filtering a specific value
282
+ of a label (i.e. list("key=value")) or by looking for the existence of a given
283
+ key (i.e. "key").
284
+ :param top_level: If True will return only routers and endpoint that are NOT children of any router.
285
+ :param uids: List of model endpoint unique ids to include in the result.
286
+
287
+ :return: A list of model endpoint dictionaries.
288
+ """
289
+ self._init_model_endpoints_table()
290
+ # Generate an empty model endpoints that will be filled afterwards with model endpoint dictionaries
291
+ endpoint_list = []
292
+
293
+ model_endpoints_table = (
294
+ self.ModelEndpointsTable.__table__ # pyright: ignore[reportAttributeAccessIssue]
295
+ )
296
+
297
+ # Get the model endpoints records using sqlalchemy ORM
298
+ with create_session(dsn=self._sql_connection_string) as session:
299
+ # Generate the list query
300
+ query = session.query(self.ModelEndpointsTable).filter_by(
301
+ project=self.project
302
+ )
303
+
304
+ # Apply filters
305
+ if model:
306
+ query = self._filter_values(
307
+ query=query,
308
+ model_endpoints_table=model_endpoints_table,
309
+ key_filter=mm_schemas.EventFieldType.MODEL,
310
+ filtered_values=[model],
311
+ )
312
+ if function:
313
+ query = self._filter_values(
314
+ query=query,
315
+ model_endpoints_table=model_endpoints_table,
316
+ key_filter=mm_schemas.EventFieldType.FUNCTION,
317
+ filtered_values=[function],
318
+ )
319
+ if uids:
320
+ query = self._filter_values(
321
+ query=query,
322
+ model_endpoints_table=model_endpoints_table,
323
+ key_filter=mm_schemas.EventFieldType.UID,
324
+ filtered_values=uids,
325
+ combined=False,
326
+ )
327
+ if top_level:
328
+ node_ep = str(mm_schemas.EndpointType.NODE_EP.value)
329
+ router_ep = str(mm_schemas.EndpointType.ROUTER.value)
330
+ endpoint_types = [node_ep, router_ep]
331
+ query = self._filter_values(
332
+ query=query,
333
+ model_endpoints_table=model_endpoints_table,
334
+ key_filter=mm_schemas.EventFieldType.ENDPOINT_TYPE,
335
+ filtered_values=endpoint_types,
336
+ combined=False,
337
+ )
338
+ # Convert the results from the DB into a ModelEndpoint object and append it to the model endpoints list
339
+ for endpoint_record in query.all():
340
+ endpoint_dict = endpoint_record.to_dict()
341
+
342
+ # Filter labels
343
+ if labels and not self._validate_labels(
344
+ endpoint_dict=endpoint_dict, labels=labels
345
+ ):
346
+ continue
347
+
348
+ endpoint_list.append(endpoint_dict)
349
+
350
+ return endpoint_list
351
+
352
+ def write_application_event(
353
+ self,
354
+ event: dict[str, typing.Any],
355
+ kind: mm_schemas.WriterEventKind = mm_schemas.WriterEventKind.RESULT,
356
+ ) -> None:
357
+ """
358
+ Write a new application event in the target table.
359
+
360
+ :param event: An event dictionary that represents the application result or metric,
361
+ should be corresponded to the schema defined in the
362
+ :py:class:`~mm_constants.constants.WriterEvent` object.
363
+ :param kind: The type of the event, can be either "result" or "metric".
364
+ """
365
+
366
+ if kind == mm_schemas.WriterEventKind.METRIC:
367
+ self._init_application_metrics_table()
368
+ table = self.application_metrics_table
369
+ table_name = mm_schemas.FileTargetKind.APP_METRICS
370
+ elif kind == mm_schemas.WriterEventKind.RESULT:
371
+ self._init_application_results_table()
372
+ table = self.application_results_table
373
+ table_name = mm_schemas.FileTargetKind.APP_RESULTS
374
+ else:
375
+ raise ValueError(f"Invalid {kind = }")
376
+
377
+ application_result_uid = self._generate_application_result_uid(event, kind=kind)
378
+ criteria = [table.uid == application_result_uid]
379
+
380
+ application_record = self._get(table=table, criteria=criteria)
381
+ if application_record:
382
+ self._convert_to_datetime(
383
+ event=event, key=mm_schemas.WriterEvent.START_INFER_TIME
384
+ )
385
+ self._convert_to_datetime(
386
+ event=event, key=mm_schemas.WriterEvent.END_INFER_TIME
387
+ )
388
+ # Update an existing application result
389
+ self._update(attributes=event, table=table, criteria=criteria)
390
+ else:
391
+ # Write a new application result
392
+ event[mm_schemas.EventFieldType.UID] = application_result_uid
393
+ self._write(table_name=table_name, event=event)
394
+
395
+ @staticmethod
396
+ def _convert_to_datetime(event: dict[str, typing.Any], key: str) -> None:
397
+ if isinstance(event[key], str):
398
+ event[key] = datetime.datetime.fromisoformat(event[key])
399
+ event[key] = event[key].astimezone(tz=datetime.timezone.utc)
400
+
401
+ @staticmethod
402
+ def _generate_application_result_uid(
403
+ event: dict[str, typing.Any],
404
+ kind: mm_schemas.WriterEventKind = mm_schemas.WriterEventKind.RESULT,
405
+ ) -> str:
406
+ if kind == mm_schemas.WriterEventKind.RESULT:
407
+ name = event[mm_schemas.ResultData.RESULT_NAME]
408
+ else:
409
+ name = event[mm_schemas.MetricData.METRIC_NAME]
410
+ return "_".join(
411
+ [
412
+ event[mm_schemas.WriterEvent.ENDPOINT_ID],
413
+ event[mm_schemas.WriterEvent.APPLICATION_NAME],
414
+ name,
415
+ ]
416
+ )
417
+
418
+ @staticmethod
419
+ def _get_filter_criteria(
420
+ *,
421
+ table: sqlalchemy.orm.decl_api.DeclarativeMeta,
422
+ endpoint_id: str,
423
+ application_name: typing.Optional[str] = None,
424
+ ) -> list[BinaryExpression]:
425
+ """
426
+ Return the filter criteria for the given endpoint_id and application_name.
427
+ Note: the table object must include the relevant columns:
428
+ `endpoint_id` and `application_name`.
429
+ """
430
+ criteria = [table.endpoint_id == endpoint_id]
431
+ if application_name is not None:
432
+ criteria.append(table.application_name == application_name)
433
+ return criteria
434
+
435
+ def get_last_analyzed(self, endpoint_id: str, application_name: str) -> int:
436
+ """
437
+ Get the last analyzed time for the provided model endpoint and application.
438
+
439
+ :param endpoint_id: The unique id of the model endpoint.
440
+ :param application_name: Registered application name.
441
+
442
+ :return: Timestamp as a Unix time.
443
+ :raise: MLRunNotFoundError if last analyzed value is not found.
444
+ """
445
+ self._init_monitoring_schedules_table()
446
+ monitoring_schedule_record = self._get(
447
+ table=self.MonitoringSchedulesTable,
448
+ criteria=self._get_filter_criteria(
449
+ table=self.MonitoringSchedulesTable,
450
+ endpoint_id=endpoint_id,
451
+ application_name=application_name,
452
+ ),
453
+ )
454
+ if not monitoring_schedule_record:
455
+ raise mlrun.errors.MLRunNotFoundError(
456
+ f"No last analyzed value has been found for {application_name} "
457
+ f"that processes model endpoint {endpoint_id}"
458
+ )
459
+ return monitoring_schedule_record.last_analyzed
460
+
461
+ def update_last_analyzed(
462
+ self, endpoint_id: str, application_name: str, last_analyzed: int
463
+ ):
464
+ """
465
+ Update the last analyzed time for the provided model endpoint and application.
466
+
467
+ :param endpoint_id: The unique id of the model endpoint.
468
+ :param application_name: Registered application name.
469
+ :param last_analyzed: Timestamp as a Unix time that represents the last analyzed time of a certain
470
+ application and model endpoint.
471
+ """
472
+ self._init_monitoring_schedules_table()
473
+
474
+ criteria = self._get_filter_criteria(
475
+ table=self.MonitoringSchedulesTable,
476
+ endpoint_id=endpoint_id,
477
+ application_name=application_name,
478
+ )
479
+ monitoring_schedule_record = self._get(
480
+ table=self.MonitoringSchedulesTable, criteria=criteria
481
+ )
482
+ if not monitoring_schedule_record:
483
+ # Add a new record with last analyzed value
484
+ self._write(
485
+ table_name=mm_schemas.FileTargetKind.MONITORING_SCHEDULES,
486
+ event={
487
+ mm_schemas.SchedulingKeys.UID: uuid.uuid4().hex,
488
+ mm_schemas.SchedulingKeys.APPLICATION_NAME: application_name,
489
+ mm_schemas.SchedulingKeys.ENDPOINT_ID: endpoint_id,
490
+ mm_schemas.SchedulingKeys.LAST_ANALYZED: last_analyzed,
491
+ },
492
+ )
493
+
494
+ self._update(
495
+ attributes={mm_schemas.SchedulingKeys.LAST_ANALYZED: last_analyzed},
496
+ table=self.MonitoringSchedulesTable,
497
+ criteria=criteria,
498
+ )
499
+
500
+ def _delete_last_analyzed(
501
+ self, endpoint_id: str, application_name: typing.Optional[str] = None
502
+ ) -> None:
503
+ self._init_monitoring_schedules_table()
504
+ criteria = self._get_filter_criteria(
505
+ table=self.MonitoringSchedulesTable,
506
+ endpoint_id=endpoint_id,
507
+ application_name=application_name,
508
+ )
509
+ # Delete the model endpoint record using sqlalchemy ORM
510
+ self._delete(table=self.MonitoringSchedulesTable, criteria=criteria)
511
+
512
+ def _delete_application_result(
513
+ self, endpoint_id: str, application_name: typing.Optional[str] = None
514
+ ) -> None:
515
+ self._init_application_results_table()
516
+ criteria = self._get_filter_criteria(
517
+ table=self.application_results_table,
518
+ endpoint_id=endpoint_id,
519
+ application_name=application_name,
520
+ )
521
+ # Delete the relevant records from the results table
522
+ self._delete(table=self.application_results_table, criteria=criteria)
523
+
524
+ def _delete_application_metrics(
525
+ self, endpoint_id: str, application_name: typing.Optional[str] = None
526
+ ) -> None:
527
+ self._init_application_metrics_table()
528
+ criteria = self._get_filter_criteria(
529
+ table=self.application_metrics_table,
530
+ endpoint_id=endpoint_id,
531
+ application_name=application_name,
532
+ )
533
+ # Delete the relevant records from the metrics table
534
+ self._delete(table=self.application_metrics_table, criteria=criteria)
535
+
536
+ def _create_tables_if_not_exist(self):
537
+ self._init_tables()
538
+
539
+ for table in self._tables:
540
+ # Create table if not exist. The `metadata` contains the `ModelEndpointsTable`
541
+ if not self._engine.has_table(table):
542
+ self._tables[table].metadata.create_all(bind=self._engine)
543
+
544
+ @staticmethod
545
+ def _filter_values(
546
+ query: sqlalchemy.orm.query.Query,
547
+ model_endpoints_table: sqlalchemy.Table,
548
+ key_filter: str,
549
+ filtered_values: list,
550
+ combined=True,
551
+ ) -> sqlalchemy.orm.query.Query:
552
+ """Filtering the SQL query object according to the provided filters.
553
+
554
+ :param query: SQLAlchemy ORM query object. Includes the SELECT statements generated by the ORM
555
+ for getting the model endpoint data from the SQL table.
556
+ :param model_endpoints_table: SQLAlchemy table object that represents the model endpoints table.
557
+ :param key_filter: Key column to filter by.
558
+ :param filtered_values: List of values to filter the query the result.
559
+ :param combined: If true, then apply AND operator on the filtered values list. Otherwise, apply OR
560
+ operator.
561
+
562
+ return: SQLAlchemy ORM query object that represents the updated query with the provided
563
+ filters.
564
+ """
565
+
566
+ if combined and len(filtered_values) > 1:
567
+ raise mlrun.errors.MLRunInvalidArgumentError(
568
+ "Can't apply combined policy with multiple values"
569
+ )
570
+
571
+ if not combined:
572
+ return query.filter(
573
+ model_endpoints_table.c[key_filter].in_(filtered_values)
574
+ )
575
+
576
+ # Generating a tuple with the relevant filters
577
+ filter_query = []
578
+ for _filter in filtered_values:
579
+ filter_query.append(model_endpoints_table.c[key_filter] == _filter)
580
+
581
+ # Apply AND operator on the SQL query object with the filters tuple
582
+ return query.filter(sqlalchemy.and_(*filter_query))
583
+
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
+ def delete_model_endpoints_resources(self) -> None:
620
+ """
621
+ Delete all the model monitoring resources of the project in the SQL tables.
622
+ """
623
+ endpoints = self.list_model_endpoints()
624
+ logger.debug("Deleting model monitoring resources", project=self.project)
625
+
626
+ for endpoint_dict in endpoints:
627
+ endpoint_id = endpoint_dict[mm_schemas.EventFieldType.UID]
628
+
629
+ # Delete last analyzed records
630
+ self._delete_last_analyzed(endpoint_id=endpoint_id)
631
+
632
+ # Delete application results and metrics records
633
+ self._delete_application_result(endpoint_id=endpoint_id)
634
+ self._delete_application_metrics(endpoint_id=endpoint_id)
635
+
636
+ # Delete model endpoint record
637
+ self.delete_model_endpoint(endpoint_id=endpoint_id)
638
+
639
+ def get_model_endpoint_metrics(
640
+ self, endpoint_id: str, type: mm_schemas.ModelEndpointMonitoringMetricType
641
+ ) -> list[mm_schemas.ModelEndpointMonitoringMetric]:
642
+ """
643
+ Fetch the model endpoint metrics or results (according to `type`) for the
644
+ requested endpoint.
645
+ """
646
+ logger.debug(
647
+ "Fetching metrics for model endpoint",
648
+ project=self.project,
649
+ endpoint_id=endpoint_id,
650
+ type=type,
651
+ )
652
+ if type == mm_schemas.ModelEndpointMonitoringMetricType.METRIC:
653
+ self._init_application_metrics_table()
654
+ table = self.application_metrics_table
655
+ name_col = mm_schemas.MetricData.METRIC_NAME
656
+ else:
657
+ self._init_application_results_table()
658
+ table = self.application_results_table
659
+ name_col = mm_schemas.ResultData.RESULT_NAME
660
+
661
+ # Note: the block below does not use self._get, as we need here all the
662
+ # results, not only `one_or_none`.
663
+ with sqlalchemy.orm.Session(self._engine) as session:
664
+ metric_rows = (
665
+ session.query(table) # pyright: ignore[reportOptionalCall]
666
+ .filter(table.endpoint_id == endpoint_id)
667
+ .all()
668
+ )
669
+
670
+ return [
671
+ mm_schemas.ModelEndpointMonitoringMetric(
672
+ project=self.project,
673
+ app=metric_row.application_name,
674
+ type=type,
675
+ name=getattr(metric_row, name_col),
676
+ full_name=mlrun.model_monitoring.helpers._compose_full_name(
677
+ project=self.project,
678
+ app=metric_row.application_name,
679
+ type=type,
680
+ name=getattr(metric_row, name_col),
681
+ ),
682
+ )
683
+ for metric_row in metric_rows
684
+ ]
@@ -0,0 +1,13 @@
1
+ # Copyright 2024 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.