mlrun 1.7.0rc4__py3-none-any.whl → 1.7.0rc6__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 (47) hide show
  1. mlrun/artifacts/base.py +2 -1
  2. mlrun/artifacts/plots.py +9 -5
  3. mlrun/common/constants.py +1 -0
  4. mlrun/common/schemas/__init__.py +10 -0
  5. mlrun/common/schemas/api_gateway.py +85 -0
  6. mlrun/common/schemas/auth.py +2 -2
  7. mlrun/config.py +19 -4
  8. mlrun/datastore/sources.py +5 -4
  9. mlrun/datastore/targets.py +16 -20
  10. mlrun/db/base.py +16 -0
  11. mlrun/db/factory.py +1 -1
  12. mlrun/db/httpdb.py +50 -8
  13. mlrun/db/nopdb.py +13 -0
  14. mlrun/launcher/__init__.py +1 -1
  15. mlrun/launcher/base.py +1 -1
  16. mlrun/launcher/client.py +1 -1
  17. mlrun/launcher/factory.py +1 -1
  18. mlrun/launcher/local.py +1 -1
  19. mlrun/launcher/remote.py +1 -1
  20. mlrun/model_monitoring/api.py +6 -12
  21. mlrun/model_monitoring/application.py +21 -21
  22. mlrun/model_monitoring/applications/histogram_data_drift.py +130 -40
  23. mlrun/model_monitoring/batch.py +1 -42
  24. mlrun/model_monitoring/controller.py +1 -8
  25. mlrun/model_monitoring/features_drift_table.py +34 -22
  26. mlrun/model_monitoring/helpers.py +45 -4
  27. mlrun/model_monitoring/stream_processing.py +2 -0
  28. mlrun/projects/project.py +229 -16
  29. mlrun/run.py +70 -74
  30. mlrun/runtimes/__init__.py +35 -0
  31. mlrun/runtimes/base.py +15 -11
  32. mlrun/runtimes/nuclio/__init__.py +1 -0
  33. mlrun/runtimes/nuclio/api_gateway.py +300 -0
  34. mlrun/runtimes/nuclio/application/__init__.py +15 -0
  35. mlrun/runtimes/nuclio/application/application.py +283 -0
  36. mlrun/runtimes/nuclio/application/reverse_proxy.go +87 -0
  37. mlrun/runtimes/nuclio/function.py +50 -1
  38. mlrun/runtimes/pod.py +1 -1
  39. mlrun/serving/states.py +7 -19
  40. mlrun/utils/logger.py +2 -2
  41. mlrun/utils/version/version.json +2 -2
  42. {mlrun-1.7.0rc4.dist-info → mlrun-1.7.0rc6.dist-info}/METADATA +1 -1
  43. {mlrun-1.7.0rc4.dist-info → mlrun-1.7.0rc6.dist-info}/RECORD +47 -42
  44. {mlrun-1.7.0rc4.dist-info → mlrun-1.7.0rc6.dist-info}/WHEEL +1 -1
  45. {mlrun-1.7.0rc4.dist-info → mlrun-1.7.0rc6.dist-info}/LICENSE +0 -0
  46. {mlrun-1.7.0rc4.dist-info → mlrun-1.7.0rc6.dist-info}/entry_points.txt +0 -0
  47. {mlrun-1.7.0rc4.dist-info → mlrun-1.7.0rc6.dist-info}/top_level.txt +0 -0
mlrun/projects/project.py CHANGED
@@ -41,6 +41,7 @@ import mlrun.db
41
41
  import mlrun.errors
42
42
  import mlrun.k8s_utils
43
43
  import mlrun.runtimes
44
+ import mlrun.runtimes.nuclio.api_gateway
44
45
  import mlrun.runtimes.pod
45
46
  import mlrun.runtimes.utils
46
47
  import mlrun.utils.regex
@@ -1374,14 +1375,7 @@ class MlrunProject(ModelObj):
1374
1375
  artifact_path = mlrun.utils.helpers.template_artifact_path(
1375
1376
  self.spec.artifact_path or mlrun.mlconf.artifact_path, self.metadata.name
1376
1377
  )
1377
- # TODO: To correctly maintain the list of artifacts from an exported project,
1378
- # we need to maintain the different trees that generated them
1379
- producer = ArtifactProducer(
1380
- "project",
1381
- self.metadata.name,
1382
- self.metadata.name,
1383
- tag=self._get_hexsha() or str(uuid.uuid4()),
1384
- )
1378
+ project_tag = self._get_project_tag()
1385
1379
  for artifact_dict in self.spec.artifacts:
1386
1380
  if _is_imported_artifact(artifact_dict):
1387
1381
  import_from = artifact_dict["import_from"]
@@ -1401,6 +1395,15 @@ class MlrunProject(ModelObj):
1401
1395
  artifact.src_path = path.join(
1402
1396
  self.spec.get_code_path(), artifact.src_path
1403
1397
  )
1398
+ producer = self._resolve_artifact_producer(artifact, project_tag)
1399
+ # log the artifact only if it doesn't already exist
1400
+ if (
1401
+ producer.name != self.metadata.name
1402
+ and self._resolve_existing_artifact(
1403
+ artifact,
1404
+ )
1405
+ ):
1406
+ continue
1404
1407
  artifact_manager.log_artifact(
1405
1408
  producer, artifact, artifact_path=artifact_path
1406
1409
  )
@@ -1497,12 +1500,20 @@ class MlrunProject(ModelObj):
1497
1500
  artifact_path = mlrun.utils.helpers.template_artifact_path(
1498
1501
  artifact_path, self.metadata.name
1499
1502
  )
1500
- producer = ArtifactProducer(
1501
- "project",
1502
- self.metadata.name,
1503
- self.metadata.name,
1504
- tag=self._get_hexsha() or str(uuid.uuid4()),
1505
- )
1503
+ producer = self._resolve_artifact_producer(item)
1504
+ if producer.name != self.metadata.name:
1505
+ # the artifact producer is retained, log it only if it doesn't already exist
1506
+ if existing_artifact := self._resolve_existing_artifact(
1507
+ item,
1508
+ tag,
1509
+ ):
1510
+ artifact_key = item if isinstance(item, str) else item.key
1511
+ logger.info(
1512
+ "Artifact already exists, skipping logging",
1513
+ key=artifact_key,
1514
+ tag=tag,
1515
+ )
1516
+ return existing_artifact
1506
1517
  item = am.log_artifact(
1507
1518
  producer,
1508
1519
  item,
@@ -2402,13 +2413,47 @@ class MlrunProject(ModelObj):
2402
2413
  clone_zip(url, self.spec.context, self._secrets)
2403
2414
 
2404
2415
  def create_remote(self, url, name="origin", branch=None):
2405
- """create remote for the project git
2416
+ """Create remote for the project git
2417
+
2418
+ This method creates a new remote repository associated with the project's Git repository.
2419
+ If a remote with the specified name already exists, it will not be overwritten.
2420
+
2421
+ If you wish to update the URL of an existing remote, use the `set_remote` method instead.
2406
2422
 
2407
2423
  :param url: remote git url
2408
2424
  :param name: name for the remote (default is 'origin')
2409
2425
  :param branch: Git branch to use as source
2410
2426
  """
2427
+ self.set_remote(url, name=name, branch=branch, overwrite=False)
2428
+
2429
+ def set_remote(self, url, name="origin", branch=None, overwrite=True):
2430
+ """Create or update a remote for the project git repository.
2431
+
2432
+ This method allows you to manage remote repositories associated with the project.
2433
+ It checks if a remote with the specified name already exists.
2434
+
2435
+ If a remote with the same name does not exist, it will be created.
2436
+ If a remote with the same name already exists,
2437
+ the behavior depends on the value of the 'overwrite' flag.
2438
+
2439
+ :param url: remote git url
2440
+ :param name: name for the remote (default is 'origin')
2441
+ :param branch: Git branch to use as source
2442
+ :param overwrite: if True (default), updates the existing remote with the given URL if it already exists.
2443
+ if False, raises an error when attempting to create a remote with a name that already exists.
2444
+ :raises MLRunConflictError: If a remote with the same name already exists and overwrite
2445
+ is set to False.
2446
+ """
2411
2447
  self._ensure_git_repo()
2448
+ if self._remote_exists(name):
2449
+ if overwrite:
2450
+ self.spec.repo.delete_remote(name)
2451
+ else:
2452
+ raise mlrun.errors.MLRunConflictError(
2453
+ f"Remote '{name}' already exists in the project, "
2454
+ f"each remote in the project must have a unique name."
2455
+ "Use 'set_remote' with 'override=True' inorder to update the remote, or choose a different name."
2456
+ )
2412
2457
  self.spec.repo.create_remote(name, url=url)
2413
2458
  url = url.replace("https://", "git://")
2414
2459
  if not branch:
@@ -2421,6 +2466,22 @@ class MlrunProject(ModelObj):
2421
2466
  self.spec._source = self.spec.source or url
2422
2467
  self.spec.origin_url = self.spec.origin_url or url
2423
2468
 
2469
+ def remove_remote(self, name):
2470
+ """Remove a remote from the project's Git repository.
2471
+
2472
+ This method removes the remote repository associated with the specified name from the project's Git repository.
2473
+
2474
+ :param name: Name of the remote to remove.
2475
+ """
2476
+ if self._remote_exists(name):
2477
+ self.spec.repo.delete_remote(name)
2478
+ else:
2479
+ logger.warning(f"The remote '{name}' does not exist. Nothing to remove.")
2480
+
2481
+ def _remote_exists(self, name):
2482
+ """Check if a remote with the given name already exists"""
2483
+ return any(remote.name == name for remote in self.spec.repo.remotes)
2484
+
2424
2485
  def _ensure_git_repo(self):
2425
2486
  if self.spec.repo:
2426
2487
  return
@@ -3332,7 +3393,12 @@ class MlrunProject(ModelObj):
3332
3393
  artifact = db.read_artifact(
3333
3394
  key, tag, iter=iter, project=self.metadata.name, tree=tree
3334
3395
  )
3335
- return dict_to_artifact(artifact)
3396
+
3397
+ # in tests, if an artifact is not found, the db returns None
3398
+ # in real usage, the db should raise an exception
3399
+ if artifact:
3400
+ return dict_to_artifact(artifact)
3401
+ return None
3336
3402
 
3337
3403
  def list_artifacts(
3338
3404
  self,
@@ -3625,6 +3691,64 @@ class MlrunProject(ModelObj):
3625
3691
  """
3626
3692
  self.spec.remove_custom_packager(packager=packager)
3627
3693
 
3694
+ def store_api_gateway(
3695
+ self, api_gateway: mlrun.runtimes.nuclio.api_gateway.APIGateway
3696
+ ) -> mlrun.runtimes.nuclio.api_gateway.APIGateway:
3697
+ """
3698
+ Creates or updates a Nuclio API Gateway using the provided APIGateway object.
3699
+
3700
+ This method interacts with the MLRun service to create/update a Nuclio API Gateway based on the provided
3701
+ APIGateway object. Once done, it returns the updated APIGateway object containing all fields propagated
3702
+ on MLRun and Nuclio sides, such as the 'host' attribute.
3703
+ Nuclio docs here: https://docs.nuclio.io/en/latest/reference/api-gateway/http.html
3704
+
3705
+ :param api_gateway: An instance of :py:class:`~mlrun.runtimes.nuclio.APIGateway` representing the configuration
3706
+ of the API Gateway to be created
3707
+
3708
+ @return: An instance of :py:class:`~mlrun.runtimes.nuclio.APIGateway` with all fields populated based on the
3709
+ information retrieved from the Nuclio API
3710
+ """
3711
+
3712
+ api_gateway_json = mlrun.db.get_run_db().store_api_gateway(
3713
+ api_gateway=api_gateway,
3714
+ project=self.name,
3715
+ )
3716
+
3717
+ if api_gateway_json:
3718
+ # fill in all the fields in the user's api_gateway object
3719
+ api_gateway = mlrun.runtimes.nuclio.api_gateway.APIGateway.from_scheme(
3720
+ api_gateway_json
3721
+ )
3722
+ return api_gateway
3723
+
3724
+ def list_api_gateways(self) -> list[mlrun.runtimes.nuclio.api_gateway.APIGateway]:
3725
+ """
3726
+ Retrieves a list of Nuclio API gateways associated with the project.
3727
+
3728
+ @return: List of :py:class:`~mlrun.runtimes.nuclio.api_gateway.APIGateway` objects representing
3729
+ the Nuclio API gateways associated with the project.
3730
+ """
3731
+ gateways_list = mlrun.db.get_run_db().list_api_gateways(self.name)
3732
+ return [
3733
+ mlrun.runtimes.nuclio.api_gateway.APIGateway.from_scheme(gateway_dict)
3734
+ for gateway_dict in gateways_list.api_gateways.values()
3735
+ ]
3736
+
3737
+ def get_api_gateway(
3738
+ self,
3739
+ name: str,
3740
+ ) -> mlrun.runtimes.nuclio.api_gateway.APIGateway:
3741
+ """
3742
+ Retrieves an API gateway by name instance.
3743
+
3744
+ :param name: The name of the API gateway to retrieve.
3745
+
3746
+ Returns:
3747
+ mlrun.runtimes.nuclio.APIGateway: An instance of APIGateway.
3748
+ """
3749
+
3750
+ return mlrun.db.get_run_db().get_api_gateway(name=name, project=self.name)
3751
+
3628
3752
  def _run_authenticated_git_action(
3629
3753
  self,
3630
3754
  action: Callable,
@@ -3702,6 +3826,83 @@ class MlrunProject(ModelObj):
3702
3826
  f"<project.spec.get_code_path()>/<{param_name}>)."
3703
3827
  )
3704
3828
 
3829
+ def _resolve_artifact_producer(
3830
+ self,
3831
+ artifact: typing.Union[str, Artifact],
3832
+ project_producer_tag: str = None,
3833
+ ) -> typing.Optional[ArtifactProducer]:
3834
+ """
3835
+ Resolve the artifact producer of the given artifact.
3836
+ If the artifact's producer is a run, the artifact is registered with the original producer.
3837
+ Otherwise, the artifact is registered with the current project as the producer.
3838
+
3839
+ :param artifact: The artifact to resolve its producer.
3840
+ :param project_producer_tag: The tag to use for the project as the producer. If not provided, a tag will be
3841
+ generated for the project.
3842
+ :return: A tuple of the resolved producer and the resolved artifact.
3843
+ """
3844
+
3845
+ if not isinstance(artifact, str) and artifact.producer:
3846
+ # if the artifact was imported from a yaml file, the producer can be a dict
3847
+ if isinstance(artifact.spec.producer, ArtifactProducer):
3848
+ producer_dict = artifact.spec.producer.get_meta()
3849
+ else:
3850
+ producer_dict = artifact.spec.producer
3851
+
3852
+ if producer_dict.get("kind", "") == "run":
3853
+ return ArtifactProducer(
3854
+ name=producer_dict.get("name", ""),
3855
+ kind=producer_dict.get("kind", ""),
3856
+ project=producer_dict.get("project", ""),
3857
+ tag=producer_dict.get("tag", ""),
3858
+ )
3859
+
3860
+ # do not retain the artifact's producer, replace it with the project as the producer
3861
+ project_producer_tag = project_producer_tag or self._get_project_tag()
3862
+ return ArtifactProducer(
3863
+ kind="project",
3864
+ name=self.metadata.name,
3865
+ project=self.metadata.name,
3866
+ tag=project_producer_tag,
3867
+ )
3868
+
3869
+ def _resolve_existing_artifact(
3870
+ self,
3871
+ item: typing.Union[str, Artifact],
3872
+ tag: str = None,
3873
+ ) -> typing.Optional[Artifact]:
3874
+ """
3875
+ Check if there is and existing artifact with the given item and tag.
3876
+ If there is, return the existing artifact. Otherwise, return None.
3877
+
3878
+ :param item: The item (or key) to check if there is an existing artifact for.
3879
+ :param tag: The tag to check if there is an existing artifact for.
3880
+ :return: The existing artifact if there is one, otherwise None.
3881
+ """
3882
+ try:
3883
+ if isinstance(item, str):
3884
+ existing_artifact = self.get_artifact(key=item, tag=tag)
3885
+ else:
3886
+ existing_artifact = self.get_artifact(
3887
+ key=item.key,
3888
+ tag=item.tag,
3889
+ iter=item.iter,
3890
+ tree=item.tree,
3891
+ )
3892
+ if existing_artifact is not None:
3893
+ return existing_artifact.from_dict(existing_artifact)
3894
+ except mlrun.errors.MLRunNotFoundError:
3895
+ logger.debug(
3896
+ "No existing artifact was found",
3897
+ key=item if isinstance(item, str) else item.key,
3898
+ tag=tag if isinstance(item, str) else item.tag,
3899
+ tree=None if isinstance(item, str) else item.tree,
3900
+ )
3901
+ return None
3902
+
3903
+ def _get_project_tag(self):
3904
+ return self._get_hexsha() or str(uuid.uuid4())
3905
+
3705
3906
 
3706
3907
  def _set_as_current_default_project(project: MlrunProject):
3707
3908
  mlrun.mlconf.default_project = project.metadata.name
@@ -3787,6 +3988,18 @@ def _init_function_from_dict(
3787
3988
  tag=tag,
3788
3989
  )
3789
3990
 
3991
+ elif image and kind in mlrun.runtimes.RuntimeKinds.nuclio_runtimes():
3992
+ func = new_function(
3993
+ name,
3994
+ command=relative_url,
3995
+ image=image,
3996
+ kind=kind,
3997
+ handler=handler,
3998
+ tag=tag,
3999
+ )
4000
+ if kind != mlrun.runtimes.RuntimeKinds.application:
4001
+ logger.info("Function code not specified, setting entry point to image")
4002
+ func.from_image(image)
3790
4003
  else:
3791
4004
  raise ValueError(f"Unsupported function url:handler {url}:{handler} or no spec")
3792
4005
 
mlrun/run.py CHANGED
@@ -34,7 +34,6 @@ import mlrun.common.schemas
34
34
  import mlrun.errors
35
35
  import mlrun.utils.helpers
36
36
  from mlrun.kfpops import format_summary_from_kfp_run, show_kfp_run
37
- from mlrun.runtimes.nuclio.serving import serving_subkind
38
37
 
39
38
  from .common.helpers import parse_versioned_object_uri
40
39
  from .config import config as mlconf
@@ -58,6 +57,7 @@ from .runtimes import (
58
57
  )
59
58
  from .runtimes.databricks_job.databricks_runtime import DatabricksRuntime
60
59
  from .runtimes.funcdoc import update_function_entry_points
60
+ from .runtimes.nuclio.application import ApplicationRuntime
61
61
  from .runtimes.utils import add_code_metadata, global_context
62
62
  from .utils import (
63
63
  extend_hub_uri_if_needed,
@@ -425,19 +425,19 @@ def import_function_to_dict(url, secrets=None):
425
425
 
426
426
 
427
427
  def new_function(
428
- name: str = "",
429
- project: str = "",
430
- tag: str = "",
431
- kind: str = "",
432
- command: str = "",
433
- image: str = "",
434
- args: list = None,
435
- runtime=None,
436
- mode=None,
437
- handler: str = None,
438
- source: str = None,
428
+ name: Optional[str] = "",
429
+ project: Optional[str] = "",
430
+ tag: Optional[str] = "",
431
+ kind: Optional[str] = "",
432
+ command: Optional[str] = "",
433
+ image: Optional[str] = "",
434
+ args: Optional[list] = None,
435
+ runtime: Optional[Union[mlrun.runtimes.BaseRuntime, dict]] = None,
436
+ mode: Optional[str] = None,
437
+ handler: Optional[str] = None,
438
+ source: Optional[str] = None,
439
439
  requirements: Union[str, list[str]] = None,
440
- kfp=None,
440
+ kfp: Optional[bool] = None,
441
441
  requirements_file: str = "",
442
442
  ):
443
443
  """Create a new ML function from base properties
@@ -535,9 +535,9 @@ def new_function(
535
535
  if source:
536
536
  runner.spec.build.source = source
537
537
  if handler:
538
- if kind == RuntimeKinds.serving:
538
+ if kind in [RuntimeKinds.serving, RuntimeKinds.application]:
539
539
  raise MLRunInvalidArgumentError(
540
- "cannot set the handler for serving runtime"
540
+ f"Handler is not supported for {kind} runtime"
541
541
  )
542
542
  elif kind in RuntimeKinds.nuclio_runtimes():
543
543
  runner.spec.function_handler = handler
@@ -575,22 +575,22 @@ def _process_runtime(command, runtime, kind):
575
575
 
576
576
 
577
577
  def code_to_function(
578
- name: str = "",
579
- project: str = "",
580
- tag: str = "",
581
- filename: str = "",
582
- handler: str = "",
583
- kind: str = "",
584
- image: str = None,
585
- code_output: str = "",
578
+ name: Optional[str] = "",
579
+ project: Optional[str] = "",
580
+ tag: Optional[str] = "",
581
+ filename: Optional[str] = "",
582
+ handler: Optional[str] = "",
583
+ kind: Optional[str] = "",
584
+ image: Optional[str] = None,
585
+ code_output: Optional[str] = "",
586
586
  embed_code: bool = True,
587
- description: str = "",
588
- requirements: Union[str, list[str]] = None,
589
- categories: list[str] = None,
590
- labels: dict[str, str] = None,
591
- with_doc: bool = True,
592
- ignored_tags=None,
593
- requirements_file: str = "",
587
+ description: Optional[str] = "",
588
+ requirements: Optional[Union[str, list[str]]] = None,
589
+ categories: Optional[list[str]] = None,
590
+ labels: Optional[dict[str, str]] = None,
591
+ with_doc: Optional[bool] = True,
592
+ ignored_tags: Optional[str] = None,
593
+ requirements_file: Optional[str] = "",
594
594
  ) -> Union[
595
595
  MpiRuntimeV1Alpha1,
596
596
  MpiRuntimeV1,
@@ -602,6 +602,7 @@ def code_to_function(
602
602
  Spark3Runtime,
603
603
  RemoteSparkRuntime,
604
604
  DatabricksRuntime,
605
+ ApplicationRuntime,
605
606
  ]:
606
607
  """Convenience function to insert code and configure an mlrun runtime.
607
608
 
@@ -628,7 +629,7 @@ def code_to_function(
628
629
  - spark: run distributed Spark job using Spark Kubernetes Operator
629
630
  - remote-spark: run distributed Spark job on remote Spark service
630
631
 
631
- Learn more about {Kinds of function (runtimes)](../concepts/functions-overview.html).
632
+ Learn more about [Kinds of function (runtimes)](../concepts/functions-overview.html).
632
633
 
633
634
  :param name: function name, typically best to use hyphen-case
634
635
  :param project: project used to namespace the function, defaults to 'default'
@@ -718,35 +719,34 @@ def code_to_function(
718
719
  fn.metadata.categories = categories
719
720
  fn.metadata.labels = labels or fn.metadata.labels
720
721
 
721
- def resolve_nuclio_subkind(kind):
722
- is_nuclio = kind.startswith("nuclio")
723
- subkind = kind[kind.find(":") + 1 :] if is_nuclio and ":" in kind else None
724
- if kind == RuntimeKinds.serving:
725
- is_nuclio = True
726
- subkind = serving_subkind
727
- return is_nuclio, subkind
728
-
729
722
  if (
730
723
  not embed_code
731
724
  and not code_output
732
725
  and (not filename or filename.endswith(".ipynb"))
733
726
  ):
734
727
  raise ValueError(
735
- "a valid code file must be specified "
728
+ "A valid code file must be specified "
736
729
  "when not using the embed_code option"
737
730
  )
738
731
 
739
732
  if kind == RuntimeKinds.databricks and not embed_code:
740
- raise ValueError("databricks tasks only support embed_code=True")
733
+ raise ValueError("Databricks tasks only support embed_code=True")
741
734
 
742
- is_nuclio, subkind = resolve_nuclio_subkind(kind)
735
+ if kind == RuntimeKinds.application:
736
+ if handler:
737
+ raise MLRunInvalidArgumentError(
738
+ "Handler is not supported for application runtime"
739
+ )
740
+ filename, handler = ApplicationRuntime.get_filename_and_handler()
741
+
742
+ is_nuclio, sub_kind = RuntimeKinds.resolve_nuclio_sub_kind(kind)
743
743
  code_origin = add_name(add_code_metadata(filename), name)
744
744
 
745
745
  name, spec, code = nuclio.build_file(
746
746
  filename,
747
747
  name=name,
748
748
  handler=handler or "handler",
749
- kind=subkind,
749
+ kind=sub_kind,
750
750
  ignored_tags=ignored_tags,
751
751
  )
752
752
  spec["spec"]["env"].append(
@@ -759,14 +759,14 @@ def code_to_function(
759
759
  if not kind and spec_kind not in ["", "Function"]:
760
760
  kind = spec_kind.lower()
761
761
 
762
- # if its a nuclio subkind, redo nb parsing
763
- is_nuclio, subkind = resolve_nuclio_subkind(kind)
762
+ # if its a nuclio sub kind, redo nb parsing
763
+ is_nuclio, sub_kind = RuntimeKinds.resolve_nuclio_sub_kind(kind)
764
764
  if is_nuclio:
765
765
  name, spec, code = nuclio.build_file(
766
766
  filename,
767
767
  name=name,
768
768
  handler=handler or "handler",
769
- kind=subkind,
769
+ kind=sub_kind,
770
770
  ignored_tags=ignored_tags,
771
771
  )
772
772
 
@@ -780,33 +780,29 @@ def code_to_function(
780
780
  raise ValueError("code_output option is only used with notebooks")
781
781
 
782
782
  if is_nuclio:
783
- if subkind == serving_subkind:
784
- r = ServingRuntime()
785
- else:
786
- r = RemoteRuntime()
787
- r.spec.function_kind = subkind
788
- # default_handler is only used in :mlrun subkind, determine the handler to invoke in function.run()
789
- r.spec.default_handler = handler if subkind == "mlrun" else ""
790
- r.spec.function_handler = (
783
+ runtime = RuntimeKinds.resolve_nuclio_runtime(kind, sub_kind)
784
+ # default_handler is only used in :mlrun sub kind, determine the handler to invoke in function.run()
785
+ runtime.spec.default_handler = handler if sub_kind == "mlrun" else ""
786
+ runtime.spec.function_handler = (
791
787
  handler if handler and ":" in handler else get_in(spec, "spec.handler")
792
788
  )
793
789
  if not embed_code:
794
- r.spec.source = filename
790
+ runtime.spec.source = filename
795
791
  nuclio_runtime = get_in(spec, "spec.runtime")
796
792
  if nuclio_runtime and not nuclio_runtime.startswith("py"):
797
- r.spec.nuclio_runtime = nuclio_runtime
793
+ runtime.spec.nuclio_runtime = nuclio_runtime
798
794
  if not name:
799
- raise ValueError("name must be specified")
800
- r.metadata.name = name
801
- r.spec.build.code_origin = code_origin
802
- r.spec.build.origin_filename = filename or (name + ".ipynb")
803
- update_common(r, spec)
804
- return r
795
+ raise ValueError("Missing required parameter: name")
796
+ runtime.metadata.name = name
797
+ runtime.spec.build.code_origin = code_origin
798
+ runtime.spec.build.origin_filename = filename or (name + ".ipynb")
799
+ update_common(runtime, spec)
800
+ return runtime
805
801
 
806
802
  if kind is None or kind in ["", "Function"]:
807
803
  raise ValueError("please specify the function kind")
808
804
  elif kind in RuntimeKinds.all():
809
- r = get_runtime_class(kind)()
805
+ runtime = get_runtime_class(kind)()
810
806
  else:
811
807
  raise ValueError(f"unsupported runtime ({kind})")
812
808
 
@@ -815,10 +811,10 @@ def code_to_function(
815
811
  if not name:
816
812
  raise ValueError("name must be specified")
817
813
  h = get_in(spec, "spec.handler", "").split(":")
818
- r.handler = h[0] if len(h) <= 1 else h[1]
819
- r.metadata = get_in(spec, "spec.metadata")
820
- r.metadata.name = name
821
- build = r.spec.build
814
+ runtime.handler = h[0] if len(h) <= 1 else h[1]
815
+ runtime.metadata = get_in(spec, "spec.metadata")
816
+ runtime.metadata.name = name
817
+ build = runtime.spec.build
822
818
  build.code_origin = code_origin
823
819
  build.origin_filename = filename or (name + ".ipynb")
824
820
  build.extra = get_in(spec, "spec.build.extra")
@@ -826,18 +822,18 @@ def code_to_function(
826
822
  build.builder_env = get_in(spec, "spec.build.builder_env")
827
823
  if not embed_code:
828
824
  if code_output:
829
- r.spec.command = code_output
825
+ runtime.spec.command = code_output
830
826
  else:
831
- r.spec.command = filename
827
+ runtime.spec.command = filename
832
828
 
833
829
  build.image = get_in(spec, "spec.build.image")
834
- update_common(r, spec)
835
- r.prepare_image_for_deploy()
830
+ update_common(runtime, spec)
831
+ runtime.prepare_image_for_deploy()
836
832
 
837
833
  if with_doc:
838
- update_function_entry_points(r, code)
839
- r.spec.default_handler = handler
840
- return r
834
+ update_function_entry_points(runtime, code)
835
+ runtime.spec.default_handler = handler
836
+ return runtime
841
837
 
842
838
 
843
839
  def _run_pipeline(
@@ -43,6 +43,8 @@ from .nuclio import (
43
43
  new_v2_model_server,
44
44
  nuclio_init_hook,
45
45
  )
46
+ from .nuclio.application import ApplicationRuntime
47
+ from .nuclio.serving import serving_subkind
46
48
  from .remotesparkjob import RemoteSparkRuntime
47
49
  from .sparkjob import Spark3Runtime
48
50
 
@@ -101,6 +103,7 @@ class RuntimeKinds:
101
103
  local = "local"
102
104
  handler = "handler"
103
105
  databricks = "databricks"
106
+ application = "application"
104
107
 
105
108
  @staticmethod
106
109
  def all():
@@ -115,6 +118,7 @@ class RuntimeKinds:
115
118
  RuntimeKinds.mpijob,
116
119
  RuntimeKinds.local,
117
120
  RuntimeKinds.databricks,
121
+ RuntimeKinds.application,
118
122
  ]
119
123
 
120
124
  @staticmethod
@@ -147,6 +151,7 @@ class RuntimeKinds:
147
151
  RuntimeKinds.remote,
148
152
  RuntimeKinds.nuclio,
149
153
  RuntimeKinds.serving,
154
+ RuntimeKinds.application,
150
155
  ]
151
156
 
152
157
  @staticmethod
@@ -211,6 +216,35 @@ class RuntimeKinds:
211
216
  # both spark and remote spark uses different mechanism for assigning images
212
217
  return kind not in [RuntimeKinds.spark, RuntimeKinds.remotespark]
213
218
 
219
+ @staticmethod
220
+ def resolve_nuclio_runtime(kind: str, sub_kind: str):
221
+ kind = kind.split(":")[0]
222
+ if kind not in RuntimeKinds.nuclio_runtimes():
223
+ raise ValueError(
224
+ f"Kind {kind} is not a nuclio runtime, available runtimes are {RuntimeKinds.nuclio_runtimes()}"
225
+ )
226
+
227
+ if sub_kind == serving_subkind:
228
+ return ServingRuntime()
229
+
230
+ if kind == RuntimeKinds.application:
231
+ return ApplicationRuntime()
232
+
233
+ runtime = RemoteRuntime()
234
+ runtime.spec.function_kind = sub_kind
235
+ return runtime
236
+
237
+ @staticmethod
238
+ def resolve_nuclio_sub_kind(kind):
239
+ is_nuclio = kind.startswith("nuclio")
240
+ sub_kind = kind[kind.find(":") + 1 :] if is_nuclio and ":" in kind else None
241
+ if kind == RuntimeKinds.serving:
242
+ is_nuclio = True
243
+ sub_kind = serving_subkind
244
+ elif kind == RuntimeKinds.application:
245
+ is_nuclio = True
246
+ return is_nuclio, sub_kind
247
+
214
248
 
215
249
  def get_runtime_class(kind: str):
216
250
  if kind == RuntimeKinds.mpijob:
@@ -228,6 +262,7 @@ def get_runtime_class(kind: str):
228
262
  RuntimeKinds.local: LocalRuntime,
229
263
  RuntimeKinds.remotespark: RemoteSparkRuntime,
230
264
  RuntimeKinds.databricks: DatabricksRuntime,
265
+ RuntimeKinds.application: ApplicationRuntime,
231
266
  }
232
267
 
233
268
  return kind_runtime_map[kind]