mlrun 1.7.0rc13__py3-none-any.whl → 1.7.0rc14__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.

@@ -22,6 +22,7 @@ from requests.auth import HTTPBasicAuth
22
22
  import mlrun
23
23
  import mlrun.common.schemas
24
24
 
25
+ from ...model import ModelObj
25
26
  from ..utils import logger
26
27
  from .function import RemoteRuntime, get_fullname, min_nuclio_versions
27
28
  from .serving import ServingRuntime
@@ -92,12 +93,50 @@ class BasicAuth(APIGatewayAuthenticator):
92
93
  }
93
94
 
94
95
 
95
- class APIGateway:
96
- @min_nuclio_versions("1.13.1")
96
+ class APIGatewayMetadata(ModelObj):
97
+ _dict_fields = ["name", "namespace", "labels", "annotations", "creation_timestamp"]
98
+
97
99
  def __init__(
98
100
  self,
99
- project,
100
101
  name: str,
102
+ namespace: str = None,
103
+ labels: dict = None,
104
+ annotations: dict = None,
105
+ creation_timestamp: str = None,
106
+ ):
107
+ """
108
+ :param name: The name of the API gateway
109
+ :param namespace: The namespace of the API gateway
110
+ :param labels: The labels of the API gateway
111
+ :param annotations: The annotations of the API gateway
112
+ :param creation_timestamp: The creation timestamp of the API gateway
113
+ """
114
+ self.name = name
115
+ self.namespace = namespace
116
+ self.labels = labels or {}
117
+ self.annotations = annotations or {}
118
+ self.creation_timestamp = creation_timestamp
119
+
120
+ if not self.name:
121
+ raise mlrun.errors.MLRunInvalidArgumentError(
122
+ "API Gateway name cannot be empty"
123
+ )
124
+
125
+
126
+ class APIGatewaySpec(ModelObj):
127
+ _dict_fields = [
128
+ "functions",
129
+ "project",
130
+ "name",
131
+ "description",
132
+ "host",
133
+ "path",
134
+ "authentication",
135
+ "canary",
136
+ ]
137
+
138
+ def __init__(
139
+ self,
101
140
  functions: Union[
102
141
  list[str],
103
142
  Union[
@@ -107,26 +146,24 @@ class APIGateway:
107
146
  ServingRuntime,
108
147
  ]
109
148
  ],
110
- Union[RemoteRuntime, ServingRuntime],
149
+ RemoteRuntime,
150
+ ServingRuntime,
111
151
  ],
112
152
  ],
153
+ project: str = None,
113
154
  description: str = "",
155
+ host: str = None,
114
156
  path: str = "/",
115
157
  authentication: Optional[APIGatewayAuthenticator] = NoneAuth(),
116
- host: Optional[str] = None,
117
158
  canary: Optional[list[int]] = None,
118
159
  ):
119
160
  """
120
- Initialize the APIGateway instance.
121
-
122
- :param project: The project name
123
- :param name: The name of the API gateway
124
161
  :param functions: The list of functions associated with the API gateway
125
162
  Can be a list of function names (["my-func1", "my-func2"])
126
163
  or a list or a single entity of
127
164
  :py:class:`~mlrun.runtimes.nuclio.function.RemoteRuntime` OR
128
165
  :py:class:`~mlrun.runtimes.nuclio.serving.ServingRuntime`
129
-
166
+ :param project: The project name
130
167
  :param description: Optional description of the API gateway
131
168
  :param path: Optional path of the API gateway, default value is "/"
132
169
  :param authentication: The authentication for the API gateway of type
@@ -134,23 +171,144 @@ class APIGateway:
134
171
  :param host: The host of the API gateway (optional). If not set, it will be automatically generated
135
172
  :param canary: The canary percents for the API gateway of type list[int]; for instance: [20,80]
136
173
  """
137
- self.functions = None
138
- self._validate(
139
- project=project,
140
- functions=functions,
141
- name=name,
142
- canary=canary,
143
- )
144
- self.project = project
145
- self.name = name
174
+ self.description = description
146
175
  self.host = host
147
-
148
176
  self.path = path
149
- self.description = description
150
- self.canary = canary
151
177
  self.authentication = authentication
178
+ self.functions = functions
179
+ self.canary = canary
180
+ self.project = project
181
+
182
+ self.validate(project=project, functions=functions, canary=canary)
183
+
184
+ def validate(
185
+ self,
186
+ project: str,
187
+ functions: Union[
188
+ list[str],
189
+ Union[
190
+ list[
191
+ Union[
192
+ RemoteRuntime,
193
+ ServingRuntime,
194
+ ]
195
+ ],
196
+ RemoteRuntime,
197
+ ServingRuntime,
198
+ ],
199
+ ],
200
+ canary: Optional[list[int]] = None,
201
+ ):
202
+ self.functions = self._validate_functions(project=project, functions=functions)
203
+
204
+ # validating canary
205
+ if canary:
206
+ self.canary = self._validate_canary(canary)
207
+
208
+ def _validate_canary(self, canary: list[int]):
209
+ if len(self.functions) != len(canary):
210
+ raise mlrun.errors.MLRunInvalidArgumentError(
211
+ "Function and canary lists lengths do not match"
212
+ )
213
+ for canary_percent in canary:
214
+ if canary_percent < 0 or canary_percent > 100:
215
+ raise mlrun.errors.MLRunInvalidArgumentError(
216
+ "The percentage value must be in the range from 0 to 100"
217
+ )
218
+ if sum(canary) != 100:
219
+ raise mlrun.errors.MLRunInvalidArgumentError(
220
+ "The sum of canary function percents should be equal to 100"
221
+ )
222
+ return canary
223
+
224
+ @staticmethod
225
+ def _validate_functions(
226
+ project: str,
227
+ functions: Union[
228
+ list[str],
229
+ Union[
230
+ list[
231
+ Union[
232
+ RemoteRuntime,
233
+ ServingRuntime,
234
+ ]
235
+ ],
236
+ Union[RemoteRuntime, ServingRuntime],
237
+ ],
238
+ ],
239
+ ):
240
+ if not isinstance(functions, list):
241
+ functions = [functions]
242
+
243
+ # validating functions
244
+ if not 1 <= len(functions) <= 2:
245
+ raise mlrun.errors.MLRunInvalidArgumentError(
246
+ f"Gateway can be created from one or two functions, "
247
+ f"the number of functions passed is {len(functions)}"
248
+ )
249
+
250
+ function_names = []
251
+ for func in functions:
252
+ if isinstance(func, str):
253
+ function_names.append(func)
254
+ continue
255
+
256
+ function_name = (
257
+ func.metadata.name if hasattr(func, "metadata") else func.name
258
+ )
259
+ if func.kind not in mlrun.runtimes.RuntimeKinds.nuclio_runtimes():
260
+ raise mlrun.errors.MLRunInvalidArgumentError(
261
+ f"Input function {function_name} is not a Nuclio function"
262
+ )
263
+ if func.metadata.project != project:
264
+ raise mlrun.errors.MLRunInvalidArgumentError(
265
+ f"input function {function_name} "
266
+ f"does not belong to this project"
267
+ )
268
+ nuclio_name = get_fullname(function_name, project, func.metadata.tag)
269
+ function_names.append(nuclio_name)
270
+ return function_names
271
+
272
+
273
+ class APIGateway(ModelObj):
274
+ _dict_fields = [
275
+ "metadata",
276
+ "spec",
277
+ "state",
278
+ ]
279
+
280
+ @min_nuclio_versions("1.13.1")
281
+ def __init__(
282
+ self,
283
+ metadata: APIGatewayMetadata,
284
+ spec: APIGatewaySpec,
285
+ ):
286
+ """
287
+ Initialize the APIGateway instance.
288
+
289
+ :param metadata: (APIGatewayMetadata) The metadata of the API gateway.
290
+ :param spec: (APIGatewaySpec) The spec of the API gateway.
291
+ """
292
+ self.metadata = metadata
293
+ self.spec = spec
152
294
  self.state = ""
153
295
 
296
+ @property
297
+ def metadata(self) -> APIGatewayMetadata:
298
+ return self._metadata
299
+
300
+ @metadata.setter
301
+ def metadata(self, metadata):
302
+ self._metadata = self._verify_dict(metadata, "metadata", APIGatewayMetadata)
303
+
304
+ @property
305
+ def spec(self) -> APIGatewaySpec:
306
+ return self._spec
307
+
308
+ @spec.setter
309
+ def spec(self, spec):
310
+ self._spec = self._verify_dict(spec, "spec", APIGatewaySpec)
311
+
154
312
  def invoke(
155
313
  self,
156
314
  method="POST",
@@ -181,7 +339,7 @@ class APIGateway:
181
339
  )
182
340
 
183
341
  if (
184
- self.authentication.authentication_mode
342
+ self.spec.authentication.authentication_mode
185
343
  == NUCLIO_API_GATEWAY_AUTHENTICATION_MODE_BASIC_AUTH
186
344
  and not auth
187
345
  ):
@@ -227,15 +385,17 @@ class APIGateway:
227
385
  """
228
386
  Synchronize the API gateway from the server.
229
387
  """
230
- synced_gateway = mlrun.get_run_db().get_api_gateway(self.name, self.project)
388
+ synced_gateway = mlrun.get_run_db().get_api_gateway(
389
+ self.metadata.name, self.spec.project
390
+ )
231
391
  synced_gateway = self.from_scheme(synced_gateway)
232
392
 
233
- self.host = synced_gateway.host
234
- self.path = synced_gateway.path
235
- self.authentication = synced_gateway.authentication
236
- self.functions = synced_gateway.functions
237
- self.canary = synced_gateway.canary
238
- self.description = synced_gateway.description
393
+ self.spec.host = synced_gateway.spec.host
394
+ self.spec.path = synced_gateway.spec.path
395
+ self.spec.authentication = synced_gateway.spec.authentication
396
+ self.spec.functions = synced_gateway.spec.functions
397
+ self.spec.canary = synced_gateway.spec.canary
398
+ self.spec.description = synced_gateway.spec.description
239
399
  self.state = synced_gateway.state
240
400
 
241
401
  def with_basic_auth(self, username: str, password: str):
@@ -245,7 +405,7 @@ class APIGateway:
245
405
  :param username: (str) The username for basic authentication.
246
406
  :param password: (str) The password for basic authentication.
247
407
  """
248
- self.authentication = BasicAuth(username=username, password=password)
408
+ self.spec.authentication = BasicAuth(username=username, password=password)
249
409
 
250
410
  def with_canary(
251
411
  self,
@@ -276,8 +436,9 @@ class APIGateway:
276
436
  f"Gateway with canary can be created only with two functions, "
277
437
  f"the number of functions passed is {len(functions)}"
278
438
  )
279
- self.functions = self._validate_functions(self.project, functions)
280
- self.canary = self._validate_canary(canary)
439
+ self.spec.validate(
440
+ project=self.spec.project, functions=functions, canary=canary
441
+ )
281
442
 
282
443
  @classmethod
283
444
  def from_scheme(cls, api_gateway: mlrun.common.schemas.APIGateway):
@@ -288,54 +449,60 @@ class APIGateway:
288
449
  if api_gateway.status
289
450
  else mlrun.common.schemas.APIGatewayState.none
290
451
  )
291
- api_gateway = cls(
292
- project=project,
293
- description=api_gateway.spec.description,
294
- name=api_gateway.spec.name,
295
- host=api_gateway.spec.host,
296
- path=api_gateway.spec.path,
297
- authentication=APIGatewayAuthenticator.from_scheme(api_gateway.spec),
298
- functions=functions,
299
- canary=canary,
452
+ new_api_gateway = cls(
453
+ metadata=APIGatewayMetadata(
454
+ name=api_gateway.spec.name,
455
+ ),
456
+ spec=APIGatewaySpec(
457
+ project=project,
458
+ description=api_gateway.spec.description,
459
+ host=api_gateway.spec.host,
460
+ path=api_gateway.spec.path,
461
+ authentication=APIGatewayAuthenticator.from_scheme(api_gateway.spec),
462
+ functions=functions,
463
+ canary=canary,
464
+ ),
300
465
  )
301
- api_gateway.state = state
302
- return api_gateway
466
+ new_api_gateway.state = state
467
+ return new_api_gateway
303
468
 
304
469
  def to_scheme(self) -> mlrun.common.schemas.APIGateway:
305
470
  upstreams = (
306
471
  [
307
472
  mlrun.common.schemas.APIGatewayUpstream(
308
- nucliofunction={"name": self.functions[0]},
309
- percentage=self.canary[0],
473
+ nucliofunction={"name": self.spec.functions[0]},
474
+ percentage=self.spec.canary[0],
310
475
  ),
311
476
  mlrun.common.schemas.APIGatewayUpstream(
312
477
  # do not set percent for the second function,
313
478
  # so we can define which function to display as a primary one in UI
314
- nucliofunction={"name": self.functions[1]},
479
+ nucliofunction={"name": self.spec.functions[1]},
315
480
  ),
316
481
  ]
317
- if self.canary
482
+ if self.spec.canary
318
483
  else [
319
484
  mlrun.common.schemas.APIGatewayUpstream(
320
485
  nucliofunction={"name": function_name},
321
486
  )
322
- for function_name in self.functions
487
+ for function_name in self.spec.functions
323
488
  ]
324
489
  )
325
490
  api_gateway = mlrun.common.schemas.APIGateway(
326
- metadata=mlrun.common.schemas.APIGatewayMetadata(name=self.name, labels={}),
491
+ metadata=mlrun.common.schemas.APIGatewayMetadata(
492
+ name=self.metadata.name, labels={}
493
+ ),
327
494
  spec=mlrun.common.schemas.APIGatewaySpec(
328
- name=self.name,
329
- description=self.description,
330
- host=self.host,
331
- path=self.path,
495
+ name=self.metadata.name,
496
+ description=self.spec.description,
497
+ host=self.spec.host,
498
+ path=self.spec.path,
332
499
  authenticationMode=mlrun.common.schemas.APIGatewayAuthenticationMode.from_str(
333
- self.authentication.authentication_mode
500
+ self.spec.authentication.authentication_mode
334
501
  ),
335
502
  upstreams=upstreams,
336
503
  ),
337
504
  )
338
- api_gateway.spec.authentication = self.authentication.to_scheme()
505
+ api_gateway.spec.authentication = self.spec.authentication.to_scheme()
339
506
  return api_gateway
340
507
 
341
508
  @property
@@ -347,103 +514,10 @@ class APIGateway:
347
514
 
348
515
  :return: (str) The invoke URL.
349
516
  """
350
- host = self.host
351
- if not self.host.startswith("http"):
352
- host = f"https://{self.host}"
353
- return urljoin(host, self.path)
354
-
355
- def _validate(
356
- self,
357
- name: str,
358
- project: str,
359
- functions: Union[
360
- list[str],
361
- Union[
362
- list[
363
- Union[
364
- RemoteRuntime,
365
- ServingRuntime,
366
- ]
367
- ],
368
- Union[RemoteRuntime, ServingRuntime],
369
- ],
370
- ],
371
- canary: Optional[list[int]] = None,
372
- ):
373
- if not name:
374
- raise mlrun.errors.MLRunInvalidArgumentError(
375
- "API Gateway name cannot be empty"
376
- )
377
-
378
- self.functions = self._validate_functions(project=project, functions=functions)
379
-
380
- # validating canary
381
- if canary:
382
- self._validate_canary(canary)
383
-
384
- def _validate_canary(self, canary: list[int]):
385
- if len(self.functions) != len(canary):
386
- raise mlrun.errors.MLRunInvalidArgumentError(
387
- "Function and canary lists lengths do not match"
388
- )
389
- for canary_percent in canary:
390
- if canary_percent < 0 or canary_percent > 100:
391
- raise mlrun.errors.MLRunInvalidArgumentError(
392
- "The percentage value must be in the range from 0 to 100"
393
- )
394
- if sum(canary) != 100:
395
- raise mlrun.errors.MLRunInvalidArgumentError(
396
- "The sum of canary function percents should be equal to 100"
397
- )
398
- return canary
399
-
400
- @staticmethod
401
- def _validate_functions(
402
- project: str,
403
- functions: Union[
404
- list[str],
405
- Union[
406
- list[
407
- Union[
408
- RemoteRuntime,
409
- ServingRuntime,
410
- ]
411
- ],
412
- Union[RemoteRuntime, ServingRuntime],
413
- ],
414
- ],
415
- ):
416
- if not isinstance(functions, list):
417
- functions = [functions]
418
-
419
- # validating functions
420
- if not 1 <= len(functions) <= 2:
421
- raise mlrun.errors.MLRunInvalidArgumentError(
422
- f"Gateway can be created from one or two functions, "
423
- f"the number of functions passed is {len(functions)}"
424
- )
425
-
426
- function_names = []
427
- for func in functions:
428
- if isinstance(func, str):
429
- function_names.append(func)
430
- continue
431
-
432
- function_name = (
433
- func.metadata.name if hasattr(func, "metadata") else func.name
434
- )
435
- if func.kind not in mlrun.runtimes.RuntimeKinds.nuclio_runtimes():
436
- raise mlrun.errors.MLRunInvalidArgumentError(
437
- f"Input function {function_name} is not a Nuclio function"
438
- )
439
- if func.metadata.project != project:
440
- raise mlrun.errors.MLRunInvalidArgumentError(
441
- f"input function {function_name} "
442
- f"does not belong to this project"
443
- )
444
- nuclio_name = get_fullname(function_name, project, func.metadata.tag)
445
- function_names.append(nuclio_name)
446
- return function_names
517
+ host = self.spec.host
518
+ if not self.spec.host.startswith("http"):
519
+ host = f"https://{self.spec.host}"
520
+ return urljoin(host, self.spec.path)
447
521
 
448
522
  @staticmethod
449
523
  def _generate_basic_auth(username: str, password: str):
@@ -475,3 +549,51 @@ class APIGateway:
475
549
  else:
476
550
  # Nuclio only supports 1 or 2 upstream functions
477
551
  return None, None
552
+
553
+ @property
554
+ def name(self):
555
+ return self.metadata.name
556
+
557
+ @name.setter
558
+ def name(self, value):
559
+ self.metadata.name = value
560
+
561
+ @property
562
+ def project(self):
563
+ return self.spec.project
564
+
565
+ @project.setter
566
+ def project(self, value):
567
+ self.spec.project = value
568
+
569
+ @property
570
+ def description(self):
571
+ return self.spec.description
572
+
573
+ @description.setter
574
+ def description(self, value):
575
+ self.spec.description = value
576
+
577
+ @property
578
+ def host(self):
579
+ return self.spec.host
580
+
581
+ @host.setter
582
+ def host(self, value):
583
+ self.spec.host = value
584
+
585
+ @property
586
+ def path(self):
587
+ return self.spec.path
588
+
589
+ @path.setter
590
+ def path(self, value):
591
+ self.spec.path = value
592
+
593
+ @property
594
+ def authentication(self):
595
+ return self.spec.authentication
596
+
597
+ @authentication.setter
598
+ def authentication(self, value):
599
+ self.spec.authentication = value
mlrun/runtimes/pod.py CHANGED
@@ -1086,12 +1086,12 @@ class KubeResource(BaseRuntime):
1086
1086
 
1087
1087
  def _set_env(self, name, value=None, value_from=None):
1088
1088
  new_var = k8s_client.V1EnvVar(name=name, value=value, value_from=value_from)
1089
- i = 0
1090
- for v in self.spec.env:
1091
- if get_item_name(v) == name:
1092
- self.spec.env[i] = new_var
1089
+
1090
+ # ensure we don't have duplicate env vars with the same name
1091
+ for env_index, value_item in enumerate(self.spec.env):
1092
+ if get_item_name(value_item) == name:
1093
+ self.spec.env[env_index] = new_var
1093
1094
  return self
1094
- i += 1
1095
1095
  self.spec.env.append(new_var)
1096
1096
  return self
1097
1097
 
mlrun/serving/states.py CHANGED
@@ -19,7 +19,7 @@ import pathlib
19
19
  import traceback
20
20
  from copy import copy, deepcopy
21
21
  from inspect import getfullargspec, signature
22
- from typing import Union
22
+ from typing import Any, Union
23
23
 
24
24
  import mlrun
25
25
 
@@ -327,7 +327,7 @@ class BaseStep(ModelObj):
327
327
  parent = self._parent
328
328
  else:
329
329
  raise GraphError(
330
- f"step {self.name} parent is not set or its not part of a graph"
330
+ f"step {self.name} parent is not set or it's not part of a graph"
331
331
  )
332
332
 
333
333
  name, step = params_to_step(
@@ -349,6 +349,36 @@ class BaseStep(ModelObj):
349
349
  parent._last_added = step
350
350
  return step
351
351
 
352
+ def set_flow(
353
+ self,
354
+ steps: list[Union[str, StepToDict, dict[str, Any]]],
355
+ force: bool = False,
356
+ ):
357
+ """set list of steps as downstream from this step, in the order specified. This will overwrite any existing
358
+ downstream steps.
359
+
360
+ :param steps: list of steps to follow this one
361
+ :param force: whether to overwrite existing downstream steps. If False, this method will fail if any downstream
362
+ steps have already been defined. Defaults to False.
363
+ :return: the last step added to the flow
364
+
365
+ example:
366
+ The below code sets the downstream nodes of step1 by using a list of steps (provided to `set_flow()`) and a
367
+ single step (provided to `to()`), resulting in the graph (step1 -> step2 -> step3 -> step4).
368
+ Notice that using `force=True` is required in case step1 already had downstream nodes (e.g. if the existing
369
+ graph is step1 -> step2_old) and that following the execution of this code the existing downstream steps
370
+ are removed. If the intention is to split the graph (and not to overwrite), please use `to()`.
371
+
372
+ step1.set_flow(
373
+ [
374
+ dict(name="step2", handler="step2_handler"),
375
+ dict(name="step3", class_name="Step3Class"),
376
+ ],
377
+ force=True,
378
+ ).to(dict(name="step4", class_name="Step4Class"))
379
+ """
380
+ raise NotImplementedError("set_flow() can only be called on a FlowStep")
381
+
352
382
 
353
383
  class TaskStep(BaseStep):
354
384
  """task execution step, runs a class or handler"""
@@ -1258,6 +1288,27 @@ class FlowStep(BaseStep):
1258
1288
  )
1259
1289
  self[step_name].after_step(name)
1260
1290
 
1291
+ def set_flow(
1292
+ self,
1293
+ steps: list[Union[str, StepToDict, dict[str, Any]]],
1294
+ force: bool = False,
1295
+ ):
1296
+ if not force and self.steps:
1297
+ raise mlrun.errors.MLRunInvalidArgumentError(
1298
+ "set_flow() called on a step that already has downstream steps. "
1299
+ "If you want to overwrite existing steps, set force=True."
1300
+ )
1301
+
1302
+ self.steps = None
1303
+ step = self
1304
+ for next_step in steps:
1305
+ if isinstance(next_step, dict):
1306
+ step = step.to(**next_step)
1307
+ else:
1308
+ step = step.to(next_step)
1309
+
1310
+ return step
1311
+
1261
1312
 
1262
1313
  class RootFlowStep(FlowStep):
1263
1314
  """root flow step"""
@@ -72,12 +72,7 @@ class SlackNotification(NotificationBase):
72
72
  event_data: mlrun.common.schemas.Event = None,
73
73
  ) -> dict:
74
74
  data = {
75
- "blocks": [
76
- {
77
- "type": "header",
78
- "text": {"type": "plain_text", "text": f"[{severity}] {message}"},
79
- },
80
- ]
75
+ "blocks": self._generate_slack_header_blocks(severity, message),
81
76
  }
82
77
  if self.name:
83
78
  data["blocks"].append(
@@ -106,6 +101,32 @@ class SlackNotification(NotificationBase):
106
101
 
107
102
  return data
108
103
 
104
+ def _generate_slack_header_blocks(self, severity: str, message: str):
105
+ header_text = block_text = f"[{severity}] {message}"
106
+ section_text = None
107
+
108
+ # Slack doesn't allow headers to be longer than 150 characters
109
+ # If there's a comma in the message, split the message at the comma
110
+ # Otherwise, split the message at 150 characters
111
+ if len(block_text) > 150:
112
+ if ", " in block_text and block_text.index(", ") < 149:
113
+ header_text = block_text.split(",")[0]
114
+ section_text = block_text[len(header_text) + 2 :]
115
+ else:
116
+ header_text = block_text[:150]
117
+ section_text = block_text[150:]
118
+ blocks = [
119
+ {"type": "header", "text": {"type": "plain_text", "text": header_text}}
120
+ ]
121
+ if section_text:
122
+ blocks.append(
123
+ {
124
+ "type": "section",
125
+ "text": self._get_slack_row(section_text),
126
+ }
127
+ )
128
+ return blocks
129
+
109
130
  def _get_alert_fields(
110
131
  self,
111
132
  alert: mlrun.common.schemas.AlertConfig,