mlrun 1.8.0rc20__py3-none-any.whl → 1.8.0rc24__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.

@@ -29,11 +29,14 @@ import mlrun.model_monitoring.db
29
29
  import mlrun.serving.states
30
30
  import mlrun.utils
31
31
  from mlrun.common.schemas.model_monitoring.constants import (
32
+ ControllerEvent,
33
+ ControllerEventKind,
32
34
  EndpointType,
33
35
  EventFieldType,
34
36
  FileTargetKind,
35
37
  ProjectSecretKeys,
36
38
  )
39
+ from mlrun.datastore import parse_kafka_url
37
40
  from mlrun.model_monitoring.db import TSDBConnector
38
41
  from mlrun.utils import logger
39
42
 
@@ -88,7 +91,9 @@ class EventStreamProcessor:
88
91
  self.v3io_framesd = v3io_framesd or mlrun.mlconf.v3io_framesd
89
92
  self.v3io_api = v3io_api or mlrun.mlconf.v3io_api
90
93
 
91
- self.v3io_access_key = v3io_access_key or os.environ.get("V3IO_ACCESS_KEY")
94
+ self.v3io_access_key = v3io_access_key or mlrun.get_secret_or_env(
95
+ "V3IO_ACCESS_KEY"
96
+ )
92
97
  self.model_monitoring_access_key = (
93
98
  model_monitoring_access_key
94
99
  or os.environ.get(ProjectSecretKeys.ACCESS_KEY)
@@ -118,6 +123,7 @@ class EventStreamProcessor:
118
123
  self,
119
124
  fn: mlrun.runtimes.ServingRuntime,
120
125
  tsdb_connector: TSDBConnector,
126
+ controller_stream_uri: str,
121
127
  ) -> None:
122
128
  """
123
129
  Apply monitoring serving graph to a given serving function. The following serving graph includes about 4 main
@@ -146,6 +152,8 @@ class EventStreamProcessor:
146
152
 
147
153
  :param fn: A serving function.
148
154
  :param tsdb_connector: Time series database connector.
155
+ :param controller_stream_uri: The controller stream URI. Runs on server api pod so needed to be provided as
156
+ input
149
157
  """
150
158
 
151
159
  graph = typing.cast(
@@ -209,6 +217,20 @@ class EventStreamProcessor:
209
217
  )
210
218
 
211
219
  apply_map_feature_names()
220
+ # split the graph between event with error vs valid event
221
+ graph.add_step(
222
+ "storey.Filter",
223
+ "FilterNOP",
224
+ after="MapFeatureNames",
225
+ _fn="(event.get('kind', " ") != 'nop_event')",
226
+ )
227
+ graph.add_step(
228
+ "storey.Filter",
229
+ "ForwardNOP",
230
+ after="MapFeatureNames",
231
+ _fn="(event.get('kind', " ") == 'nop_event')",
232
+ )
233
+
212
234
  tsdb_connector.apply_monitoring_stream_steps(
213
235
  graph=graph,
214
236
  aggregate_windows=self.aggregate_windows,
@@ -221,7 +243,7 @@ class EventStreamProcessor:
221
243
  graph.add_step(
222
244
  "ProcessBeforeParquet",
223
245
  name="ProcessBeforeParquet",
224
- after="MapFeatureNames",
246
+ after="FilterNOP",
225
247
  _fn="(event)",
226
248
  )
227
249
 
@@ -248,6 +270,44 @@ class EventStreamProcessor:
248
270
 
249
271
  apply_parquet_target()
250
272
 
273
+ # controller branch
274
+ def apply_push_controller_stream(stream_uri: str):
275
+ if stream_uri.startswith("v3io://"):
276
+ graph.add_step(
277
+ ">>",
278
+ "controller_stream_v3io",
279
+ path=stream_uri,
280
+ sharding_func=ControllerEvent.ENDPOINT_ID,
281
+ access_key=self.v3io_access_key,
282
+ after="ForwardNOP",
283
+ )
284
+ elif stream_uri.startswith("kafka://"):
285
+ topic, brokers = parse_kafka_url(stream_uri)
286
+ logger.info(
287
+ "Controller stream uri for kafka",
288
+ stream_uri=stream_uri,
289
+ topic=topic,
290
+ brokers=brokers,
291
+ )
292
+ if isinstance(brokers, list):
293
+ path = f"kafka://{brokers[0]}/{topic}"
294
+ elif isinstance(brokers, str):
295
+ path = f"kafka://{brokers}/{topic}"
296
+ else:
297
+ raise mlrun.errors.MLRunInvalidArgumentError(
298
+ "Brokers must be a list or str check controller stream uri"
299
+ )
300
+ graph.add_step(
301
+ ">>",
302
+ "controller_stream_kafka",
303
+ path=path,
304
+ kafka_brokers=brokers,
305
+ _sharding_func="kafka_sharding_func", # TODO: remove this when storey handle str key
306
+ after="ForwardNOP",
307
+ )
308
+
309
+ apply_push_controller_stream(controller_stream_uri)
310
+
251
311
 
252
312
  class ProcessBeforeParquet(mlrun.feature_store.steps.MapClass):
253
313
  def __init__(self, **kwargs):
@@ -321,6 +381,9 @@ class ProcessEndpointEvent(mlrun.feature_store.steps.MapClass):
321
381
 
322
382
  def do(self, full_event):
323
383
  event = full_event.body
384
+ if event.get(ControllerEvent.KIND, "") == ControllerEventKind.NOP_EVENT:
385
+ logger.info("Skipped nop event inside of ProcessEndpointEvent", event=event)
386
+ return storey.Event(body=[event])
324
387
  # Getting model version and function uri from event
325
388
  # and use them for retrieving the endpoint_id
326
389
  function_uri = full_event.body.get(EventFieldType.FUNCTION_URI)
@@ -589,6 +652,9 @@ class MapFeatureNames(mlrun.feature_store.steps.MapClass):
589
652
  return None
590
653
 
591
654
  def do(self, event: dict):
655
+ if event.get(ControllerEvent.KIND, "") == ControllerEventKind.NOP_EVENT:
656
+ logger.info("Skipped nop event inside of MapFeatureNames", event=event)
657
+ return event
592
658
  endpoint_id = event[EventFieldType.ENDPOINT_ID]
593
659
 
594
660
  feature_values = event[EventFieldType.FEATURES]
@@ -827,3 +893,7 @@ def update_monitoring_feature_set(
827
893
  )
828
894
 
829
895
  monitoring_feature_set.save()
896
+
897
+
898
+ def kafka_sharding_func(event):
899
+ return event.body[ControllerEvent.ENDPOINT_ID].encode("UTF-8")
mlrun/projects/project.py CHANGED
@@ -29,6 +29,7 @@ import zipfile
29
29
  from copy import deepcopy
30
30
  from os import environ, makedirs, path
31
31
  from typing import Callable, Optional, Union, cast
32
+ from urllib.parse import urlparse
32
33
 
33
34
  import dotenv
34
35
  import git
@@ -1964,17 +1965,17 @@ class MlrunProject(ModelObj):
1964
1965
  ... )
1965
1966
 
1966
1967
  """
1968
+ document_loader_spec = document_loader_spec or DocumentLoaderSpec()
1967
1969
  if not document_loader_spec.download_object and upload:
1968
1970
  raise ValueError(
1969
- "This document loader expects direct links/URLs and does not support file uploads. "
1970
- "Either set download_object=True or set upload=False"
1971
+ "The document loader is configured to not support downloads but the upload flag is set to True."
1972
+ "Either set loader.download_object=True or set upload=False"
1971
1973
  )
1972
1974
  doc_artifact = DocumentArtifact(
1973
1975
  key=key,
1974
1976
  original_source=local_path or target_path,
1975
- document_loader_spec=document_loader_spec
1976
- if document_loader_spec
1977
- else DocumentLoaderSpec(),
1977
+ document_loader_spec=document_loader_spec,
1978
+ collections=kwargs.pop("collections", None),
1978
1979
  **kwargs,
1979
1980
  )
1980
1981
  return self.log_artifact(
@@ -3608,9 +3609,12 @@ class MlrunProject(ModelObj):
3608
3609
  def set_model_monitoring_credentials(
3609
3610
  self,
3610
3611
  access_key: Optional[str] = None,
3611
- stream_path: Optional[str] = None,
3612
- tsdb_connection: Optional[str] = None,
3612
+ stream_path: Optional[str] = None, # Deprecated
3613
+ tsdb_connection: Optional[str] = None, # Deprecated
3613
3614
  replace_creds: bool = False,
3615
+ *,
3616
+ stream_profile_name: Optional[str] = None,
3617
+ tsdb_profile_name: Optional[str] = None,
3614
3618
  ):
3615
3619
  """
3616
3620
  Set the credentials that will be used by the project's model monitoring
@@ -3622,50 +3626,109 @@ class MlrunProject(ModelObj):
3622
3626
  * None - will be set from the system configuration.
3623
3627
  * v3io - for v3io endpoint store, pass `v3io` and the system will generate the
3624
3628
  exact path.
3625
- :param stream_path: Path to the model monitoring stream. By default, None. Options:
3626
-
3627
- * None - will be set from the system configuration.
3628
- * v3io - for v3io stream, pass `v3io` and the system will generate the exact
3629
- path.
3630
- * Kafka - for Kafka stream, provide the full connection string without custom
3631
- topic, for example kafka://<some_kafka_broker>:<port>.
3632
- :param tsdb_connection: Connection string to the time series database. By default, None.
3629
+ :param stream_path: (Deprecated) This argument is deprecated. Use ``stream_profile_name`` instead.
3630
+ Path to the model monitoring stream. By default, None. Options:
3631
+
3632
+ * ``"v3io"`` - for v3io stream, pass ``"v3io"`` and the system will generate
3633
+ the exact path.
3634
+ * Kafka - for Kafka stream, provide the full connection string without acustom
3635
+ topic, for example ``"kafka://<some_kafka_broker>:<port>"``.
3636
+ :param tsdb_connection: (Deprecated) Connection string to the time series database. By default, None.
3633
3637
  Options:
3634
3638
 
3635
- * None - will be set from the system configuration.
3636
- * v3io - for v3io stream, pass `v3io` and the system will generate the exact
3637
- path.
3639
+ * v3io - for v3io stream, pass ``"v3io"`` and the system will generate the
3640
+ exact path.
3638
3641
  * TDEngine - for TDEngine tsdb, provide the full websocket connection URL,
3639
- for example taosws://<username>:<password>@<host>:<port>.
3642
+ for example ``"taosws://<username>:<password>@<host>:<port>"``.
3640
3643
  :param replace_creds: If True, will override the existing credentials.
3641
3644
  Please keep in mind that if you already enabled model monitoring on
3642
3645
  your project this action can cause data loose and will require redeploying
3643
3646
  all model monitoring functions & model monitoring infra
3644
3647
  & tracked model server.
3648
+ :param stream_profile_name: The datastore profile name of the stream to be used in model monitoring.
3649
+ The supported profiles are:
3650
+
3651
+ * :py:class:`~mlrun.datastore.datastore_profile.DatastoreProfileV3io`
3652
+ * :py:class:`~mlrun.datastore.datastore_profile.DatastoreProfileKafkaSource`
3653
+
3654
+ You need to register one of them, and pass the profile's name.
3655
+ :param tsdb_profile_name: The datastore profile name of the time-series database to be used in model
3656
+ monitoring. The supported profiles are:
3657
+
3658
+ * :py:class:`~mlrun.datastore.datastore_profile.DatastoreProfileV3io`
3659
+ * :py:class:`~mlrun.datastore.datastore_profile.TDEngineDatastoreProfile`
3660
+
3661
+ You need to register one of them, and pass the profile's name.
3645
3662
  """
3646
3663
  db = mlrun.db.get_run_db(secrets=self._secrets)
3647
- if tsdb_connection == "v3io":
3648
- tsdb_profile = mlrun.datastore.datastore_profile.DatastoreProfileV3io(
3649
- name="mm-infra-tsdb"
3664
+
3665
+ if tsdb_connection:
3666
+ warnings.warn(
3667
+ "The `tsdb_connection` argument is deprecated and will be removed in MLRun version 1.8.0. "
3668
+ "Use `tsdb_profile_name` instead.",
3669
+ FutureWarning,
3650
3670
  )
3671
+ if tsdb_profile_name:
3672
+ raise mlrun.errors.MLRunValueError(
3673
+ "If you set `tsdb_profile_name`, you must not pass `tsdb_connection`."
3674
+ )
3675
+ if tsdb_connection == "v3io":
3676
+ tsdb_profile = mlrun.datastore.datastore_profile.DatastoreProfileV3io(
3677
+ name=mm_constants.DefaultProfileName.TSDB
3678
+ )
3679
+ else:
3680
+ parsed_url = urlparse(tsdb_connection)
3681
+ if parsed_url.scheme != "taosws":
3682
+ raise mlrun.errors.MLRunValueError(
3683
+ f"Unsupported `tsdb_connection`: '{tsdb_connection}'."
3684
+ )
3685
+ tsdb_profile = (
3686
+ mlrun.datastore.datastore_profile.TDEngineDatastoreProfile(
3687
+ name=mm_constants.DefaultProfileName.TSDB,
3688
+ user=parsed_url.username,
3689
+ password=parsed_url.password,
3690
+ host=parsed_url.hostname,
3691
+ port=parsed_url.port,
3692
+ )
3693
+ )
3694
+
3651
3695
  self.register_datastore_profile(tsdb_profile)
3652
3696
  tsdb_profile_name = tsdb_profile.name
3653
- else:
3654
- tsdb_profile_name = None
3655
- if stream_path == "v3io":
3656
- stream_profile = mlrun.datastore.datastore_profile.DatastoreProfileV3io(
3657
- name="mm-infra-stream"
3697
+
3698
+ if stream_path:
3699
+ warnings.warn(
3700
+ "The `stream_path` argument is deprecated and will be removed in MLRun version 1.8.0. "
3701
+ "Use `stream_profile_name` instead.",
3702
+ FutureWarning,
3658
3703
  )
3704
+ if stream_profile_name:
3705
+ raise mlrun.errors.MLRunValueError(
3706
+ "If you set `stream_profile_name`, you must not pass `stream_path`."
3707
+ )
3708
+ if stream_path == "v3io":
3709
+ stream_profile = mlrun.datastore.datastore_profile.DatastoreProfileV3io(
3710
+ name=mm_constants.DefaultProfileName.STREAM
3711
+ )
3712
+ else:
3713
+ parsed_stream = urlparse(stream_path)
3714
+ if parsed_stream.scheme != "kafka":
3715
+ raise mlrun.errors.MLRunValueError(
3716
+ f"Unsupported `stream_path`: '{stream_path}'."
3717
+ )
3718
+ stream_profile = (
3719
+ mlrun.datastore.datastore_profile.DatastoreProfileKafkaSource(
3720
+ name=mm_constants.DefaultProfileName.STREAM,
3721
+ brokers=[parsed_stream.netloc],
3722
+ topics=[],
3723
+ )
3724
+ )
3659
3725
  self.register_datastore_profile(stream_profile)
3660
3726
  stream_profile_name = stream_profile.name
3661
- else:
3662
- stream_profile_name = None
3727
+
3663
3728
  db.set_model_monitoring_credentials(
3664
3729
  project=self.name,
3665
3730
  credentials={
3666
3731
  "access_key": access_key,
3667
- "stream_path": stream_path,
3668
- "tsdb_connection": tsdb_connection,
3669
3732
  "tsdb_profile_name": tsdb_profile_name,
3670
3733
  "stream_profile_name": stream_profile_name,
3671
3734
  },
@@ -3676,7 +3739,7 @@ class MlrunProject(ModelObj):
3676
3739
  "Model monitoring credentials were set successfully. "
3677
3740
  "Please keep in mind that if you already had model monitoring functions "
3678
3741
  "/ model monitoring infra / tracked model server "
3679
- "deployed on your project, you will need to redeploy them."
3742
+ "deployed on your project, you will need to redeploy them. "
3680
3743
  "For redeploying the model monitoring infra, please use `enable_model_monitoring` API "
3681
3744
  "and set `rebuild_images=True`"
3682
3745
  )
@@ -688,7 +688,7 @@ class ServingRuntime(RemoteRuntime):
688
688
  "project": self.metadata.project,
689
689
  "version": "v2",
690
690
  "parameters": self.spec.parameters,
691
- "graph": self.spec.graph.to_dict() if self.spec.graph else {},
691
+ "graph": self.spec.graph.to_dict(strip=True) if self.spec.graph else {},
692
692
  "load_mode": self.spec.load_mode,
693
693
  "functions": function_name_uri_map,
694
694
  "graph_initializer": self.spec.graph_initializer,
mlrun/serving/server.py CHANGED
@@ -44,6 +44,8 @@ from ..utils import get_caller_globals
44
44
  from .states import RootFlowStep, RouterStep, get_function, graph_root_setter
45
45
  from .utils import event_id_key, event_path_key
46
46
 
47
+ DUMMY_STREAM = "dummy://"
48
+
47
49
 
48
50
  class _StreamContext:
49
51
  """Handles the stream context for the events stream process. Includes the configuration for the output stream
@@ -72,14 +74,20 @@ class _StreamContext:
72
74
  function_uri, config.default_project
73
75
  )
74
76
 
75
- self.stream_uri = mlrun.model_monitoring.get_stream_path(project=project)
77
+ stream_args = parameters.get("stream_args", {})
78
+
79
+ if log_stream == DUMMY_STREAM:
80
+ # Dummy stream used for testing, see tests/serving/test_serving.py
81
+ self.stream_uri = DUMMY_STREAM
82
+ elif not stream_args.get("mock"): # if not a mock: `context.is_mock = True`
83
+ self.stream_uri = mlrun.model_monitoring.get_stream_path(
84
+ project=project
85
+ )
76
86
 
77
87
  if log_stream:
78
88
  # Update the stream path to the log stream value
79
89
  self.stream_uri = log_stream.format(project=project)
80
90
 
81
- stream_args = parameters.get("stream_args", {})
82
-
83
91
  self.output_stream = get_stream_pusher(self.stream_uri, **stream_args)
84
92
 
85
93
 
mlrun/serving/states.py CHANGED
@@ -31,6 +31,7 @@ import storey.utils
31
31
 
32
32
  import mlrun
33
33
  import mlrun.common.schemas as schemas
34
+ from mlrun.utils import logger
34
35
 
35
36
  from ..config import config
36
37
  from ..datastore import get_stream_pusher
@@ -49,6 +50,8 @@ path_splitter = "/"
49
50
  previous_step = "$prev"
50
51
  queue_class_names = [">>", "$queue"]
51
52
 
53
+ MAX_MODELS_PER_ROUTER = 5000
54
+
52
55
 
53
56
  class GraphError(Exception):
54
57
  """error in graph topology or configuration"""
@@ -86,8 +89,10 @@ _task_step_fields = [
86
89
  "endpoint_type",
87
90
  ]
88
91
 
89
-
90
- MAX_ALLOWED_STEPS = 4500
92
+ _default_fields_to_strip_from_step = [
93
+ "model_endpoint_creation_strategy",
94
+ "endpoint_type",
95
+ ]
91
96
 
92
97
 
93
98
  def new_remote_endpoint(
@@ -110,6 +115,7 @@ class BaseStep(ModelObj):
110
115
  kind = "BaseStep"
111
116
  default_shape = "ellipse"
112
117
  _dict_fields = ["kind", "comment", "after", "on_error"]
118
+ _default_fields_to_strip = _default_fields_to_strip_from_step
113
119
 
114
120
  def __init__(
115
121
  self,
@@ -625,6 +631,19 @@ class TaskStep(BaseStep):
625
631
  raise exc
626
632
  return event
627
633
 
634
+ def to_dict(
635
+ self,
636
+ fields: Optional[list] = None,
637
+ exclude: Optional[list] = None,
638
+ strip: bool = False,
639
+ ) -> dict:
640
+ self.endpoint_type = (
641
+ self.endpoint_type.value
642
+ if isinstance(self.endpoint_type, schemas.EndpointType)
643
+ else self.endpoint_type
644
+ )
645
+ return super().to_dict(fields, exclude, strip)
646
+
628
647
 
629
648
  class MonitoringApplicationStep(TaskStep):
630
649
  """monitoring application execution step, runs users class code"""
@@ -755,7 +774,7 @@ class RouterStep(TaskStep):
755
774
  creation_strategy: schemas.ModelEndpointCreationStrategy = schemas.ModelEndpointCreationStrategy.INPLACE,
756
775
  **class_args,
757
776
  ):
758
- """add child route step or class to the router
777
+ """add child route step or class to the router, if key exists it will be updated
759
778
 
760
779
  :param key: unique name (and route path) for the child step
761
780
  :param route: child step object (Task, ..)
@@ -775,7 +794,13 @@ class RouterStep(TaskStep):
775
794
  2. Create a new model endpoint with the same name and set it to `latest`.
776
795
 
777
796
  """
778
-
797
+ if len(self.routes.keys()) >= MAX_MODELS_PER_ROUTER and key not in self.routes:
798
+ raise mlrun.errors.MLRunModelLimitExceededError(
799
+ f"Router cannot support more than {MAX_MODELS_PER_ROUTER} model endpoints. "
800
+ f"To add a new route, edit an existing one by passing the same key."
801
+ )
802
+ if key in self.routes:
803
+ logger.info(f"Model {key} already exists, updating it.")
779
804
  if not route and not class_name and not handler:
780
805
  raise MLRunInvalidArgumentError("route or class_name must be specified")
781
806
  if not route:
@@ -790,10 +815,6 @@ class RouterStep(TaskStep):
790
815
  )
791
816
  route.function = function or route.function
792
817
 
793
- if len(self._routes) >= MAX_ALLOWED_STEPS:
794
- raise mlrun.errors.MLRunInvalidArgumentError(
795
- f"Cannot create the serving graph: the maximum number of steps is {MAX_ALLOWED_STEPS}"
796
- )
797
818
  route = self._routes.update(key, route)
798
819
  route.set_parent(self)
799
820
  return route
@@ -806,6 +827,10 @@ class RouterStep(TaskStep):
806
827
  del self._routes[key]
807
828
 
808
829
  def init_object(self, context, namespace, mode="sync", reset=False, **extra_kwargs):
830
+ if not self.routes:
831
+ raise mlrun.errors.MLRunRuntimeError(
832
+ "You have to add models to the router step before initializing it"
833
+ )
809
834
  if not self._is_local_function(context):
810
835
  return
811
836
 
@@ -412,8 +412,17 @@ class NotificationPusher(_NotificationPusherBase):
412
412
  sent_time: typing.Optional[datetime.datetime] = None,
413
413
  reason: typing.Optional[str] = None,
414
414
  ):
415
- if run_state not in runtimes_constants.RunStates.terminal_states():
416
- # we want to update the notification status only if the run is in a terminal state for BC
415
+ # Skip update the notification state if the following conditions are met:
416
+ # 1. the run is not in a terminal state
417
+ # 2. the when contains only one state (which is the current state)
418
+ # Skip updating because currently each notification has only one row in the db, even if it has multiple when.
419
+ # This means that if the notification is updated to sent for running state for example, it will not send for
420
+ # The terminal state
421
+ # TODO: Change this behavior after implementing ML-8723
422
+ if (
423
+ run_state not in runtimes_constants.RunStates.terminal_states()
424
+ and len(notification.when) > 1
425
+ ):
417
426
  logger.debug(
418
427
  "Skip updating notification status - run not in terminal state",
419
428
  run_uid=run_uid,
@@ -1,4 +1,4 @@
1
1
  {
2
- "git_commit": "91ab2fb641a35e93a5056d93c55b98b96d74ebc1",
3
- "version": "1.8.0-rc20"
2
+ "git_commit": "f1df8f0da3910eb0d0603cb299703b00e79947f4",
3
+ "version": "1.8.0-rc24"
4
4
  }
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: mlrun
3
- Version: 1.8.0rc20
3
+ Version: 1.8.0rc24
4
4
  Summary: Tracking and config of machine learning runs
5
5
  Home-page: https://github.com/mlrun/mlrun
6
6
  Author: Yaron Haviv
@@ -225,6 +225,18 @@ Requires-Dist: taos-ws-py==0.3.2; extra == "complete-api"
225
225
  Requires-Dist: taoswswrap~=0.3.0; extra == "complete-api"
226
226
  Requires-Dist: timelength~=1.1; extra == "complete-api"
227
227
  Requires-Dist: uvicorn~=0.32.1; extra == "complete-api"
228
+ Dynamic: author
229
+ Dynamic: author-email
230
+ Dynamic: classifier
231
+ Dynamic: description
232
+ Dynamic: description-content-type
233
+ Dynamic: home-page
234
+ Dynamic: keywords
235
+ Dynamic: license
236
+ Dynamic: provides-extra
237
+ Dynamic: requires-dist
238
+ Dynamic: requires-python
239
+ Dynamic: summary
228
240
 
229
241
  <a id="top"></a>
230
242
  [![Build Status](https://github.com/mlrun/mlrun/actions/workflows/build.yaml/badge.svg?branch=development)](https://github.com/mlrun/mlrun/actions/workflows/build.yaml?query=branch%3Adevelopment)