apolo-app-types 25.4.3__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.
Files changed (81) hide show
  1. apolo_app_types/__init__.py +159 -0
  2. apolo_app_types/app_types.py +70 -0
  3. apolo_app_types/cli.py +50 -0
  4. apolo_app_types/clients/__init__.py +0 -0
  5. apolo_app_types/clients/kube.py +106 -0
  6. apolo_app_types/helm/__init__.py +0 -0
  7. apolo_app_types/helm/apps/__init__.py +14 -0
  8. apolo_app_types/helm/apps/base.py +32 -0
  9. apolo_app_types/helm/apps/common.py +264 -0
  10. apolo_app_types/helm/apps/custom_deployment.py +133 -0
  11. apolo_app_types/helm/apps/dockerhub.py +42 -0
  12. apolo_app_types/helm/apps/fooocus.py +119 -0
  13. apolo_app_types/helm/apps/ingress.py +90 -0
  14. apolo_app_types/helm/apps/llm.py +185 -0
  15. apolo_app_types/helm/apps/postgres.py +231 -0
  16. apolo_app_types/helm/apps/spark_job.py +191 -0
  17. apolo_app_types/helm/apps/stable_diffusion.py +88 -0
  18. apolo_app_types/helm/apps/weaviate.py +133 -0
  19. apolo_app_types/helm/utils/__init__.py +0 -0
  20. apolo_app_types/helm/utils/credentials.py +28 -0
  21. apolo_app_types/helm/utils/deep_merging.py +24 -0
  22. apolo_app_types/helm/utils/images.py +51 -0
  23. apolo_app_types/helm/utils/storage.py +13 -0
  24. apolo_app_types/inputs/__init__.py +0 -0
  25. apolo_app_types/inputs/args.py +100 -0
  26. apolo_app_types/outputs/__init__.py +0 -0
  27. apolo_app_types/outputs/custom_deployment.py +42 -0
  28. apolo_app_types/outputs/dockerhub.py +29 -0
  29. apolo_app_types/outputs/huggingface_cache.py +22 -0
  30. apolo_app_types/outputs/llm.py +71 -0
  31. apolo_app_types/outputs/postgres.py +64 -0
  32. apolo_app_types/outputs/spark_job.py +11 -0
  33. apolo_app_types/outputs/stable_diffusion.py +45 -0
  34. apolo_app_types/outputs/update_outputs.py +76 -0
  35. apolo_app_types/outputs/utils/__init__.py +0 -0
  36. apolo_app_types/outputs/utils/ingress.py +37 -0
  37. apolo_app_types/outputs/utils/parsing.py +18 -0
  38. apolo_app_types/outputs/weaviate.py +94 -0
  39. apolo_app_types/protocols/__init__.py +0 -0
  40. apolo_app_types/protocols/apolo_deploy.py +11 -0
  41. apolo_app_types/protocols/common/__init__.py +69 -0
  42. apolo_app_types/protocols/common/abc_.py +55 -0
  43. apolo_app_types/protocols/common/auth.py +28 -0
  44. apolo_app_types/protocols/common/autoscaling.py +24 -0
  45. apolo_app_types/protocols/common/buckets.py +128 -0
  46. apolo_app_types/protocols/common/containers.py +20 -0
  47. apolo_app_types/protocols/common/hugging_face.py +49 -0
  48. apolo_app_types/protocols/common/ingress.py +36 -0
  49. apolo_app_types/protocols/common/k8s.py +56 -0
  50. apolo_app_types/protocols/common/networking.py +41 -0
  51. apolo_app_types/protocols/common/postgres.py +33 -0
  52. apolo_app_types/protocols/common/preset.py +19 -0
  53. apolo_app_types/protocols/common/py.typed +0 -0
  54. apolo_app_types/protocols/common/redis.py +9 -0
  55. apolo_app_types/protocols/common/schema_extra.py +20 -0
  56. apolo_app_types/protocols/common/secrets_.py +45 -0
  57. apolo_app_types/protocols/common/storage.py +125 -0
  58. apolo_app_types/protocols/common/validator.py +26 -0
  59. apolo_app_types/protocols/custom_deployment.py +39 -0
  60. apolo_app_types/protocols/dify.py +44 -0
  61. apolo_app_types/protocols/dockerhub.py +61 -0
  62. apolo_app_types/protocols/fooocus.py +54 -0
  63. apolo_app_types/protocols/huggingface_cache.py +9 -0
  64. apolo_app_types/protocols/jupyter.py +19 -0
  65. apolo_app_types/protocols/llm.py +130 -0
  66. apolo_app_types/protocols/mlflow.py +16 -0
  67. apolo_app_types/protocols/postgres.py +125 -0
  68. apolo_app_types/protocols/private_gpt.py +29 -0
  69. apolo_app_types/protocols/py.typed +0 -0
  70. apolo_app_types/protocols/pycharm.py +10 -0
  71. apolo_app_types/protocols/shell.py +10 -0
  72. apolo_app_types/protocols/spark_job.py +189 -0
  73. apolo_app_types/protocols/stable_diffusion.py +81 -0
  74. apolo_app_types/protocols/text_embeddings.py +24 -0
  75. apolo_app_types/protocols/vscode.py +10 -0
  76. apolo_app_types/protocols/weaviate.py +75 -0
  77. apolo_app_types/py.typed +0 -0
  78. apolo_app_types-25.4.3.dist-info/METADATA +33 -0
  79. apolo_app_types-25.4.3.dist-info/RECORD +81 -0
  80. apolo_app_types-25.4.3.dist-info/WHEEL +4 -0
  81. apolo_app_types-25.4.3.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,159 @@
1
+ from apolo_app_types.protocols.apolo_deploy import ApoloDeployInputs, ApoloDeployOutputs
2
+ from apolo_app_types.protocols.common import (
3
+ ApoloFilesMount,
4
+ ApoloSecret,
5
+ AppInputs,
6
+ AppInputsDeployer,
7
+ AppOutputs,
8
+ AppOutputsDeployer,
9
+ BasicAuth,
10
+ Bucket,
11
+ Env,
12
+ HuggingFaceModel,
13
+ OptionalStrOrSecret,
14
+ StrOrSecret,
15
+ )
16
+ from apolo_app_types.protocols.custom_deployment import (
17
+ Container,
18
+ ContainerImage,
19
+ CustomDeploymentInputs,
20
+ CustomDeploymentOutputs,
21
+ Service,
22
+ )
23
+ from apolo_app_types.protocols.dify import (
24
+ DifyApi,
25
+ DifyInputs,
26
+ DifyOutputs,
27
+ DifyProxy,
28
+ DifyWeb,
29
+ DifyWorker,
30
+ )
31
+ from apolo_app_types.protocols.dockerhub import (
32
+ DockerConfigModel,
33
+ DockerHubInputs,
34
+ DockerHubOutputs,
35
+ )
36
+ from apolo_app_types.protocols.fooocus import (
37
+ FooocusAppInputs,
38
+ FooocusInputs,
39
+ FooocusOutputs,
40
+ )
41
+ from apolo_app_types.protocols.huggingface_cache import (
42
+ HuggingFaceCacheInputs,
43
+ HuggingFaceCacheOutputs,
44
+ )
45
+ from apolo_app_types.protocols.jupyter import (
46
+ JupyterInputs,
47
+ JupyterOutputs,
48
+ JupyterTypes,
49
+ )
50
+ from apolo_app_types.protocols.llm import (
51
+ LLMInputs,
52
+ OpenAICompatibleAPI,
53
+ OpenAICompatibleChatAPI,
54
+ OpenAICompatibleCompletionsAPI,
55
+ OpenAICompatibleEmbeddingsAPI,
56
+ VLLMOutputs,
57
+ VLLMOutputsV2,
58
+ )
59
+ from apolo_app_types.protocols.mlflow import MLFlowInputs, MLFlowOutputs
60
+ from apolo_app_types.protocols.postgres import (
61
+ CrunchyPostgresOutputs,
62
+ CrunchyPostgresUserCredentials,
63
+ PGBouncer,
64
+ PostgresConfig,
65
+ PostgresInputs,
66
+ PostgresOutputs,
67
+ )
68
+ from apolo_app_types.protocols.private_gpt import PrivateGPTInputs, PrivateGPTOutputs
69
+ from apolo_app_types.protocols.pycharm import PycharmInputs, PycharmOutputs
70
+ from apolo_app_types.protocols.shell import ShellInputs, ShellOutputs
71
+ from apolo_app_types.protocols.spark_job import SparkJobInputs, SparkJobOutputs
72
+ from apolo_app_types.protocols.stable_diffusion import (
73
+ SDModel,
74
+ SDOutputs,
75
+ StableDiffusionInputs,
76
+ StableDiffusionOutputs,
77
+ TextToImgAPI,
78
+ )
79
+ from apolo_app_types.protocols.text_embeddings import (
80
+ TextEmbeddingsInferenceInputs,
81
+ TextEmbeddingsInferenceOutputs,
82
+ )
83
+ from apolo_app_types.protocols.vscode import VSCodeInputs, VSCodeOutputs
84
+ from apolo_app_types.protocols.weaviate import WeaviateInputs, WeaviateOutputs
85
+
86
+
87
+ __all__ = [
88
+ "AppInputs",
89
+ "AppOutputs",
90
+ "DifyApi",
91
+ "DifyInputs",
92
+ "DifyOutputs",
93
+ "DifyProxy",
94
+ "DifyWeb",
95
+ "DifyWorker",
96
+ "PycharmInputs",
97
+ "PycharmOutputs",
98
+ "VSCodeInputs",
99
+ "VSCodeOutputs",
100
+ "FooocusInputs",
101
+ "FooocusOutputs",
102
+ "JupyterInputs",
103
+ "JupyterOutputs",
104
+ "LLMInputs",
105
+ "VLLMOutputs",
106
+ "MLFlowInputs",
107
+ "MLFlowOutputs",
108
+ "PostgresInputs",
109
+ "CrunchyPostgresOutputs",
110
+ "ShellInputs",
111
+ "ShellOutputs",
112
+ "SDOutputs",
113
+ "StableDiffusionInputs",
114
+ "TextEmbeddingsInferenceInputs",
115
+ "TextEmbeddingsInferenceOutputs",
116
+ "WeaviateInputs",
117
+ "WeaviateOutputs",
118
+ "OpenAICompatibleEmbeddingsAPI",
119
+ "OpenAICompatibleAPI",
120
+ "OpenAICompatibleChatAPI",
121
+ "OpenAICompatibleCompletionsAPI",
122
+ "TextToImgAPI",
123
+ "CrunchyPostgresUserCredentials",
124
+ "SDModel",
125
+ "BasicAuth",
126
+ "ApoloDeployInputs",
127
+ "ApoloDeployOutputs",
128
+ "JupyterTypes",
129
+ "PrivateGPTInputs",
130
+ "PrivateGPTOutputs",
131
+ "ApoloSecret",
132
+ "StrOrSecret",
133
+ "OptionalStrOrSecret",
134
+ "StableDiffusionOutputs",
135
+ "HuggingFaceModel",
136
+ "VLLMOutputsV2",
137
+ "AppInputsDeployer",
138
+ "PGBouncer",
139
+ "PostgresConfig",
140
+ "DockerHubInputs",
141
+ "DockerHubOutputs",
142
+ "DockerConfigModel",
143
+ "ApoloFilesMount",
144
+ "HuggingFaceCacheInputs",
145
+ "HuggingFaceCacheOutputs",
146
+ "CustomDeploymentInputs",
147
+ "CustomDeploymentOutputs",
148
+ "ContainerImage",
149
+ "Container",
150
+ "Service",
151
+ "Env",
152
+ "AppOutputsDeployer",
153
+ "PostgresOutputs",
154
+ "SparkJobInputs",
155
+ "SparkJobOutputs",
156
+ "FooocusAppInputs",
157
+ "ApoloFilesMount",
158
+ "Bucket",
159
+ ]
@@ -0,0 +1,70 @@
1
+ import enum
2
+
3
+ from apolo_app_types import StableDiffusionInputs
4
+ from apolo_app_types.protocols.common import AppInputs
5
+ from apolo_app_types.protocols.dockerhub import DockerHubInputs
6
+ from apolo_app_types.protocols.huggingface_cache import (
7
+ HuggingFaceCacheInputs,
8
+ )
9
+ from apolo_app_types.protocols.llm import LLMInputs
10
+ from apolo_app_types.protocols.spark_job import SparkJobInputs
11
+ from apolo_app_types.protocols.weaviate import WeaviateInputs
12
+
13
+
14
+ class AppType(enum.StrEnum):
15
+ PostgreSQL = "postgresql"
16
+ TextEmbeddingsInference = "text-embeddings-inference"
17
+ LLMInference = "llm-inference"
18
+ PrivateGPT = "private-gpt"
19
+ Dify = "dify"
20
+ StableDiffusion = "stable-diffusion"
21
+ Weaviate = "weaviate"
22
+ Fooocus = "fooocus"
23
+ Jupyter = "jupyter"
24
+ VSCode = "vscode"
25
+ Pycharm = "pycharm"
26
+ MLFlow = "mlflow"
27
+ Shell = "shell"
28
+ ApoloDeploy = "apolo-deploy"
29
+ DockerHub = "dockerhub"
30
+ HuggingFaceCache = "huggingface-cache"
31
+ CustomDeployment = "custom-deployment"
32
+ SparkJob = "spark-job"
33
+
34
+ def __repr__(self) -> str:
35
+ return str(self)
36
+
37
+ def deploys_as_job(self) -> bool:
38
+ return self in {
39
+ AppType.PrivateGPT,
40
+ AppType.Fooocus,
41
+ AppType.Jupyter,
42
+ AppType.VSCode,
43
+ AppType.Pycharm,
44
+ AppType.MLFlow,
45
+ AppType.Shell,
46
+ AppType.ApoloDeploy,
47
+ }
48
+
49
+ def is_appless(self) -> bool:
50
+ return self in {
51
+ AppType.HuggingFaceCache,
52
+ }
53
+
54
+ @classmethod
55
+ def from_app_inputs(cls, inputs: AppInputs) -> "AppType":
56
+ if isinstance(inputs, LLMInputs):
57
+ return AppType.LLMInference
58
+ if isinstance(inputs, WeaviateInputs):
59
+ return AppType.Weaviate
60
+ if isinstance(inputs, StableDiffusionInputs):
61
+ return AppType.StableDiffusion
62
+ if isinstance(inputs, DockerHubInputs):
63
+ return AppType.DockerHub
64
+ if isinstance(inputs, HuggingFaceCacheInputs):
65
+ return AppType.HuggingFaceCache
66
+ if isinstance(inputs, SparkJobInputs):
67
+ return AppType.SparkJob
68
+
69
+ error_message = f"Unsupported input type: {type(inputs).__name__}"
70
+ raise ValueError(error_message)
apolo_app_types/cli.py ADDED
@@ -0,0 +1,50 @@
1
+ import asyncio
2
+ import json
3
+ import logging
4
+ import os
5
+ import sys
6
+
7
+ import click
8
+
9
+ from apolo_app_types.outputs.update_outputs import update_app_outputs
10
+
11
+
12
+ log_level = os.getenv("LOG_LEVEL", "INFO").upper()
13
+
14
+ logging.basicConfig(
15
+ level=log_level,
16
+ format="%(asctime)s - %(levelname)s - %(message)s",
17
+ handlers=[logging.StreamHandler(sys.stdout)],
18
+ )
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ @click.group()
24
+ def cli() -> None:
25
+ pass
26
+
27
+
28
+ @cli.command("update-outputs", context_settings={"ignore_unknown_options": True})
29
+ @click.argument("helm_outputs_json", type=str)
30
+ def update_outputs(
31
+ helm_outputs_json: str,
32
+ ) -> None:
33
+ try:
34
+ logger.info("Helm input: %s", helm_outputs_json)
35
+ helm_outputs_dict = json.loads(helm_outputs_json)
36
+ logger.info("Helm outputs: %s", helm_outputs_dict)
37
+ result = asyncio.run(update_app_outputs(helm_outputs_dict))
38
+ if not result:
39
+ m = "Failed to run update_app_outputs"
40
+ raise Exception(m)
41
+ except json.JSONDecodeError as e:
42
+ logger.error("Failed to parse JSON input: %s", e)
43
+ sys.exit(1)
44
+ except Exception as e:
45
+ logger.error("An error occurred: %s", e)
46
+ sys.exit(1)
47
+
48
+
49
+ if __name__ == "__main__":
50
+ cli()
File without changes
@@ -0,0 +1,106 @@
1
+ import logging
2
+ import typing
3
+ from pathlib import Path
4
+
5
+ from kubernetes import client, config # type: ignore
6
+ from kubernetes.client.rest import ApiException # type: ignore
7
+
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ SERVICE_ACC_NAMESPACE_FILE = "/var/run/secrets/kubernetes.io/serviceaccount/namespace"
12
+
13
+
14
+ def get_current_namespace() -> str:
15
+ """
16
+ Retrieve the current namespace from the Kubernetes service account namespace file.
17
+ """
18
+ try:
19
+ with Path.open(Path(SERVICE_ACC_NAMESPACE_FILE)) as f:
20
+ return f.read().strip()
21
+ except FileNotFoundError:
22
+ logger.error("Namespace file not found. Are you running in a Kubernetes pod?")
23
+ raise
24
+ except Exception as e:
25
+ err_msg = f"Error while reading namespace file: {e}"
26
+ logger.error(err_msg)
27
+ raise
28
+
29
+
30
+ async def get_ingresses_as_dict(label_selectors: str) -> dict[str, typing.Any]:
31
+ try:
32
+ config.load_incluster_config()
33
+
34
+ networking_v1 = client.NetworkingV1Api()
35
+ namespace = get_current_namespace()
36
+ ingresses = networking_v1.list_namespaced_ingress(
37
+ namespace=namespace, label_selector=label_selectors
38
+ )
39
+
40
+ return client.ApiClient().sanitize_for_serialization(ingresses)
41
+
42
+ except ApiException as e:
43
+ err_msg = (
44
+ f"Exception when calling NetworkingV1Api->list_namespaced_ingress: {e}"
45
+ )
46
+ logger.error(err_msg)
47
+ raise e
48
+
49
+
50
+ async def get_services_by_label(label_selectors: str) -> dict[str, typing.Any]:
51
+ try:
52
+ config.load_incluster_config()
53
+
54
+ v1 = client.CoreV1Api()
55
+ namespace = get_current_namespace()
56
+ services = v1.list_namespaced_service(
57
+ namespace=namespace, label_selector=label_selectors
58
+ )
59
+
60
+ return client.ApiClient().sanitize_for_serialization(services)
61
+
62
+ except ApiException as e:
63
+ err_msg = f"Exception when calling CoreV1Api->list_namespaced_service: {e}"
64
+ logger.error(err_msg)
65
+ raise e
66
+
67
+
68
+ async def get_services(match_labels: dict[str, str]) -> list[dict[str, typing.Any]]:
69
+ label_selectors = ",".join(f"{k}={v}" for k, v in match_labels.items())
70
+ get_svc_stdout = await get_services_by_label(label_selectors)
71
+
72
+ return get_svc_stdout["items"]
73
+
74
+
75
+ async def get_service_host_port(match_labels: dict[str, str]) -> tuple[str, str]:
76
+ services = await get_services(match_labels)
77
+ if not services:
78
+ msg = f"Service with labels {match_labels} not found"
79
+ raise Exception(msg)
80
+ if len(services) > 1:
81
+ msg = f"Multiple services with labels {match_labels} found"
82
+ logger.warning(msg)
83
+
84
+ service = services[0]
85
+ host = f"{service['metadata']['name']}.{service['metadata']['namespace']}"
86
+ post = str(service["spec"]["ports"][0]["port"])
87
+
88
+ return host, post
89
+
90
+
91
+ async def get_secret(label: str) -> typing.Any:
92
+ try:
93
+ config.load_incluster_config()
94
+
95
+ v1 = client.CoreV1Api()
96
+ namespace = get_current_namespace()
97
+
98
+ return v1.list_namespaced_secret(
99
+ namespace=namespace,
100
+ label_selector=label,
101
+ )
102
+
103
+ except ApiException as e:
104
+ err_msg = f"Exception when calling CoreV1Api->read_namespaced_secret: {e}"
105
+ logger.error(err_msg)
106
+ raise e
File without changes
@@ -0,0 +1,14 @@
1
+ from .base import BaseChartValueProcessor
2
+ from .common import parse_chart_values_simple
3
+ from .custom_deployment import CustomDeploymentChartValueProcessor
4
+ from .llm import LLMChartValueProcessor
5
+ from .stable_diffusion import StableDiffusionChartValueProcessor
6
+
7
+
8
+ __all__ = [
9
+ "BaseChartValueProcessor",
10
+ "LLMChartValueProcessor",
11
+ "StableDiffusionChartValueProcessor",
12
+ "parse_chart_values_simple",
13
+ "CustomDeploymentChartValueProcessor",
14
+ ]
@@ -0,0 +1,32 @@
1
+ import abc
2
+ import logging
3
+ import typing as t
4
+
5
+ import apolo_sdk
6
+
7
+ from apolo_app_types import AppInputs
8
+
9
+
10
+ logger = logging.getLogger()
11
+
12
+ AppInputT = t.TypeVar("AppInputT", bound="AppInputs")
13
+
14
+
15
+ class BaseChartValueProcessor(abc.ABC, t.Generic[AppInputT]):
16
+ def __init__(self, client: apolo_sdk.Client):
17
+ self.client = client
18
+
19
+ async def gen_extra_helm_args(self, *_: t.Any) -> list[str]:
20
+ return ["--timeout", "15m", "--dependency-update"]
21
+
22
+ @abc.abstractmethod
23
+ async def gen_extra_values(
24
+ self,
25
+ input_: AppInputT,
26
+ app_name: str,
27
+ namespace: str,
28
+ app_secrets_name: str,
29
+ *args: t.Any,
30
+ **kwargs: t.Any,
31
+ ) -> dict[str, t.Any]:
32
+ raise NotImplementedError
@@ -0,0 +1,264 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ import re
5
+ import typing as t
6
+ from copy import deepcopy
7
+ from decimal import Decimal
8
+
9
+ import apolo_sdk
10
+ import yaml
11
+ from apolo_sdk import Preset
12
+
13
+ from apolo_app_types.helm.apps.ingress import get_ingress_values
14
+ from apolo_app_types.protocols.common import (
15
+ ApoloFilesMount,
16
+ Ingress,
17
+ Preset as PresetType,
18
+ )
19
+
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ APOLO_STORAGE_ANNOTATION = "platform.apolo.us/inject-storage"
24
+
25
+
26
+ def get_preset(client: apolo_sdk.Client, preset_name: str) -> apolo_sdk.Preset:
27
+ if os.environ.get("ENV") == "local":
28
+ return Preset(
29
+ credits_per_hour=Decimal(1.0),
30
+ cpu=1,
31
+ memory=1024,
32
+ resource_pool_names=("default",),
33
+ available_resource_pool_names=("default",),
34
+ )
35
+ preset = client.config.presets.get(preset_name)
36
+ if not preset:
37
+ msg = f"Preset {preset_name} not exist in cluster {client.config.cluster_name}"
38
+ raise Exception(msg)
39
+ return preset
40
+
41
+
42
+ def preset_to_resources(preset: apolo_sdk.Preset) -> dict[str, t.Any]:
43
+ requests = {
44
+ "cpu": f"{preset.cpu * 1000}m",
45
+ "memory": f"{preset.memory_mb}M",
46
+ }
47
+ if preset.nvidia_gpu:
48
+ requests["nvidia.com/gpu"] = str(preset.nvidia_gpu)
49
+ if preset.amd_gpu:
50
+ requests["amd.com/gpu"] = str(preset.amd_gpu)
51
+
52
+ return {"requests": requests, "limits": requests.copy()}
53
+
54
+
55
+ def get_component_values(preset: Preset, preset_name: str) -> dict[str, t.Any]:
56
+ return {
57
+ "labels": {
58
+ "platform.apolo.us/component": "app",
59
+ "platform.apolo.us/preset": preset_name,
60
+ },
61
+ "resources": preset_to_resources(preset),
62
+ "tolerations": preset_to_tolerations(preset),
63
+ "affinity": preset_to_affinity(preset),
64
+ }
65
+
66
+
67
+ def _get_match_expressions(pool_names: list[str]) -> list[dict[str, t.Any]]:
68
+ return [
69
+ {
70
+ "key": "platform.neuromation.io/nodepool",
71
+ "operator": "In",
72
+ "values": pool_names,
73
+ }
74
+ ]
75
+
76
+
77
+ def preset_to_affinity(preset: apolo_sdk.Preset) -> dict[str, t.Any]:
78
+ affinity = {}
79
+ if preset.available_resource_pool_names:
80
+ affinity["nodeAffinity"] = {
81
+ "requiredDuringSchedulingIgnoredDuringExecution": {
82
+ "nodeSelectorTerms": [
83
+ {
84
+ "matchExpressions": _get_match_expressions(
85
+ list(preset.available_resource_pool_names)
86
+ )
87
+ }
88
+ ]
89
+ }
90
+ }
91
+ return affinity
92
+
93
+
94
+ def preset_to_tolerations(preset: apolo_sdk.Preset) -> list[dict[str, t.Any]]:
95
+ tolerations: list[dict[str, t.Any]] = [
96
+ {
97
+ "effect": "NoSchedule",
98
+ "key": "platform.neuromation.io/job",
99
+ "operator": "Exists",
100
+ },
101
+ {
102
+ "effect": "NoExecute",
103
+ "key": "node.kubernetes.io/not-ready",
104
+ "operator": "Exists",
105
+ "tolerationSeconds": 300,
106
+ },
107
+ {
108
+ "effect": "NoExecute",
109
+ "key": "node.kubernetes.io/unreachable",
110
+ "operator": "Exists",
111
+ "tolerationSeconds": 300,
112
+ },
113
+ ]
114
+ if preset.amd_gpu:
115
+ tolerations.append(
116
+ {"effect": "NoSchedule", "key": "amd.com/gpu", "operator": "Exists"}
117
+ )
118
+ if preset.nvidia_gpu:
119
+ tolerations.append(
120
+ {"effect": "NoSchedule", "key": "nvidia.com/gpu", "operator": "Exists"}
121
+ )
122
+ return tolerations
123
+
124
+
125
+ def parse_chart_values_simple(helm_args: list[str]) -> dict[str, t.Any]:
126
+ chart_values = {}
127
+ set_re = re.compile(r"--set[\s+,=](.+?)(?= --set|\s|$)")
128
+
129
+ for match in set_re.finditer(" ".join(helm_args)):
130
+ keyvalue = match.group(1).strip()
131
+ key, value = keyvalue.split("=", 1)
132
+ chart_values[key] = value
133
+ return chart_values
134
+
135
+
136
+ # TODO: hack - should define specific input models for each helm app
137
+ def get_extra_env_vars_from_job() -> tuple[dict[str, t.Any], list[str]]:
138
+ """
139
+ Get extra env vars from job environment.
140
+ Currently, only HUGGING_FACE_HUB_TOKEN is supported.
141
+
142
+ Returns:
143
+ Tuple[dict[str, t.Any], list[str]]: Tuple of extra env vars and secret strings.
144
+ """
145
+ extra_env_vars = {}
146
+ secret_strings = []
147
+ hf_token = os.getenv("HUGGING_FACE_HUB_TOKEN")
148
+ if hf_token:
149
+ extra_env_vars["HUGGING_FACE_HUB_TOKEN"] = hf_token
150
+ secret_strings.append(hf_token)
151
+ return extra_env_vars, secret_strings
152
+
153
+
154
+ def set_value_from_dot_notation(
155
+ data: dict[str, t.Any], key: str, value: t.Any
156
+ ) -> dict[str, t.Any]:
157
+ """
158
+ Set value in nested dict using dot notation.
159
+
160
+ Args:
161
+ data (dict[str, t.Any]): Nested dict.
162
+ key (str): Dot notation key.
163
+ value (t.Any): Value to set.
164
+
165
+ Returns:
166
+ dict[str, t.Any]: Updated nested dict.
167
+ """
168
+ data_ref = data
169
+ keys = key.split(".")
170
+ for k in keys[:-1]:
171
+ data = data.setdefault(k, {})
172
+ data[keys[-1]] = value
173
+ return data_ref
174
+
175
+
176
+ def sanitize_dict_string(
177
+ values: dict[str, t.Any],
178
+ secrets: t.Sequence[str] | None = None,
179
+ keys: t.Sequence[str] | None = None,
180
+ ) -> str:
181
+ keys = keys or []
182
+ secrets = secrets or []
183
+ values = deepcopy(values)
184
+ for key in keys:
185
+ values = set_value_from_dot_notation(values, key, "****")
186
+ dict_str = yaml.dump(values)
187
+ if len(secrets) > 0:
188
+ sec_re = re.compile(f"({'|'.join(secrets)})")
189
+ return sec_re.sub("****", dict_str)
190
+ return dict_str
191
+
192
+
193
+ def gen_apolo_storage_integration_annotations(
194
+ files_mouts: t.Sequence[ApoloFilesMount],
195
+ ) -> list[dict[str, str]]:
196
+ storage_mount_annotations = []
197
+ for storage_mount in files_mouts:
198
+ storage_mount_annotations.append(
199
+ {
200
+ "storage_uri": storage_mount.storage_uri.path,
201
+ "mount_path": storage_mount.mount_path.path,
202
+ "mount_mode": storage_mount.mode.mode.value,
203
+ }
204
+ )
205
+ return storage_mount_annotations
206
+
207
+
208
+ def gen_apolo_storage_integration_labels(
209
+ *,
210
+ inject_storage: bool = False,
211
+ ) -> dict[str, str]:
212
+ if inject_storage:
213
+ return {APOLO_STORAGE_ANNOTATION: "true"}
214
+ return {}
215
+
216
+
217
+ def append_apolo_storage_integration_annotations(
218
+ current_annotations: dict[str, t.Any], files_mounts: t.Sequence[ApoloFilesMount]
219
+ ) -> dict[str, str]:
220
+ """
221
+ Returns a new dict with the storage annotations appended to the current annotations.
222
+ """
223
+ cur_annot = deepcopy(current_annotations)
224
+ cur_apolo_annotation_str: str | None = cur_annot.get(APOLO_STORAGE_ANNOTATION)
225
+ if cur_apolo_annotation_str:
226
+ current_list = json.loads(cur_apolo_annotation_str)
227
+ else:
228
+ current_list = []
229
+
230
+ new_annotations = gen_apolo_storage_integration_annotations(files_mounts)
231
+ current_list.extend(new_annotations)
232
+ cur_annot[APOLO_STORAGE_ANNOTATION] = json.dumps(current_list)
233
+ return cur_annot
234
+
235
+
236
+ async def gen_extra_values(
237
+ apolo_client: apolo_sdk.Client,
238
+ preset_type: PresetType,
239
+ ingress: Ingress | None = None,
240
+ namespace: str | None = None,
241
+ ) -> dict[str, t.Any]:
242
+ preset_name = preset_type.name
243
+ if not preset_name:
244
+ logger.warning("No preset_name found in helm args.")
245
+ return {}
246
+
247
+ preset = get_preset(apolo_client, preset_name)
248
+ tolerations_vals = preset_to_tolerations(preset)
249
+ affinity_vals = preset_to_affinity(preset)
250
+ resources_vals = preset_to_resources(preset)
251
+ ingress_vals: dict[str, t.Any] = {}
252
+ if ingress:
253
+ if not namespace:
254
+ exception_msg = "Namespace is required when ingress is provided."
255
+ raise ValueError(exception_msg)
256
+ ingress_vals = await get_ingress_values(apolo_client, ingress, namespace)
257
+
258
+ return {
259
+ "preset_name": preset_name,
260
+ "resources": resources_vals,
261
+ "tolerations": tolerations_vals,
262
+ "affinity": affinity_vals,
263
+ **ingress_vals,
264
+ }