mlrun 1.7.0rc4__py3-none-any.whl → 1.7.2__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 (235) hide show
  1. mlrun/__init__.py +11 -1
  2. mlrun/__main__.py +39 -121
  3. mlrun/{datastore/helpers.py → alerts/__init__.py} +2 -5
  4. mlrun/alerts/alert.py +248 -0
  5. mlrun/api/schemas/__init__.py +4 -3
  6. mlrun/artifacts/__init__.py +8 -3
  7. mlrun/artifacts/base.py +39 -254
  8. mlrun/artifacts/dataset.py +9 -190
  9. mlrun/artifacts/manager.py +73 -46
  10. mlrun/artifacts/model.py +30 -158
  11. mlrun/artifacts/plots.py +23 -380
  12. mlrun/common/constants.py +73 -1
  13. mlrun/common/db/sql_session.py +3 -2
  14. mlrun/common/formatters/__init__.py +21 -0
  15. mlrun/common/formatters/artifact.py +46 -0
  16. mlrun/common/formatters/base.py +113 -0
  17. mlrun/common/formatters/feature_set.py +44 -0
  18. mlrun/common/formatters/function.py +46 -0
  19. mlrun/common/formatters/pipeline.py +53 -0
  20. mlrun/common/formatters/project.py +51 -0
  21. mlrun/common/formatters/run.py +29 -0
  22. mlrun/common/helpers.py +11 -1
  23. mlrun/{runtimes → common/runtimes}/constants.py +32 -4
  24. mlrun/common/schemas/__init__.py +31 -4
  25. mlrun/common/schemas/alert.py +202 -0
  26. mlrun/common/schemas/api_gateway.py +196 -0
  27. mlrun/common/schemas/artifact.py +28 -1
  28. mlrun/common/schemas/auth.py +13 -2
  29. mlrun/common/schemas/client_spec.py +2 -1
  30. mlrun/common/schemas/common.py +7 -4
  31. mlrun/common/schemas/constants.py +3 -0
  32. mlrun/common/schemas/feature_store.py +58 -28
  33. mlrun/common/schemas/frontend_spec.py +8 -0
  34. mlrun/common/schemas/function.py +11 -0
  35. mlrun/common/schemas/hub.py +7 -9
  36. mlrun/common/schemas/model_monitoring/__init__.py +21 -4
  37. mlrun/common/schemas/model_monitoring/constants.py +136 -42
  38. mlrun/common/schemas/model_monitoring/grafana.py +9 -5
  39. mlrun/common/schemas/model_monitoring/model_endpoints.py +89 -41
  40. mlrun/common/schemas/notification.py +69 -12
  41. mlrun/{runtimes/mpijob/v1alpha1.py → common/schemas/pagination.py} +10 -13
  42. mlrun/common/schemas/pipeline.py +7 -0
  43. mlrun/common/schemas/project.py +67 -16
  44. mlrun/common/schemas/runs.py +17 -0
  45. mlrun/common/schemas/schedule.py +1 -1
  46. mlrun/common/schemas/workflow.py +10 -2
  47. mlrun/common/types.py +14 -1
  48. mlrun/config.py +233 -58
  49. mlrun/data_types/data_types.py +11 -1
  50. mlrun/data_types/spark.py +5 -4
  51. mlrun/data_types/to_pandas.py +75 -34
  52. mlrun/datastore/__init__.py +8 -10
  53. mlrun/datastore/alibaba_oss.py +131 -0
  54. mlrun/datastore/azure_blob.py +131 -43
  55. mlrun/datastore/base.py +107 -47
  56. mlrun/datastore/datastore.py +17 -7
  57. mlrun/datastore/datastore_profile.py +91 -7
  58. mlrun/datastore/dbfs_store.py +3 -7
  59. mlrun/datastore/filestore.py +1 -3
  60. mlrun/datastore/google_cloud_storage.py +92 -32
  61. mlrun/datastore/hdfs.py +5 -0
  62. mlrun/datastore/inmem.py +6 -3
  63. mlrun/datastore/redis.py +3 -2
  64. mlrun/datastore/s3.py +30 -12
  65. mlrun/datastore/snowflake_utils.py +45 -0
  66. mlrun/datastore/sources.py +274 -59
  67. mlrun/datastore/spark_utils.py +30 -0
  68. mlrun/datastore/store_resources.py +9 -7
  69. mlrun/datastore/storeytargets.py +151 -0
  70. mlrun/datastore/targets.py +387 -119
  71. mlrun/datastore/utils.py +68 -5
  72. mlrun/datastore/v3io.py +28 -50
  73. mlrun/db/auth_utils.py +152 -0
  74. mlrun/db/base.py +245 -20
  75. mlrun/db/factory.py +1 -4
  76. mlrun/db/httpdb.py +909 -231
  77. mlrun/db/nopdb.py +279 -14
  78. mlrun/errors.py +35 -5
  79. mlrun/execution.py +111 -38
  80. mlrun/feature_store/__init__.py +0 -2
  81. mlrun/feature_store/api.py +46 -53
  82. mlrun/feature_store/common.py +6 -11
  83. mlrun/feature_store/feature_set.py +48 -23
  84. mlrun/feature_store/feature_vector.py +13 -2
  85. mlrun/feature_store/ingestion.py +7 -6
  86. mlrun/feature_store/retrieval/base.py +9 -4
  87. mlrun/feature_store/retrieval/dask_merger.py +2 -0
  88. mlrun/feature_store/retrieval/job.py +13 -4
  89. mlrun/feature_store/retrieval/local_merger.py +2 -0
  90. mlrun/feature_store/retrieval/spark_merger.py +24 -32
  91. mlrun/feature_store/steps.py +38 -19
  92. mlrun/features.py +6 -14
  93. mlrun/frameworks/_common/plan.py +3 -3
  94. mlrun/frameworks/_dl_common/loggers/tensorboard_logger.py +7 -12
  95. mlrun/frameworks/_ml_common/plan.py +1 -1
  96. mlrun/frameworks/auto_mlrun/auto_mlrun.py +2 -2
  97. mlrun/frameworks/lgbm/__init__.py +1 -1
  98. mlrun/frameworks/lgbm/callbacks/callback.py +2 -4
  99. mlrun/frameworks/lgbm/model_handler.py +1 -1
  100. mlrun/frameworks/parallel_coordinates.py +4 -4
  101. mlrun/frameworks/pytorch/__init__.py +2 -2
  102. mlrun/frameworks/sklearn/__init__.py +1 -1
  103. mlrun/frameworks/sklearn/mlrun_interface.py +13 -3
  104. mlrun/frameworks/tf_keras/__init__.py +5 -2
  105. mlrun/frameworks/tf_keras/callbacks/logging_callback.py +1 -1
  106. mlrun/frameworks/tf_keras/mlrun_interface.py +2 -2
  107. mlrun/frameworks/xgboost/__init__.py +1 -1
  108. mlrun/k8s_utils.py +57 -12
  109. mlrun/launcher/__init__.py +1 -1
  110. mlrun/launcher/base.py +6 -5
  111. mlrun/launcher/client.py +13 -11
  112. mlrun/launcher/factory.py +1 -1
  113. mlrun/launcher/local.py +15 -5
  114. mlrun/launcher/remote.py +10 -3
  115. mlrun/lists.py +6 -2
  116. mlrun/model.py +297 -48
  117. mlrun/model_monitoring/__init__.py +1 -1
  118. mlrun/model_monitoring/api.py +152 -357
  119. mlrun/model_monitoring/applications/__init__.py +10 -0
  120. mlrun/model_monitoring/applications/_application_steps.py +190 -0
  121. mlrun/model_monitoring/applications/base.py +108 -0
  122. mlrun/model_monitoring/applications/context.py +341 -0
  123. mlrun/model_monitoring/{evidently_application.py → applications/evidently_base.py} +27 -22
  124. mlrun/model_monitoring/applications/histogram_data_drift.py +227 -91
  125. mlrun/model_monitoring/applications/results.py +99 -0
  126. mlrun/model_monitoring/controller.py +130 -303
  127. mlrun/model_monitoring/{stores/models/sqlite.py → db/__init__.py} +5 -10
  128. mlrun/model_monitoring/db/stores/__init__.py +136 -0
  129. mlrun/model_monitoring/db/stores/base/__init__.py +15 -0
  130. mlrun/model_monitoring/db/stores/base/store.py +213 -0
  131. mlrun/model_monitoring/db/stores/sqldb/__init__.py +13 -0
  132. mlrun/model_monitoring/db/stores/sqldb/models/__init__.py +71 -0
  133. mlrun/model_monitoring/db/stores/sqldb/models/base.py +190 -0
  134. mlrun/model_monitoring/db/stores/sqldb/models/mysql.py +103 -0
  135. mlrun/model_monitoring/{stores/models/mysql.py → db/stores/sqldb/models/sqlite.py} +19 -13
  136. mlrun/model_monitoring/db/stores/sqldb/sql_store.py +659 -0
  137. mlrun/model_monitoring/db/stores/v3io_kv/__init__.py +13 -0
  138. mlrun/model_monitoring/db/stores/v3io_kv/kv_store.py +726 -0
  139. mlrun/model_monitoring/db/tsdb/__init__.py +105 -0
  140. mlrun/model_monitoring/db/tsdb/base.py +448 -0
  141. mlrun/model_monitoring/db/tsdb/helpers.py +30 -0
  142. mlrun/model_monitoring/db/tsdb/tdengine/__init__.py +15 -0
  143. mlrun/model_monitoring/db/tsdb/tdengine/schemas.py +298 -0
  144. mlrun/model_monitoring/db/tsdb/tdengine/stream_graph_steps.py +42 -0
  145. mlrun/model_monitoring/db/tsdb/tdengine/tdengine_connector.py +522 -0
  146. mlrun/model_monitoring/db/tsdb/v3io/__init__.py +15 -0
  147. mlrun/model_monitoring/db/tsdb/v3io/stream_graph_steps.py +158 -0
  148. mlrun/model_monitoring/db/tsdb/v3io/v3io_connector.py +849 -0
  149. mlrun/model_monitoring/features_drift_table.py +34 -22
  150. mlrun/model_monitoring/helpers.py +177 -39
  151. mlrun/model_monitoring/model_endpoint.py +3 -2
  152. mlrun/model_monitoring/stream_processing.py +165 -398
  153. mlrun/model_monitoring/tracking_policy.py +7 -1
  154. mlrun/model_monitoring/writer.py +161 -125
  155. mlrun/package/packagers/default_packager.py +2 -2
  156. mlrun/package/packagers_manager.py +1 -0
  157. mlrun/package/utils/_formatter.py +2 -2
  158. mlrun/platforms/__init__.py +11 -10
  159. mlrun/platforms/iguazio.py +67 -228
  160. mlrun/projects/__init__.py +6 -1
  161. mlrun/projects/operations.py +47 -20
  162. mlrun/projects/pipelines.py +396 -249
  163. mlrun/projects/project.py +1176 -406
  164. mlrun/render.py +28 -22
  165. mlrun/run.py +208 -181
  166. mlrun/runtimes/__init__.py +76 -11
  167. mlrun/runtimes/base.py +54 -24
  168. mlrun/runtimes/daskjob.py +9 -2
  169. mlrun/runtimes/databricks_job/databricks_runtime.py +1 -0
  170. mlrun/runtimes/databricks_job/databricks_wrapper.py +1 -1
  171. mlrun/runtimes/funcdoc.py +1 -29
  172. mlrun/runtimes/kubejob.py +34 -128
  173. mlrun/runtimes/local.py +39 -10
  174. mlrun/runtimes/mpijob/__init__.py +0 -20
  175. mlrun/runtimes/mpijob/abstract.py +8 -8
  176. mlrun/runtimes/mpijob/v1.py +1 -1
  177. mlrun/runtimes/nuclio/__init__.py +1 -0
  178. mlrun/runtimes/nuclio/api_gateway.py +769 -0
  179. mlrun/runtimes/nuclio/application/__init__.py +15 -0
  180. mlrun/runtimes/nuclio/application/application.py +758 -0
  181. mlrun/runtimes/nuclio/application/reverse_proxy.go +95 -0
  182. mlrun/runtimes/nuclio/function.py +188 -68
  183. mlrun/runtimes/nuclio/serving.py +57 -60
  184. mlrun/runtimes/pod.py +191 -58
  185. mlrun/runtimes/remotesparkjob.py +11 -8
  186. mlrun/runtimes/sparkjob/spark3job.py +17 -18
  187. mlrun/runtimes/utils.py +40 -73
  188. mlrun/secrets.py +6 -2
  189. mlrun/serving/__init__.py +8 -1
  190. mlrun/serving/remote.py +2 -3
  191. mlrun/serving/routers.py +89 -64
  192. mlrun/serving/server.py +54 -26
  193. mlrun/serving/states.py +187 -56
  194. mlrun/serving/utils.py +19 -11
  195. mlrun/serving/v2_serving.py +136 -63
  196. mlrun/track/tracker.py +2 -1
  197. mlrun/track/trackers/mlflow_tracker.py +5 -0
  198. mlrun/utils/async_http.py +26 -6
  199. mlrun/utils/db.py +18 -0
  200. mlrun/utils/helpers.py +375 -105
  201. mlrun/utils/http.py +2 -2
  202. mlrun/utils/logger.py +75 -9
  203. mlrun/utils/notifications/notification/__init__.py +14 -10
  204. mlrun/utils/notifications/notification/base.py +48 -0
  205. mlrun/utils/notifications/notification/console.py +2 -0
  206. mlrun/utils/notifications/notification/git.py +24 -1
  207. mlrun/utils/notifications/notification/ipython.py +2 -0
  208. mlrun/utils/notifications/notification/slack.py +96 -21
  209. mlrun/utils/notifications/notification/webhook.py +63 -2
  210. mlrun/utils/notifications/notification_pusher.py +146 -16
  211. mlrun/utils/regex.py +9 -0
  212. mlrun/utils/retryer.py +3 -2
  213. mlrun/utils/v3io_clients.py +2 -3
  214. mlrun/utils/version/version.json +2 -2
  215. mlrun-1.7.2.dist-info/METADATA +390 -0
  216. mlrun-1.7.2.dist-info/RECORD +351 -0
  217. {mlrun-1.7.0rc4.dist-info → mlrun-1.7.2.dist-info}/WHEEL +1 -1
  218. mlrun/feature_store/retrieval/conversion.py +0 -271
  219. mlrun/kfpops.py +0 -868
  220. mlrun/model_monitoring/application.py +0 -310
  221. mlrun/model_monitoring/batch.py +0 -974
  222. mlrun/model_monitoring/controller_handler.py +0 -37
  223. mlrun/model_monitoring/prometheus.py +0 -216
  224. mlrun/model_monitoring/stores/__init__.py +0 -111
  225. mlrun/model_monitoring/stores/kv_model_endpoint_store.py +0 -574
  226. mlrun/model_monitoring/stores/model_endpoint_store.py +0 -145
  227. mlrun/model_monitoring/stores/models/__init__.py +0 -27
  228. mlrun/model_monitoring/stores/models/base.py +0 -84
  229. mlrun/model_monitoring/stores/sql_model_endpoint_store.py +0 -382
  230. mlrun/platforms/other.py +0 -305
  231. mlrun-1.7.0rc4.dist-info/METADATA +0 -269
  232. mlrun-1.7.0rc4.dist-info/RECORD +0 -321
  233. {mlrun-1.7.0rc4.dist-info → mlrun-1.7.2.dist-info}/LICENSE +0 -0
  234. {mlrun-1.7.0rc4.dist-info → mlrun-1.7.2.dist-info}/entry_points.txt +0 -0
  235. {mlrun-1.7.0rc4.dist-info → mlrun-1.7.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,769 @@
1
+ # Copyright 2023 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
+ import base64
15
+ import typing
16
+ from typing import Optional, Union
17
+ from urllib.parse import urljoin
18
+
19
+ import requests
20
+ from nuclio.auth import AuthInfo as NuclioAuthInfo
21
+ from nuclio.auth import AuthKinds as NuclioAuthKinds
22
+
23
+ import mlrun
24
+ import mlrun.common.constants as mlrun_constants
25
+ import mlrun.common.helpers
26
+ import mlrun.common.schemas as schemas
27
+ import mlrun.common.types
28
+ from mlrun.model import ModelObj
29
+ from mlrun.platforms.iguazio import min_iguazio_versions
30
+ from mlrun.utils import logger
31
+
32
+ from .function import min_nuclio_versions
33
+
34
+
35
+ class Authenticator(typing.Protocol):
36
+ @property
37
+ def authentication_mode(self) -> str:
38
+ return schemas.APIGatewayAuthenticationMode.none.value
39
+
40
+ @classmethod
41
+ def from_scheme(cls, api_gateway_spec: schemas.APIGatewaySpec):
42
+ if (
43
+ api_gateway_spec.authenticationMode
44
+ == schemas.APIGatewayAuthenticationMode.basic.value
45
+ ):
46
+ if api_gateway_spec.authentication:
47
+ return BasicAuth(
48
+ username=api_gateway_spec.authentication.get("username", ""),
49
+ password=api_gateway_spec.authentication.get("password", ""),
50
+ )
51
+ else:
52
+ return BasicAuth()
53
+ elif (
54
+ api_gateway_spec.authenticationMode
55
+ == schemas.APIGatewayAuthenticationMode.access_key.value
56
+ ):
57
+ return AccessKeyAuth()
58
+ else:
59
+ return NoneAuth()
60
+
61
+ def to_scheme(
62
+ self,
63
+ ) -> Optional[dict[str, Optional[schemas.APIGatewayBasicAuth]]]:
64
+ return None
65
+
66
+
67
+ class APIGatewayAuthenticator(Authenticator, ModelObj):
68
+ _dict_fields = ["authentication_mode"]
69
+
70
+
71
+ class NoneAuth(APIGatewayAuthenticator):
72
+ """
73
+ An API gateway authenticator with no authentication.
74
+ """
75
+
76
+ pass
77
+
78
+
79
+ class BasicAuth(APIGatewayAuthenticator):
80
+ """
81
+ An API gateway authenticator with basic authentication.
82
+
83
+ :param username: (str) The username for basic authentication.
84
+ :param password: (str) The password for basic authentication.
85
+ """
86
+
87
+ def __init__(self, username=None, password=None):
88
+ self._username = username
89
+ self._password = password
90
+
91
+ @property
92
+ def authentication_mode(self) -> str:
93
+ return schemas.APIGatewayAuthenticationMode.basic.value
94
+
95
+ def to_scheme(
96
+ self,
97
+ ) -> Optional[dict[str, Optional[schemas.APIGatewayBasicAuth]]]:
98
+ return {
99
+ "basicAuth": schemas.APIGatewayBasicAuth(
100
+ username=self._username, password=self._password
101
+ )
102
+ }
103
+
104
+
105
+ class AccessKeyAuth(APIGatewayAuthenticator):
106
+ """
107
+ An API gateway authenticator with access key authentication.
108
+ """
109
+
110
+ @property
111
+ def authentication_mode(self) -> str:
112
+ return schemas.APIGatewayAuthenticationMode.access_key.value
113
+
114
+
115
+ class APIGatewayMetadata(ModelObj):
116
+ _dict_fields = ["name", "namespace", "labels", "annotations", "creation_timestamp"]
117
+
118
+ def __init__(
119
+ self,
120
+ name: str,
121
+ namespace: str = None,
122
+ labels: dict = None,
123
+ annotations: dict = None,
124
+ creation_timestamp: str = None,
125
+ ):
126
+ """
127
+ :param name: The name of the API gateway
128
+ :param namespace: The namespace of the API gateway
129
+ :param labels: The labels of the API gateway
130
+ :param annotations: The annotations of the API gateway
131
+ :param creation_timestamp: The creation timestamp of the API gateway
132
+ """
133
+ self.name = name
134
+ self.namespace = namespace
135
+ self.labels = labels or {}
136
+ self.annotations = annotations or {}
137
+ self.creation_timestamp = creation_timestamp
138
+
139
+ if not self.name:
140
+ raise mlrun.errors.MLRunInvalidArgumentError(
141
+ "API Gateway name cannot be empty"
142
+ )
143
+
144
+
145
+ class APIGatewaySpec(ModelObj):
146
+ _dict_fields = [
147
+ "functions",
148
+ "project",
149
+ "name",
150
+ "description",
151
+ "host",
152
+ "path",
153
+ "authentication",
154
+ "canary",
155
+ ]
156
+
157
+ def __init__(
158
+ self,
159
+ functions: Union[
160
+ list[
161
+ Union[
162
+ str,
163
+ "mlrun.runtimes.nuclio.function.RemoteRuntime",
164
+ "mlrun.runtimes.nuclio.serving.ServingRuntime",
165
+ "mlrun.runtimes.nuclio.application.ApplicationRuntime",
166
+ ]
167
+ ],
168
+ "mlrun.runtimes.nuclio.function.RemoteRuntime",
169
+ "mlrun.runtimes.nuclio.serving.ServingRuntime",
170
+ "mlrun.runtimes.nuclio.application.ApplicationRuntime",
171
+ ],
172
+ project: str = None,
173
+ description: str = "",
174
+ host: str = None,
175
+ path: str = "/",
176
+ authentication: Optional[APIGatewayAuthenticator] = NoneAuth(),
177
+ canary: Optional[list[int]] = None,
178
+ ports: Optional[list[int]] = None,
179
+ ):
180
+ """
181
+ :param functions: The list of functions associated with the API gateway
182
+ Can be a list of function names (["my-func1", "my-func2"])
183
+ or a list or a single entity of
184
+ :py:class:`~mlrun.runtimes.nuclio.function.RemoteRuntime` OR
185
+ :py:class:`~mlrun.runtimes.nuclio.serving.ServingRuntime` OR
186
+ :py:class:`~mlrun.runtimes.nuclio.application.ApplicationRuntime`
187
+ :param project: The project name
188
+ :param description: Optional description of the API gateway
189
+ :param path: Optional path of the API gateway, default value is "/"
190
+ :param authentication: The authentication for the API gateway of type
191
+ :py:class:`~mlrun.runtimes.nuclio.api_gateway.BasicAuth`
192
+ :param host: The host of the API gateway (optional). If not set, it will be automatically generated
193
+ :param canary: The canary percents for the API gateway of type list[int]; for instance: [20,80] (optional)
194
+ :param ports: The ports of the API gateway, as a list of integers that correspond to the functions in the
195
+ functions list. for instance: [8050] or [8050, 8081] (optional)
196
+ """
197
+ self.description = description
198
+ self.host = host
199
+ self.path = path
200
+ self.authentication = authentication
201
+ self.functions = functions
202
+ self.canary = canary
203
+ self.project = project
204
+ self.ports = ports
205
+
206
+ self.enrich()
207
+ self.validate(project=project, functions=functions, canary=canary, ports=ports)
208
+
209
+ def enrich(self):
210
+ if self.path and not self.path.startswith("/"):
211
+ self.path = f"/{self.path}"
212
+
213
+ def validate(
214
+ self,
215
+ project: str,
216
+ functions: Union[
217
+ list[
218
+ Union[
219
+ str,
220
+ "mlrun.runtimes.nuclio.function.RemoteRuntime",
221
+ "mlrun.runtimes.nuclio.serving.ServingRuntime",
222
+ "mlrun.runtimes.nuclio.application.ApplicationRuntime",
223
+ ]
224
+ ],
225
+ "mlrun.runtimes.nuclio.function.RemoteRuntime",
226
+ "mlrun.runtimes.nuclio.serving.ServingRuntime",
227
+ "mlrun.runtimes.nuclio.application.ApplicationRuntime",
228
+ ],
229
+ canary: Optional[list[int]] = None,
230
+ ports: Optional[list[int]] = None,
231
+ ):
232
+ self.functions = self._validate_functions(project=project, functions=functions)
233
+
234
+ # validating canary
235
+ if canary:
236
+ self.canary = self._validate_canary(canary)
237
+
238
+ # validating ports
239
+ if ports:
240
+ self.ports = self._validate_ports(ports)
241
+
242
+ def _validate_canary(self, canary: list[int]):
243
+ if len(self.functions) != len(canary):
244
+ raise mlrun.errors.MLRunInvalidArgumentError(
245
+ "Function and canary lists lengths do not match"
246
+ )
247
+ for canary_percent in canary:
248
+ if canary_percent < 0 or canary_percent > 100:
249
+ raise mlrun.errors.MLRunInvalidArgumentError(
250
+ "The percentage value must be in the range from 0 to 100"
251
+ )
252
+ if sum(canary) != 100:
253
+ raise mlrun.errors.MLRunInvalidArgumentError(
254
+ "The sum of canary function percents should be equal to 100"
255
+ )
256
+ return canary
257
+
258
+ def _validate_ports(self, ports):
259
+ if len(self.functions) != len(ports):
260
+ raise mlrun.errors.MLRunInvalidArgumentError(
261
+ "Function and port lists lengths do not match"
262
+ )
263
+
264
+ return ports
265
+
266
+ @staticmethod
267
+ def _validate_functions(
268
+ project: str,
269
+ functions: Union[
270
+ list[
271
+ Union[
272
+ str,
273
+ "mlrun.runtimes.nuclio.function.RemoteRuntime",
274
+ "mlrun.runtimes.nuclio.serving.ServingRuntime",
275
+ "mlrun.runtimes.nuclio.application.ApplicationRuntime",
276
+ ]
277
+ ],
278
+ "mlrun.runtimes.nuclio.function.RemoteRuntime",
279
+ "mlrun.runtimes.nuclio.serving.ServingRuntime",
280
+ "mlrun.runtimes.nuclio.application.ApplicationRuntime",
281
+ ],
282
+ ):
283
+ if not isinstance(functions, list):
284
+ functions = [functions]
285
+
286
+ # validating functions
287
+ if not 1 <= len(functions) <= 2:
288
+ raise mlrun.errors.MLRunInvalidArgumentError(
289
+ f"Gateway can be created from one or two functions, "
290
+ f"the number of functions passed is {len(functions)}"
291
+ )
292
+
293
+ function_names = []
294
+ for func in functions:
295
+ if isinstance(func, str):
296
+ # check whether the function was passed as a URI or just a name
297
+ parsed_project, function_name, _, _ = (
298
+ mlrun.common.helpers.parse_versioned_object_uri(func)
299
+ )
300
+
301
+ if parsed_project and function_name:
302
+ # check that parsed project and passed project are the same
303
+ if parsed_project != project:
304
+ raise mlrun.errors.MLRunInvalidArgumentError(
305
+ "Function doesn't belong to passed project"
306
+ )
307
+ function_uri = func
308
+ else:
309
+ function_uri = mlrun.utils.generate_object_uri(project, func)
310
+ function_names.append(function_uri)
311
+ continue
312
+
313
+ function_name = (
314
+ func.metadata.name if hasattr(func, "metadata") else func.name
315
+ )
316
+ if func.kind not in mlrun.runtimes.RuntimeKinds.nuclio_runtimes():
317
+ raise mlrun.errors.MLRunInvalidArgumentError(
318
+ f"Input function {function_name} is not a Nuclio function"
319
+ )
320
+ if func.metadata.project != project:
321
+ raise mlrun.errors.MLRunInvalidArgumentError(
322
+ f"input function {function_name} "
323
+ f"does not belong to this project"
324
+ )
325
+ function_uri = mlrun.utils.generate_object_uri(
326
+ project,
327
+ function_name,
328
+ func.metadata.tag,
329
+ func.metadata.hash,
330
+ )
331
+ function_names.append(function_uri)
332
+ return function_names
333
+
334
+
335
+ class APIGatewayStatus(ModelObj):
336
+ def __init__(self, state: Optional[schemas.APIGatewayState] = None):
337
+ self.state = state or schemas.APIGatewayState.none
338
+
339
+
340
+ class APIGateway(ModelObj):
341
+ _dict_fields = [
342
+ "metadata",
343
+ "spec",
344
+ "state",
345
+ ]
346
+
347
+ @min_nuclio_versions("1.13.1")
348
+ def __init__(
349
+ self,
350
+ metadata: APIGatewayMetadata,
351
+ spec: APIGatewaySpec,
352
+ status: Optional[APIGatewayStatus] = None,
353
+ ):
354
+ """
355
+ Initialize the APIGateway instance.
356
+
357
+ :param metadata: (APIGatewayMetadata) The metadata of the API gateway.
358
+ :param spec: (APIGatewaySpec) The spec of the API gateway.
359
+ :param status: (APIGatewayStatus) The status of the API gateway.
360
+ """
361
+ self.metadata = metadata
362
+ self.spec = spec
363
+ self.status = status
364
+
365
+ @property
366
+ def metadata(self) -> APIGatewayMetadata:
367
+ return self._metadata
368
+
369
+ @metadata.setter
370
+ def metadata(self, metadata):
371
+ self._metadata = self._verify_dict(metadata, "metadata", APIGatewayMetadata)
372
+
373
+ @property
374
+ def spec(self) -> APIGatewaySpec:
375
+ return self._spec
376
+
377
+ @spec.setter
378
+ def spec(self, spec):
379
+ self._spec = self._verify_dict(spec, "spec", APIGatewaySpec)
380
+
381
+ @property
382
+ def status(self) -> APIGatewayStatus:
383
+ return self._status
384
+
385
+ @status.setter
386
+ def status(self, status):
387
+ self._status = self._verify_dict(status, "status", APIGatewayStatus)
388
+
389
+ def invoke(
390
+ self,
391
+ method="POST",
392
+ headers: dict = None,
393
+ credentials: Optional[tuple[str, str]] = None,
394
+ path: Optional[str] = None,
395
+ body: Optional[Union[str, bytes, dict]] = None,
396
+ **kwargs,
397
+ ):
398
+ """
399
+ Invoke the API gateway.
400
+
401
+ :param method: (str, optional) The HTTP method for the invocation.
402
+ :param headers: (dict, optional) The HTTP headers for the invocation.
403
+ :param credentials: (Optional[tuple[str, str]], optional) The (username,password) for the invocation if required
404
+ can also be set by the environment variable (_, V3IO_ACCESS_KEY) for access key authentication.
405
+ :param path: (str, optional) The sub-path for the invocation.
406
+ :param body: (Optional[Union[str, bytes, dict]]) The body of the invocation.
407
+ :param kwargs: (dict) Additional keyword arguments.
408
+
409
+ :return: The response from the API gateway invocation.
410
+ """
411
+ if not self.invoke_url:
412
+ # try to resolve invoke_url before fail
413
+ self.sync()
414
+ if not self.invoke_url:
415
+ raise mlrun.errors.MLRunInvalidArgumentError(
416
+ "Invocation url is not set. Set up gateway's `invoke_url` attribute."
417
+ )
418
+ if not self.is_ready():
419
+ raise mlrun.errors.MLRunPreconditionFailedError(
420
+ f"API gateway is not ready. " f"Current state: {self.status.state}"
421
+ )
422
+
423
+ auth = None
424
+
425
+ if (
426
+ self.spec.authentication.authentication_mode
427
+ == schemas.APIGatewayAuthenticationMode.basic.value
428
+ ):
429
+ if not credentials:
430
+ raise mlrun.errors.MLRunInvalidArgumentError(
431
+ "API Gateway invocation requires authentication. Please pass credentials"
432
+ )
433
+ auth = NuclioAuthInfo(
434
+ username=credentials[0], password=credentials[1]
435
+ ).to_requests_auth()
436
+
437
+ if (
438
+ self.spec.authentication.authentication_mode
439
+ == schemas.APIGatewayAuthenticationMode.access_key.value
440
+ ):
441
+ # inject access key from env
442
+ if credentials:
443
+ auth = NuclioAuthInfo(
444
+ username=credentials[0],
445
+ password=credentials[1],
446
+ mode=NuclioAuthKinds.iguazio,
447
+ ).to_requests_auth()
448
+ else:
449
+ auth = NuclioAuthInfo().from_envvar().to_requests_auth()
450
+ if not auth:
451
+ raise mlrun.errors.MLRunInvalidArgumentError(
452
+ "API Gateway invocation requires authentication. Please set V3IO_ACCESS_KEY env var"
453
+ )
454
+ url = urljoin(self.invoke_url, path or "")
455
+
456
+ # Determine the correct keyword argument for the body
457
+ if isinstance(body, dict):
458
+ kwargs["json"] = body
459
+ elif isinstance(body, (str, bytes)):
460
+ kwargs["data"] = body
461
+
462
+ return requests.request(
463
+ method=method,
464
+ url=url,
465
+ headers=headers or {},
466
+ auth=auth,
467
+ **kwargs,
468
+ )
469
+
470
+ def wait_for_readiness(self, max_wait_time=90):
471
+ """
472
+ Wait for the API gateway to become ready within the maximum wait time.
473
+
474
+ Parameters:
475
+ max_wait_time: int - Maximum time to wait in seconds (default is 90 seconds).
476
+
477
+ Returns:
478
+ bool: True if the entity becomes ready within the maximum wait time, False otherwise
479
+ """
480
+
481
+ def _ensure_ready():
482
+ if not self.is_ready():
483
+ raise AssertionError(
484
+ f"Waiting for gateway readiness is taking more than {max_wait_time} seconds"
485
+ )
486
+
487
+ return mlrun.utils.helpers.retry_until_successful(
488
+ 3, max_wait_time, logger, False, _ensure_ready
489
+ )
490
+
491
+ def is_ready(self):
492
+ if self.status.state is not schemas.api_gateway.APIGatewayState.ready:
493
+ # try to sync the state
494
+ self.sync()
495
+ return self.status.state == schemas.api_gateway.APIGatewayState.ready
496
+
497
+ def sync(self):
498
+ """
499
+ Synchronize the API gateway from the server.
500
+ """
501
+ synced_gateway = mlrun.get_run_db().get_api_gateway(
502
+ self.metadata.name, self.spec.project
503
+ )
504
+ synced_gateway = self.from_scheme(synced_gateway)
505
+
506
+ self.spec.host = synced_gateway.spec.host
507
+ self.spec.path = synced_gateway.spec.path
508
+ self.spec.authentication = synced_gateway.spec.authentication
509
+ self.spec.functions = synced_gateway.spec.functions
510
+ self.spec.canary = synced_gateway.spec.canary
511
+ self.spec.description = synced_gateway.spec.description
512
+ self.status.state = synced_gateway.status.state
513
+
514
+ def with_basic_auth(self, username: str, password: str):
515
+ """
516
+ Set basic authentication for the API gateway.
517
+
518
+ :param username: (str) The username for basic authentication.
519
+ :param password: (str) The password for basic authentication.
520
+ """
521
+ self.spec.authentication = BasicAuth(username=username, password=password)
522
+
523
+ @min_iguazio_versions("3.5.5")
524
+ def with_access_key_auth(self):
525
+ """
526
+ Set access key authentication for the API gateway.
527
+ """
528
+ self.spec.authentication = AccessKeyAuth()
529
+
530
+ def with_canary(
531
+ self,
532
+ functions: Union[
533
+ list[
534
+ Union[
535
+ str,
536
+ "mlrun.runtimes.nuclio.function.RemoteRuntime",
537
+ "mlrun.runtimes.nuclio.serving.ServingRuntime",
538
+ "mlrun.runtimes.nuclio.application.ApplicationRuntime",
539
+ ]
540
+ ],
541
+ "mlrun.runtimes.nuclio.function.RemoteRuntime",
542
+ "mlrun.runtimes.nuclio.serving.ServingRuntime",
543
+ "mlrun.runtimes.nuclio.application.ApplicationRuntime",
544
+ ],
545
+ canary: list[int],
546
+ ):
547
+ """
548
+ Set canary function for the API gateway
549
+
550
+ :param functions: The list of functions associated with the API gateway
551
+ Can be a list of function names (["my-func1", "my-func2"])
552
+ or a list of nuclio functions of types
553
+ :py:class:`~mlrun.runtimes.nuclio.function.RemoteRuntime` OR
554
+ :py:class:`~mlrun.runtimes.nuclio.serving.ServingRuntime` OR
555
+ :py:class:`~mlrun.runtimes.nuclio.application.ApplicationRuntime`
556
+ :param canary: The canary percents for the API gateway of type list[int]; for instance: [20,80]
557
+
558
+ """
559
+ if len(functions) != 2:
560
+ raise mlrun.errors.MLRunInvalidArgumentError(
561
+ f"Gateway with canary can be created only with two functions, "
562
+ f"the number of functions passed is {len(functions)}"
563
+ )
564
+ self.spec.validate(
565
+ project=self.spec.project, functions=functions, canary=canary
566
+ )
567
+
568
+ def with_ports(self, ports: list[int]):
569
+ """
570
+ Set ports for the API gateway
571
+
572
+ :param ports: The ports of the API gateway, as a list of integers that correspond to the functions in the
573
+ functions list. for instance: [8050] or [8050, 8081]
574
+ """
575
+ self.spec.validate(
576
+ project=self.spec.project, functions=self.spec.functions, ports=ports
577
+ )
578
+
579
+ def with_force_ssl_redirect(self):
580
+ """
581
+ Set SSL redirect annotation for the API gateway.
582
+ """
583
+ self.metadata.annotations["nginx.ingress.kubernetes.io/force-ssl-redirect"] = (
584
+ "true"
585
+ )
586
+
587
+ def with_gateway_timeout(self, gateway_timeout: int):
588
+ """
589
+ Set gateway proxy connect/read/send timeout annotations
590
+ :param gateway_timeout: The timeout in seconds
591
+ """
592
+ mlrun.runtimes.utils.enrich_gateway_timeout_annotations(
593
+ self.metadata.annotations, gateway_timeout
594
+ )
595
+
596
+ def with_annotations(self, annotations: dict):
597
+ """set a key/value annotations in the metadata of the api gateway"""
598
+ for key, value in annotations.items():
599
+ self.metadata.annotations[key] = str(value)
600
+ return self
601
+
602
+ @classmethod
603
+ def from_scheme(cls, api_gateway: schemas.APIGateway):
604
+ project = api_gateway.metadata.labels.get(
605
+ mlrun_constants.MLRunInternalLabels.nuclio_project_name
606
+ )
607
+ functions, canary = cls._resolve_canary(api_gateway.spec.upstreams)
608
+ state = (
609
+ api_gateway.status.state
610
+ if api_gateway.status
611
+ else schemas.APIGatewayState.none
612
+ )
613
+ new_api_gateway = cls(
614
+ metadata=APIGatewayMetadata(
615
+ name=api_gateway.spec.name,
616
+ annotations=api_gateway.metadata.annotations,
617
+ labels=api_gateway.metadata.labels,
618
+ ),
619
+ spec=APIGatewaySpec(
620
+ project=project,
621
+ description=api_gateway.spec.description,
622
+ host=api_gateway.spec.host,
623
+ path=api_gateway.spec.path,
624
+ authentication=APIGatewayAuthenticator.from_scheme(api_gateway.spec),
625
+ functions=functions,
626
+ canary=canary,
627
+ ),
628
+ status=APIGatewayStatus(state=state),
629
+ )
630
+ return new_api_gateway
631
+
632
+ def to_scheme(self) -> schemas.APIGateway:
633
+ upstreams = (
634
+ [
635
+ schemas.APIGatewayUpstream(
636
+ nucliofunction={"name": self.spec.functions[0]},
637
+ percentage=self.spec.canary[0],
638
+ ),
639
+ schemas.APIGatewayUpstream(
640
+ # do not set percent for the second function,
641
+ # so we can define which function to display as a primary one in UI
642
+ nucliofunction={"name": self.spec.functions[1]},
643
+ ),
644
+ ]
645
+ if self.spec.canary
646
+ else [
647
+ schemas.APIGatewayUpstream(
648
+ nucliofunction={"name": function_name},
649
+ )
650
+ for function_name in self.spec.functions
651
+ ]
652
+ )
653
+ if self.spec.ports:
654
+ for i, port in enumerate(self.spec.ports):
655
+ upstreams[i].port = port
656
+
657
+ api_gateway = schemas.APIGateway(
658
+ metadata=schemas.APIGatewayMetadata(
659
+ name=self.metadata.name,
660
+ labels=self.metadata.labels,
661
+ annotations=self.metadata.annotations,
662
+ ),
663
+ spec=schemas.APIGatewaySpec(
664
+ name=self.metadata.name,
665
+ description=self.spec.description,
666
+ host=self.spec.host,
667
+ path=self.spec.path,
668
+ authenticationMode=schemas.APIGatewayAuthenticationMode.from_str(
669
+ self.spec.authentication.authentication_mode
670
+ ),
671
+ upstreams=upstreams,
672
+ ),
673
+ status=schemas.APIGatewayStatus(state=self.status.state),
674
+ )
675
+ api_gateway.spec.authentication = self.spec.authentication.to_scheme()
676
+ return api_gateway
677
+
678
+ @property
679
+ def invoke_url(
680
+ self,
681
+ ):
682
+ """
683
+ Get the invoke URL.
684
+
685
+ :return: (str) The invoke URL.
686
+ """
687
+ host = self.spec.host
688
+ if not self.spec.host.startswith("http"):
689
+ host = f"https://{self.spec.host}"
690
+ return urljoin(host, self.spec.path).rstrip("/")
691
+
692
+ @staticmethod
693
+ def _generate_basic_auth(username: str, password: str):
694
+ token = base64.b64encode(f"{username}:{password}".encode()).decode()
695
+ return f"Basic {token}"
696
+
697
+ @staticmethod
698
+ def _resolve_canary(
699
+ upstreams: list[schemas.APIGatewayUpstream],
700
+ ) -> tuple[Union[list[str], None], Union[list[int], None]]:
701
+ if len(upstreams) == 1:
702
+ return [upstreams[0].nucliofunction.get("name")], None
703
+ elif len(upstreams) == 2:
704
+ canary = [0, 0]
705
+ functions = [
706
+ upstreams[0].nucliofunction.get("name"),
707
+ upstreams[1].nucliofunction.get("name"),
708
+ ]
709
+ percentage_1 = upstreams[0].percentage
710
+ percentage_2 = upstreams[1].percentage
711
+
712
+ if not percentage_1 and percentage_2:
713
+ percentage_1 = 100 - percentage_2
714
+ if not percentage_2 and percentage_1:
715
+ percentage_2 = 100 - percentage_1
716
+ if percentage_1 and percentage_2:
717
+ canary = [percentage_1, percentage_2]
718
+ return functions, canary
719
+ else:
720
+ # Nuclio only supports 1 or 2 upstream functions
721
+ return None, None
722
+
723
+ @property
724
+ def name(self):
725
+ return self.metadata.name
726
+
727
+ @name.setter
728
+ def name(self, value):
729
+ self.metadata.name = value
730
+
731
+ @property
732
+ def project(self):
733
+ return self.spec.project
734
+
735
+ @project.setter
736
+ def project(self, value):
737
+ self.spec.project = value
738
+
739
+ @property
740
+ def description(self):
741
+ return self.spec.description
742
+
743
+ @description.setter
744
+ def description(self, value):
745
+ self.spec.description = value
746
+
747
+ @property
748
+ def host(self):
749
+ return self.spec.host
750
+
751
+ @host.setter
752
+ def host(self, value):
753
+ self.spec.host = value
754
+
755
+ @property
756
+ def path(self):
757
+ return self.spec.path
758
+
759
+ @path.setter
760
+ def path(self, value):
761
+ self.spec.path = value
762
+
763
+ @property
764
+ def authentication(self):
765
+ return self.spec.authentication
766
+
767
+ @authentication.setter
768
+ def authentication(self, value):
769
+ self.spec.authentication = value