zenml-nightly 0.70.0.dev20241127__py3-none-any.whl → 0.70.0.dev20241128__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.
- zenml/VERSION +1 -1
- zenml/integrations/__init__.py +1 -0
- zenml/integrations/constants.py +1 -0
- zenml/integrations/kubernetes/orchestrators/kube_utils.py +46 -2
- zenml/integrations/kubernetes/orchestrators/kubernetes_orchestrator.py +13 -2
- zenml/integrations/kubernetes/orchestrators/kubernetes_orchestrator_entrypoint.py +3 -1
- zenml/integrations/kubernetes/orchestrators/manifest_utils.py +3 -2
- zenml/integrations/kubernetes/step_operators/kubernetes_step_operator.py +3 -1
- zenml/integrations/modal/__init__.py +46 -0
- zenml/integrations/modal/flavors/__init__.py +26 -0
- zenml/integrations/modal/flavors/modal_step_operator_flavor.py +125 -0
- zenml/integrations/modal/step_operators/__init__.py +22 -0
- zenml/integrations/modal/step_operators/modal_step_operator.py +242 -0
- zenml/orchestrators/step_runner.py +1 -1
- zenml/orchestrators/utils.py +24 -2
- {zenml_nightly-0.70.0.dev20241127.dist-info → zenml_nightly-0.70.0.dev20241128.dist-info}/METADATA +1 -1
- {zenml_nightly-0.70.0.dev20241127.dist-info → zenml_nightly-0.70.0.dev20241128.dist-info}/RECORD +20 -15
- {zenml_nightly-0.70.0.dev20241127.dist-info → zenml_nightly-0.70.0.dev20241128.dist-info}/LICENSE +0 -0
- {zenml_nightly-0.70.0.dev20241127.dist-info → zenml_nightly-0.70.0.dev20241128.dist-info}/WHEEL +0 -0
- {zenml_nightly-0.70.0.dev20241127.dist-info → zenml_nightly-0.70.0.dev20241128.dist-info}/entry_points.txt +0 -0
zenml/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.70.0.
|
1
|
+
0.70.0.dev20241128
|
zenml/integrations/__init__.py
CHANGED
@@ -48,6 +48,7 @@ from zenml.integrations.lightgbm import LightGBMIntegration # noqa
|
|
48
48
|
|
49
49
|
# from zenml.integrations.llama_index import LlamaIndexIntegration # noqa
|
50
50
|
from zenml.integrations.mlflow import MlflowIntegration # noqa
|
51
|
+
from zenml.integrations.modal import ModalIntegration # noqa
|
51
52
|
from zenml.integrations.neptune import NeptuneIntegration # noqa
|
52
53
|
from zenml.integrations.neural_prophet import NeuralProphetIntegration # noqa
|
53
54
|
from zenml.integrations.numpy import NumpyIntegration # noqa
|
zenml/integrations/constants.py
CHANGED
@@ -94,18 +94,62 @@ def load_kube_config(
|
|
94
94
|
k8s_config.load_kube_config(context=context)
|
95
95
|
|
96
96
|
|
97
|
-
def
|
97
|
+
def calculate_max_pod_name_length_for_namespace(namespace: str) -> int:
|
98
|
+
"""Calculate the max pod length for a certain namespace.
|
99
|
+
|
100
|
+
Args:
|
101
|
+
namespace: The namespace in which the pod will be created.
|
102
|
+
|
103
|
+
Returns:
|
104
|
+
The maximum pod name length.
|
105
|
+
"""
|
106
|
+
# Kubernetes allows Pod names to have 253 characters. However, when
|
107
|
+
# creating a pod they try to create a log file which is called
|
108
|
+
# <NAMESPACE>_<POD_NAME>_<UUID>, which adds additional characters and
|
109
|
+
# runs into filesystem limitations for filename lengths (255). We therefore
|
110
|
+
# subtract the length of a UUID (36), the two underscores and the
|
111
|
+
# namespace length from the max filename length.
|
112
|
+
return 255 - 38 - len(namespace)
|
113
|
+
|
114
|
+
|
115
|
+
def sanitize_pod_name(pod_name: str, namespace: str) -> str:
|
98
116
|
"""Sanitize pod names so they conform to Kubernetes pod naming convention.
|
99
117
|
|
100
118
|
Args:
|
101
119
|
pod_name: Arbitrary input pod name.
|
120
|
+
namespace: Namespace in which the Pod will be created.
|
102
121
|
|
103
122
|
Returns:
|
104
123
|
Sanitized pod name.
|
105
124
|
"""
|
125
|
+
# https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-subdomain-names
|
106
126
|
pod_name = re.sub(r"[^a-z0-9-]", "-", pod_name.lower())
|
107
127
|
pod_name = re.sub(r"^[-]+", "", pod_name)
|
108
|
-
|
128
|
+
pod_name = re.sub(r"[-]+$", "", pod_name)
|
129
|
+
pod_name = re.sub(r"[-]+", "-", pod_name)
|
130
|
+
|
131
|
+
allowed_length = calculate_max_pod_name_length_for_namespace(
|
132
|
+
namespace=namespace
|
133
|
+
)
|
134
|
+
return pod_name[:allowed_length]
|
135
|
+
|
136
|
+
|
137
|
+
def sanitize_label(label: str) -> str:
|
138
|
+
"""Sanitize a label for a Kubernetes resource.
|
139
|
+
|
140
|
+
Args:
|
141
|
+
label: The label to sanitize.
|
142
|
+
|
143
|
+
Returns:
|
144
|
+
The sanitized label.
|
145
|
+
"""
|
146
|
+
# https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#rfc-1035-label-names
|
147
|
+
label = re.sub(r"[^a-z0-9-]", "-", label.lower())
|
148
|
+
label = re.sub(r"^[-]+", "", label)
|
149
|
+
label = re.sub(r"[-]+$", "", label)
|
150
|
+
label = re.sub(r"[-]+", "-", label)
|
151
|
+
|
152
|
+
return label[:63]
|
109
153
|
|
110
154
|
|
111
155
|
def pod_is_not_pending(pod: k8s_client.V1Pod) -> bool:
|
@@ -395,8 +395,19 @@ class KubernetesOrchestrator(ContainerizedOrchestrator):
|
|
395
395
|
)
|
396
396
|
|
397
397
|
pipeline_name = deployment.pipeline_configuration.name
|
398
|
-
|
399
|
-
|
398
|
+
|
399
|
+
# We already make sure the orchestrator run name has the correct length
|
400
|
+
# to make sure we don't cut off the randomized suffix later when
|
401
|
+
# sanitizing the pod name. This avoids any pod naming collisions.
|
402
|
+
max_length = kube_utils.calculate_max_pod_name_length_for_namespace(
|
403
|
+
namespace=self.config.kubernetes_namespace
|
404
|
+
)
|
405
|
+
orchestrator_run_name = get_orchestrator_run_name(
|
406
|
+
pipeline_name, max_length=max_length
|
407
|
+
)
|
408
|
+
pod_name = kube_utils.sanitize_pod_name(
|
409
|
+
orchestrator_run_name, namespace=self.config.kubernetes_namespace
|
410
|
+
)
|
400
411
|
|
401
412
|
assert stack.container_registry
|
402
413
|
|
@@ -90,7 +90,9 @@ def main() -> None:
|
|
90
90
|
"""
|
91
91
|
# Define Kubernetes pod name.
|
92
92
|
pod_name = f"{orchestrator_run_id}-{step_name}"
|
93
|
-
pod_name = kube_utils.sanitize_pod_name(
|
93
|
+
pod_name = kube_utils.sanitize_pod_name(
|
94
|
+
pod_name, namespace=args.kubernetes_namespace
|
95
|
+
)
|
94
96
|
|
95
97
|
image = KubernetesOrchestrator.get_image(
|
96
98
|
deployment=deployment_config, step_name=step_name
|
@@ -25,6 +25,7 @@ from zenml.constants import ENV_ZENML_ENABLE_REPO_INIT_WARNINGS
|
|
25
25
|
from zenml.integrations.airflow.orchestrators.dag_generator import (
|
26
26
|
ENV_ZENML_LOCAL_STORES_PATH,
|
27
27
|
)
|
28
|
+
from zenml.integrations.kubernetes.orchestrators import kube_utils
|
28
29
|
from zenml.integrations.kubernetes.pod_settings import KubernetesPodSettings
|
29
30
|
|
30
31
|
|
@@ -167,8 +168,8 @@ def build_pod_manifest(
|
|
167
168
|
# Add run_name and pipeline_name to the labels
|
168
169
|
labels.update(
|
169
170
|
{
|
170
|
-
"run": run_name,
|
171
|
-
"pipeline": pipeline_name,
|
171
|
+
"run": kube_utils.sanitize_label(run_name),
|
172
|
+
"pipeline": kube_utils.sanitize_label(pipeline_name),
|
172
173
|
}
|
173
174
|
)
|
174
175
|
|
@@ -197,7 +197,9 @@ class KubernetesStepOperator(BaseStepOperator):
|
|
197
197
|
)
|
198
198
|
|
199
199
|
pod_name = f"{info.run_name}_{info.pipeline_step_name}"
|
200
|
-
pod_name = kube_utils.sanitize_pod_name(
|
200
|
+
pod_name = kube_utils.sanitize_pod_name(
|
201
|
+
pod_name, namespace=self.config.kubernetes_namespace
|
202
|
+
)
|
201
203
|
|
202
204
|
command = entrypoint_command[:3]
|
203
205
|
args = entrypoint_command[3:]
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# Copyright (c) ZenML GmbH 2024. All Rights Reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at:
|
6
|
+
#
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
12
|
+
# or implied. See the License for the specific language governing
|
13
|
+
# permissions and limitations under the License.
|
14
|
+
"""Modal integration for cloud-native step execution.
|
15
|
+
|
16
|
+
The Modal integration sub-module provides a step operator flavor that allows
|
17
|
+
executing steps on Modal's cloud infrastructure.
|
18
|
+
"""
|
19
|
+
from typing import List, Type
|
20
|
+
|
21
|
+
from zenml.integrations.constants import MODAL
|
22
|
+
from zenml.integrations.integration import Integration
|
23
|
+
from zenml.stack import Flavor
|
24
|
+
|
25
|
+
MODAL_STEP_OPERATOR_FLAVOR = "modal"
|
26
|
+
|
27
|
+
|
28
|
+
class ModalIntegration(Integration):
|
29
|
+
"""Definition of Modal integration for ZenML."""
|
30
|
+
|
31
|
+
NAME = MODAL
|
32
|
+
REQUIREMENTS = ["modal>=0.64.49,<1"]
|
33
|
+
|
34
|
+
@classmethod
|
35
|
+
def flavors(cls) -> List[Type[Flavor]]:
|
36
|
+
"""Declare the stack component flavors for the Modal integration.
|
37
|
+
|
38
|
+
Returns:
|
39
|
+
List of new stack component flavors.
|
40
|
+
"""
|
41
|
+
from zenml.integrations.modal.flavors import ModalStepOperatorFlavor
|
42
|
+
|
43
|
+
return [ModalStepOperatorFlavor]
|
44
|
+
|
45
|
+
|
46
|
+
ModalIntegration.check_installation()
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# Copyright (c) ZenML GmbH 2024. All Rights Reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at:
|
6
|
+
#
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
12
|
+
# or implied. See the License for the specific language governing
|
13
|
+
# permissions and limitations under the License.
|
14
|
+
"""Modal integration flavors."""
|
15
|
+
|
16
|
+
from zenml.integrations.modal.flavors.modal_step_operator_flavor import (
|
17
|
+
ModalStepOperatorConfig,
|
18
|
+
ModalStepOperatorFlavor,
|
19
|
+
ModalStepOperatorSettings,
|
20
|
+
)
|
21
|
+
|
22
|
+
__all__ = [
|
23
|
+
"ModalStepOperatorConfig",
|
24
|
+
"ModalStepOperatorFlavor",
|
25
|
+
"ModalStepOperatorSettings",
|
26
|
+
]
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# Copyright (c) ZenML GmbH 2024. All Rights Reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at:
|
6
|
+
#
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
12
|
+
# or implied. See the License for the specific language governing
|
13
|
+
# permissions and limitations under the License.
|
14
|
+
"""Modal step operator flavor."""
|
15
|
+
|
16
|
+
from typing import TYPE_CHECKING, Optional, Type
|
17
|
+
|
18
|
+
from zenml.config.base_settings import BaseSettings
|
19
|
+
from zenml.integrations.modal import MODAL_STEP_OPERATOR_FLAVOR
|
20
|
+
from zenml.step_operators import BaseStepOperatorConfig, BaseStepOperatorFlavor
|
21
|
+
|
22
|
+
if TYPE_CHECKING:
|
23
|
+
from zenml.integrations.modal.step_operators import ModalStepOperator
|
24
|
+
|
25
|
+
|
26
|
+
class ModalStepOperatorSettings(BaseSettings):
|
27
|
+
"""Settings for the Modal step operator.
|
28
|
+
|
29
|
+
Specifying the region and cloud provider is only available for Enterprise
|
30
|
+
and Team plan customers.
|
31
|
+
|
32
|
+
Certain combinations of settings are not available. It is suggested to err
|
33
|
+
on the side of looser settings rather than more restrictive ones to avoid
|
34
|
+
pipeline execution failures. In the case of failures, however, Modal
|
35
|
+
provides detailed error messages that can help identify what is
|
36
|
+
incompatible. See more in the Modal docs at https://modal.com/docs/guide/region-selection.
|
37
|
+
|
38
|
+
Attributes:
|
39
|
+
gpu: The type of GPU to use for the step execution.
|
40
|
+
region: The region to use for the step execution.
|
41
|
+
cloud: The cloud provider to use for the step execution.
|
42
|
+
"""
|
43
|
+
|
44
|
+
gpu: Optional[str] = None
|
45
|
+
region: Optional[str] = None
|
46
|
+
cloud: Optional[str] = None
|
47
|
+
|
48
|
+
|
49
|
+
class ModalStepOperatorConfig(
|
50
|
+
BaseStepOperatorConfig, ModalStepOperatorSettings
|
51
|
+
):
|
52
|
+
"""Configuration for the Modal step operator."""
|
53
|
+
|
54
|
+
@property
|
55
|
+
def is_remote(self) -> bool:
|
56
|
+
"""Checks if this stack component is running remotely.
|
57
|
+
|
58
|
+
This designation is used to determine if the stack component can be
|
59
|
+
used with a local ZenML database or if it requires a remote ZenML
|
60
|
+
server.
|
61
|
+
|
62
|
+
Returns:
|
63
|
+
True if this config is for a remote component, False otherwise.
|
64
|
+
"""
|
65
|
+
return True
|
66
|
+
|
67
|
+
|
68
|
+
class ModalStepOperatorFlavor(BaseStepOperatorFlavor):
|
69
|
+
"""Modal step operator flavor."""
|
70
|
+
|
71
|
+
@property
|
72
|
+
def name(self) -> str:
|
73
|
+
"""Name of the flavor.
|
74
|
+
|
75
|
+
Returns:
|
76
|
+
The name of the flavor.
|
77
|
+
"""
|
78
|
+
return MODAL_STEP_OPERATOR_FLAVOR
|
79
|
+
|
80
|
+
@property
|
81
|
+
def docs_url(self) -> Optional[str]:
|
82
|
+
"""A url to point at docs explaining this flavor.
|
83
|
+
|
84
|
+
Returns:
|
85
|
+
A flavor docs url.
|
86
|
+
"""
|
87
|
+
return self.generate_default_docs_url()
|
88
|
+
|
89
|
+
@property
|
90
|
+
def sdk_docs_url(self) -> Optional[str]:
|
91
|
+
"""A url to point at SDK docs explaining this flavor.
|
92
|
+
|
93
|
+
Returns:
|
94
|
+
A flavor SDK docs url.
|
95
|
+
"""
|
96
|
+
return self.generate_default_sdk_docs_url()
|
97
|
+
|
98
|
+
@property
|
99
|
+
def logo_url(self) -> str:
|
100
|
+
"""A url to represent the flavor in the dashboard.
|
101
|
+
|
102
|
+
Returns:
|
103
|
+
The flavor logo.
|
104
|
+
"""
|
105
|
+
return "https://public-flavor-logos.s3.eu-central-1.amazonaws.com/step_operator/modal.png"
|
106
|
+
|
107
|
+
@property
|
108
|
+
def config_class(self) -> Type[ModalStepOperatorConfig]:
|
109
|
+
"""Returns `ModalStepOperatorConfig` config class.
|
110
|
+
|
111
|
+
Returns:
|
112
|
+
The config class.
|
113
|
+
"""
|
114
|
+
return ModalStepOperatorConfig
|
115
|
+
|
116
|
+
@property
|
117
|
+
def implementation_class(self) -> Type["ModalStepOperator"]:
|
118
|
+
"""Implementation class for this flavor.
|
119
|
+
|
120
|
+
Returns:
|
121
|
+
The implementation class.
|
122
|
+
"""
|
123
|
+
from zenml.integrations.modal.step_operators import ModalStepOperator
|
124
|
+
|
125
|
+
return ModalStepOperator
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# Copyright (c) ZenML GmbH 2024. All Rights Reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at:
|
6
|
+
#
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
12
|
+
# or implied. See the License for the specific language governing
|
13
|
+
# permissions and limitations under the License.
|
14
|
+
"""Modal step operator."""
|
15
|
+
|
16
|
+
from zenml.integrations.modal.step_operators.modal_step_operator import (
|
17
|
+
ModalStepOperator,
|
18
|
+
)
|
19
|
+
|
20
|
+
__all__ = [
|
21
|
+
"ModalStepOperator",
|
22
|
+
]
|
@@ -0,0 +1,242 @@
|
|
1
|
+
# Copyright (c) ZenML GmbH 2024. All Rights Reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at:
|
6
|
+
#
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
12
|
+
# or implied. See the License for the specific language governing
|
13
|
+
# permissions and limitations under the License.
|
14
|
+
"""Modal step operator implementation."""
|
15
|
+
|
16
|
+
import asyncio
|
17
|
+
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type, cast
|
18
|
+
|
19
|
+
import modal
|
20
|
+
from modal_proto import api_pb2
|
21
|
+
|
22
|
+
from zenml.client import Client
|
23
|
+
from zenml.config.build_configuration import BuildConfiguration
|
24
|
+
from zenml.config.resource_settings import ByteUnit, ResourceSettings
|
25
|
+
from zenml.enums import StackComponentType
|
26
|
+
from zenml.integrations.modal.flavors import (
|
27
|
+
ModalStepOperatorConfig,
|
28
|
+
ModalStepOperatorSettings,
|
29
|
+
)
|
30
|
+
from zenml.logger import get_logger
|
31
|
+
from zenml.stack import Stack, StackValidator
|
32
|
+
from zenml.step_operators import BaseStepOperator
|
33
|
+
|
34
|
+
if TYPE_CHECKING:
|
35
|
+
from zenml.config.base_settings import BaseSettings
|
36
|
+
from zenml.config.step_run_info import StepRunInfo
|
37
|
+
from zenml.models import PipelineDeploymentBase
|
38
|
+
|
39
|
+
logger = get_logger(__name__)
|
40
|
+
|
41
|
+
MODAL_STEP_OPERATOR_DOCKER_IMAGE_KEY = "modal_step_operator"
|
42
|
+
|
43
|
+
|
44
|
+
def get_gpu_values(
|
45
|
+
settings: ModalStepOperatorSettings, resource_settings: ResourceSettings
|
46
|
+
) -> Optional[str]:
|
47
|
+
"""Get the GPU values for the Modal step operator.
|
48
|
+
|
49
|
+
Args:
|
50
|
+
settings: The Modal step operator settings.
|
51
|
+
resource_settings: The resource settings.
|
52
|
+
|
53
|
+
Returns:
|
54
|
+
The GPU string if a count is specified, otherwise the GPU type.
|
55
|
+
"""
|
56
|
+
if not settings.gpu:
|
57
|
+
return None
|
58
|
+
gpu_count = resource_settings.gpu_count
|
59
|
+
return f"{settings.gpu}:{gpu_count}" if gpu_count else settings.gpu
|
60
|
+
|
61
|
+
|
62
|
+
class ModalStepOperator(BaseStepOperator):
|
63
|
+
"""Step operator to run a step on Modal.
|
64
|
+
|
65
|
+
This class defines code that can set up a Modal environment and run
|
66
|
+
functions in it.
|
67
|
+
"""
|
68
|
+
|
69
|
+
@property
|
70
|
+
def config(self) -> ModalStepOperatorConfig:
|
71
|
+
"""Get the Modal step operator configuration.
|
72
|
+
|
73
|
+
Returns:
|
74
|
+
The Modal step operator configuration.
|
75
|
+
"""
|
76
|
+
return cast(ModalStepOperatorConfig, self._config)
|
77
|
+
|
78
|
+
@property
|
79
|
+
def settings_class(self) -> Optional[Type["BaseSettings"]]:
|
80
|
+
"""Get the settings class for the Modal step operator.
|
81
|
+
|
82
|
+
Returns:
|
83
|
+
The Modal step operator settings class.
|
84
|
+
"""
|
85
|
+
return ModalStepOperatorSettings
|
86
|
+
|
87
|
+
@property
|
88
|
+
def validator(self) -> Optional[StackValidator]:
|
89
|
+
"""Get the stack validator for the Modal step operator.
|
90
|
+
|
91
|
+
Returns:
|
92
|
+
The stack validator.
|
93
|
+
"""
|
94
|
+
|
95
|
+
def _validate_remote_components(stack: "Stack") -> Tuple[bool, str]:
|
96
|
+
if stack.artifact_store.config.is_local:
|
97
|
+
return False, (
|
98
|
+
"The Modal step operator runs code remotely and "
|
99
|
+
"needs to write files into the artifact store, but the "
|
100
|
+
f"artifact store `{stack.artifact_store.name}` of the "
|
101
|
+
"active stack is local. Please ensure that your stack "
|
102
|
+
"contains a remote artifact store when using the Modal "
|
103
|
+
"step operator."
|
104
|
+
)
|
105
|
+
|
106
|
+
container_registry = stack.container_registry
|
107
|
+
assert container_registry is not None
|
108
|
+
|
109
|
+
if container_registry.config.is_local:
|
110
|
+
return False, (
|
111
|
+
"The Modal step operator runs code remotely and "
|
112
|
+
"needs to push/pull Docker images, but the "
|
113
|
+
f"container registry `{container_registry.name}` of the "
|
114
|
+
"active stack is local. Please ensure that your stack "
|
115
|
+
"contains a remote container registry when using the "
|
116
|
+
"Modal step operator."
|
117
|
+
)
|
118
|
+
|
119
|
+
return True, ""
|
120
|
+
|
121
|
+
return StackValidator(
|
122
|
+
required_components={
|
123
|
+
StackComponentType.CONTAINER_REGISTRY,
|
124
|
+
StackComponentType.IMAGE_BUILDER,
|
125
|
+
},
|
126
|
+
custom_validation_function=_validate_remote_components,
|
127
|
+
)
|
128
|
+
|
129
|
+
def get_docker_builds(
|
130
|
+
self, deployment: "PipelineDeploymentBase"
|
131
|
+
) -> List["BuildConfiguration"]:
|
132
|
+
"""Get the Docker build configurations for the Modal step operator.
|
133
|
+
|
134
|
+
Args:
|
135
|
+
deployment: The pipeline deployment.
|
136
|
+
|
137
|
+
Returns:
|
138
|
+
A list of Docker build configurations.
|
139
|
+
"""
|
140
|
+
builds = []
|
141
|
+
for step_name, step in deployment.step_configurations.items():
|
142
|
+
if step.config.step_operator == self.name:
|
143
|
+
build = BuildConfiguration(
|
144
|
+
key=MODAL_STEP_OPERATOR_DOCKER_IMAGE_KEY,
|
145
|
+
settings=step.config.docker_settings,
|
146
|
+
step_name=step_name,
|
147
|
+
)
|
148
|
+
builds.append(build)
|
149
|
+
|
150
|
+
return builds
|
151
|
+
|
152
|
+
def launch(
|
153
|
+
self,
|
154
|
+
info: "StepRunInfo",
|
155
|
+
entrypoint_command: List[str],
|
156
|
+
environment: Dict[str, str],
|
157
|
+
) -> None:
|
158
|
+
"""Launch a step run on Modal.
|
159
|
+
|
160
|
+
Args:
|
161
|
+
info: The step run information.
|
162
|
+
entrypoint_command: The entrypoint command for the step.
|
163
|
+
environment: The environment variables for the step.
|
164
|
+
|
165
|
+
Raises:
|
166
|
+
RuntimeError: If no Docker credentials are found for the container registry.
|
167
|
+
ValueError: If no container registry is found in the stack.
|
168
|
+
"""
|
169
|
+
settings = cast(ModalStepOperatorSettings, self.get_settings(info))
|
170
|
+
image_name = info.get_image(key=MODAL_STEP_OPERATOR_DOCKER_IMAGE_KEY)
|
171
|
+
zc = Client()
|
172
|
+
stack = zc.active_stack
|
173
|
+
|
174
|
+
if not stack.container_registry:
|
175
|
+
raise ValueError(
|
176
|
+
"No Container registry found in the stack. "
|
177
|
+
"Please add a container registry and ensure "
|
178
|
+
"it is correctly configured."
|
179
|
+
)
|
180
|
+
|
181
|
+
if docker_creds := stack.container_registry.credentials:
|
182
|
+
docker_username, docker_password = docker_creds
|
183
|
+
else:
|
184
|
+
raise RuntimeError(
|
185
|
+
"No Docker credentials found for the container registry."
|
186
|
+
)
|
187
|
+
|
188
|
+
my_secret = modal.secret._Secret.from_dict(
|
189
|
+
{
|
190
|
+
"REGISTRY_USERNAME": docker_username,
|
191
|
+
"REGISTRY_PASSWORD": docker_password,
|
192
|
+
}
|
193
|
+
)
|
194
|
+
|
195
|
+
spec = modal.image.DockerfileSpec(
|
196
|
+
commands=[f"FROM {image_name}"], context_files={}
|
197
|
+
)
|
198
|
+
|
199
|
+
zenml_image = modal.Image._from_args(
|
200
|
+
dockerfile_function=lambda *_, **__: spec,
|
201
|
+
force_build=False,
|
202
|
+
image_registry_config=modal.image._ImageRegistryConfig(
|
203
|
+
api_pb2.REGISTRY_AUTH_TYPE_STATIC_CREDS, my_secret
|
204
|
+
),
|
205
|
+
).env(environment)
|
206
|
+
|
207
|
+
resource_settings = info.config.resource_settings
|
208
|
+
gpu_values = get_gpu_values(settings, resource_settings)
|
209
|
+
|
210
|
+
app = modal.App(
|
211
|
+
f"zenml-{info.run_name}-{info.step_run_id}-{info.pipeline_step_name}"
|
212
|
+
)
|
213
|
+
|
214
|
+
async def run_sandbox() -> asyncio.Future[None]:
|
215
|
+
loop = asyncio.get_event_loop()
|
216
|
+
future = loop.create_future()
|
217
|
+
with modal.enable_output():
|
218
|
+
async with app.run():
|
219
|
+
memory_mb = resource_settings.get_memory(ByteUnit.MB)
|
220
|
+
memory_int = (
|
221
|
+
int(memory_mb) if memory_mb is not None else None
|
222
|
+
)
|
223
|
+
sb = await modal.Sandbox.create.aio(
|
224
|
+
"bash",
|
225
|
+
"-c",
|
226
|
+
" ".join(entrypoint_command),
|
227
|
+
image=zenml_image,
|
228
|
+
gpu=gpu_values,
|
229
|
+
cpu=resource_settings.cpu_count,
|
230
|
+
memory=memory_int,
|
231
|
+
cloud=settings.cloud,
|
232
|
+
region=settings.region,
|
233
|
+
app=app,
|
234
|
+
timeout=86400, # 24h, the max Modal allows
|
235
|
+
)
|
236
|
+
|
237
|
+
await sb.wait.aio()
|
238
|
+
|
239
|
+
future.set_result(None)
|
240
|
+
return future
|
241
|
+
|
242
|
+
asyncio.run(run_sandbox())
|
@@ -400,7 +400,7 @@ class StepRunner:
|
|
400
400
|
**artifact.get_hydrated_version().model_dump()
|
401
401
|
)
|
402
402
|
|
403
|
-
if data_type
|
403
|
+
if data_type in (None, Any) or is_union(get_origin(data_type)):
|
404
404
|
# Entrypoint function does not define a specific type for the input,
|
405
405
|
# we use the datatype of the stored artifact
|
406
406
|
data_type = source_utils.load(artifact.data_type)
|
zenml/orchestrators/utils.py
CHANGED
@@ -40,7 +40,9 @@ if TYPE_CHECKING:
|
|
40
40
|
from zenml.artifact_stores.base_artifact_store import BaseArtifactStore
|
41
41
|
|
42
42
|
|
43
|
-
def get_orchestrator_run_name(
|
43
|
+
def get_orchestrator_run_name(
|
44
|
+
pipeline_name: str, max_length: Optional[int] = None
|
45
|
+
) -> str:
|
44
46
|
"""Gets an orchestrator run name.
|
45
47
|
|
46
48
|
This run name is not the same as the ZenML run name but can instead be
|
@@ -48,11 +50,31 @@ def get_orchestrator_run_name(pipeline_name: str) -> str:
|
|
48
50
|
|
49
51
|
Args:
|
50
52
|
pipeline_name: Name of the pipeline that will run.
|
53
|
+
max_length: Maximum length of the generated name.
|
54
|
+
|
55
|
+
Raises:
|
56
|
+
ValueError: If the max length is below 8 characters.
|
51
57
|
|
52
58
|
Returns:
|
53
59
|
The orchestrator run name.
|
54
60
|
"""
|
55
|
-
|
61
|
+
suffix_length = 32
|
62
|
+
pipeline_name = f"{pipeline_name}_"
|
63
|
+
|
64
|
+
if max_length:
|
65
|
+
if max_length < 8:
|
66
|
+
raise ValueError(
|
67
|
+
"Maximum length for orchestrator run name must be 8 or above."
|
68
|
+
)
|
69
|
+
|
70
|
+
# Make sure we always have a certain suffix to guarantee no overlap
|
71
|
+
# with other runs
|
72
|
+
suffix_length = min(32, max(8, max_length - len(pipeline_name)))
|
73
|
+
pipeline_name = pipeline_name[: (max_length - suffix_length)]
|
74
|
+
|
75
|
+
suffix = "".join(random.choices("0123456789abcdef", k=suffix_length))
|
76
|
+
|
77
|
+
return f"{pipeline_name}{suffix}"
|
56
78
|
|
57
79
|
|
58
80
|
def is_setting_enabled(
|
{zenml_nightly-0.70.0.dev20241127.dist-info → zenml_nightly-0.70.0.dev20241128.dist-info}/RECORD
RENAMED
@@ -6,7 +6,7 @@ RELEASE_NOTES.md,sha256=DleauURHESDrTrcVzCVLqPiSM9NIAk5vldvEFMc7qlk,389375
|
|
6
6
|
ROADMAP.md,sha256=hiLSmr16BH8Dfx7SaQM4JcXCGCVl6mFZPFAwJeDTrJU,407
|
7
7
|
SECURITY.md,sha256=9DepA8y03yvCZLHEfcXLTDH4lUyKHquAdukBsccNN7c,682
|
8
8
|
zenml/README.md,sha256=827dekbOWAs1BpW7VF1a4d7EbwPbjwccX-2zdXBENZo,1777
|
9
|
-
zenml/VERSION,sha256=
|
9
|
+
zenml/VERSION,sha256=NNMdj0FLYeBDdXqt2fW55JGAJCyPz22NxDtWUZCXycg,19
|
10
10
|
zenml/__init__.py,sha256=SkMObQA41ajqdZqGErN00S1Vf3KAxpLvbZ-OBy5uYoo,2130
|
11
11
|
zenml/actions/__init__.py,sha256=mrt6wPo73iKRxK754_NqsGyJ3buW7RnVeIGXr1xEw8Y,681
|
12
12
|
zenml/actions/base_action.py,sha256=UcaHev6BTuLDwuswnyaPjdA8AgUqB5xPZ-lRtuvf2FU,25553
|
@@ -130,7 +130,7 @@ zenml/image_builders/base_image_builder.py,sha256=-Y5N3zFZsMJvVuzm1M3tU-r38fT9KC
|
|
130
130
|
zenml/image_builders/build_context.py,sha256=TTY5T8aG4epeKOOpLItr8PDjmDijfcGaY3zFzmGV1II,6157
|
131
131
|
zenml/image_builders/local_image_builder.py,sha256=R_zMpERtUCWLLDZg9kXuAQSWLtin1ve_rxpbUBiym7s,6448
|
132
132
|
zenml/integrations/README.md,sha256=hFIZwjsAItHjvDWVBqGSF-ZAeMsFR2GKX1Axl2g1Bz0,6190
|
133
|
-
zenml/integrations/__init__.py,sha256=
|
133
|
+
zenml/integrations/__init__.py,sha256=GiLRzSCu1O9Jg5NSCRKVDWLIXtKuc90ozntqYjUHo08,4905
|
134
134
|
zenml/integrations/airflow/__init__.py,sha256=7ffV98vlrdH1RfWHkv8TXNd3hjtXSx4z2U7MZin-87I,1483
|
135
135
|
zenml/integrations/airflow/flavors/__init__.py,sha256=Y48mn5OxERPPaXDBd5CFAIn6yhLPsgN5ZMk26hLXiNM,800
|
136
136
|
zenml/integrations/airflow/flavors/airflow_orchestrator_flavor.py,sha256=VfZQD2H-WwIgVD1Fi7uewdnkvRoSykY0YCfROFDadXg,6189
|
@@ -198,7 +198,7 @@ zenml/integrations/comet/experiment_trackers/__init__.py,sha256=reGygyAEgMrlc-9Q
|
|
198
198
|
zenml/integrations/comet/experiment_trackers/comet_experiment_tracker.py,sha256=JnB_TqiCD8t9t6cVxWoomxvBuhA4jIJHYFZ-gKdGXf8,5767
|
199
199
|
zenml/integrations/comet/flavors/__init__.py,sha256=x-XK-YwHMxz3zZPoIXo3X5vq_5VYUJAnsIoEX_ZooOU,883
|
200
200
|
zenml/integrations/comet/flavors/comet_experiment_tracker_flavor.py,sha256=Rkk1UtEVY2MQBKbUHKxYQpDTWndkOYF8KuKuMGZAb24,3706
|
201
|
-
zenml/integrations/constants.py,sha256=
|
201
|
+
zenml/integrations/constants.py,sha256=hbRRrkXz4qBFFZOl81G_2u7O-gWLU8DTSy43HlyUDUY,2071
|
202
202
|
zenml/integrations/databricks/__init__.py,sha256=dkyTxfwIete7mRBlDzIfsTmllYgrd4DB2P4brXHPMUs,2414
|
203
203
|
zenml/integrations/databricks/flavors/__init__.py,sha256=S-BZ3R9iKGOw-aUltR8I0ULEe2-LKGTIZhQv9TlnXfk,1122
|
204
204
|
zenml/integrations/databricks/flavors/databricks_model_deployer_flavor.py,sha256=eDyYVqO2x1A9qgGICKJx5Z3qiUuTMfW9R3NZUO8OiRk,3591
|
@@ -337,17 +337,17 @@ zenml/integrations/kubernetes/flavors/__init__.py,sha256=a5gU45qCj3FkLwl_uVjlIkL
|
|
337
337
|
zenml/integrations/kubernetes/flavors/kubernetes_orchestrator_flavor.py,sha256=HkCyDWqv1lDd8W6GeXE6PeHQiUrHPfSkfw3sB0B2xuA,7911
|
338
338
|
zenml/integrations/kubernetes/flavors/kubernetes_step_operator_flavor.py,sha256=ILN-H4cl7z3i4ltb4UBs55wbtIo871b4ib28pYkQoyQ,5605
|
339
339
|
zenml/integrations/kubernetes/orchestrators/__init__.py,sha256=TJID3OTieZBox36WpQpzD0jdVRA_aZVcs_bNtfXS8ik,811
|
340
|
-
zenml/integrations/kubernetes/orchestrators/kube_utils.py,sha256=
|
341
|
-
zenml/integrations/kubernetes/orchestrators/kubernetes_orchestrator.py,sha256=
|
342
|
-
zenml/integrations/kubernetes/orchestrators/kubernetes_orchestrator_entrypoint.py,sha256=
|
340
|
+
zenml/integrations/kubernetes/orchestrators/kube_utils.py,sha256=0Cj1RoiuXL4oACnfyzEpOiCTnHt_YoF5ml3bhobyOEg,12195
|
341
|
+
zenml/integrations/kubernetes/orchestrators/kubernetes_orchestrator.py,sha256=W6YpTQXXzQcpfskJcuYwN1LMiN8eUJQHWJ4QXMmADFs,22899
|
342
|
+
zenml/integrations/kubernetes/orchestrators/kubernetes_orchestrator_entrypoint.py,sha256=4BBy8-0xJahRjpkFc9fYy90uIRxOoy-s-GmXZeFE2tQ,6442
|
343
343
|
zenml/integrations/kubernetes/orchestrators/kubernetes_orchestrator_entrypoint_configuration.py,sha256=Y7uGU8eksMluGyXYsf688CwpiXwI_W6WYLscYwRZXRY,2494
|
344
|
-
zenml/integrations/kubernetes/orchestrators/manifest_utils.py,sha256=
|
344
|
+
zenml/integrations/kubernetes/orchestrators/manifest_utils.py,sha256=urcPGUm51vRA1a5eZk2jEGjFrDLPHqD4jTVJDpxngnU,11486
|
345
345
|
zenml/integrations/kubernetes/pod_settings.py,sha256=NMp4aHKRG29mh1Nq5uvV78Hzj1cMZj93poWCBiwov-M,4898
|
346
346
|
zenml/integrations/kubernetes/serialization_utils.py,sha256=cPSe4szdBLzDnUZT9nQc2CCA8h84aj5oTA8vsUE36ig,7000
|
347
347
|
zenml/integrations/kubernetes/service_connectors/__init__.py,sha256=Uf6zlHIapYrRDl3xOPWQ2jA7jt85SXx1U7DmSxzxTvQ,818
|
348
348
|
zenml/integrations/kubernetes/service_connectors/kubernetes_service_connector.py,sha256=kgdh25dOBNTxLAFft_cknwHoWRAGrdzUu9fLsm4ZlfY,19579
|
349
349
|
zenml/integrations/kubernetes/step_operators/__init__.py,sha256=40utDPYAezxHsFgO0UUIT_6XpCDzDapje6OH951XsTs,806
|
350
|
-
zenml/integrations/kubernetes/step_operators/kubernetes_step_operator.py,sha256=
|
350
|
+
zenml/integrations/kubernetes/step_operators/kubernetes_step_operator.py,sha256=x7-ZO1r85eqOE_RS4IM0H7NGYZcB9okTj6qFrh9N5HM,8376
|
351
351
|
zenml/integrations/label_studio/__init__.py,sha256=tXmK0Wu_bFgtL7CqPPubSK99PaBZSyAu90aghHlXAek,1520
|
352
352
|
zenml/integrations/label_studio/annotators/__init__.py,sha256=YtOtSfS1_NBoLoXIygEerElBP1-B98UU0HOAEfzdRY0,821
|
353
353
|
zenml/integrations/label_studio/annotators/label_studio_annotator.py,sha256=VkuW4zsZhHz8__P9WTTLRTF-FOmoYB-_cFqBdu-PUyA,30498
|
@@ -396,6 +396,11 @@ zenml/integrations/mlflow/services/mlflow_deployment.py,sha256=iu8u0jQLKNf5oueGP
|
|
396
396
|
zenml/integrations/mlflow/steps/__init__.py,sha256=5IXeipGRfBjtqr0ZdbQLliuQNr5GXsm7xkhpqfOg6qI,770
|
397
397
|
zenml/integrations/mlflow/steps/mlflow_deployer.py,sha256=E4A-tiKsVG-GDy1vaiOw37mpa4SzJR3T-HZ6crzONyo,12238
|
398
398
|
zenml/integrations/mlflow/steps/mlflow_registry.py,sha256=BeOhuo72ghuJWEIdr4YNqit_ImSljW4suUE9XguFjeQ,6425
|
399
|
+
zenml/integrations/modal/__init__.py,sha256=jyIhsLVjxhepKEpO-uyNJ5NSuSqzBIJnhb_lHdHFFFM,1525
|
400
|
+
zenml/integrations/modal/flavors/__init__.py,sha256=169Oirq-NUVl-2oiByrMcL3HBM0R971BrS6xVBk0DB4,922
|
401
|
+
zenml/integrations/modal/flavors/modal_step_operator_flavor.py,sha256=r7xkzER0hvNY6N0CEZMiF1vRk7A7CfkuYOeGDJ517N4,3978
|
402
|
+
zenml/integrations/modal/step_operators/__init__.py,sha256=8NYCXe7aNPnldCI9IjcHzJC_B5e4gtfGzpHNZ-ToKnE,780
|
403
|
+
zenml/integrations/modal/step_operators/modal_step_operator.py,sha256=J2HiNaGnlP6kDLBnQKz8mQKaoaQLitFkjHhOP_XRWgI,8543
|
399
404
|
zenml/integrations/neptune/__init__.py,sha256=FUaMxVC9uPryIY7reWdzfMcMdsf5T3HjBZ1QjpjEX4Q,1717
|
400
405
|
zenml/integrations/neptune/experiment_trackers/__init__.py,sha256=DJi7lk7NXYrRg3VGPPSEsMycKECDfXL-h2V0A0r7z8Y,833
|
401
406
|
zenml/integrations/neptune/experiment_trackers/neptune_experiment_tracker.py,sha256=c5CigxkeYGBadcchTOSjZPaDAtAkSzNM9EnaE2dOgc8,3730
|
@@ -681,9 +686,9 @@ zenml/orchestrators/output_utils.py,sha256=Gz7SX2cbQ3w4eyfU0XuhKEKGtalQGBoc6moDR
|
|
681
686
|
zenml/orchestrators/publish_utils.py,sha256=aNwgTDmVSq9qCDP3Ldk77YNXnWx_YHjYNTEJwYeZo9s,4579
|
682
687
|
zenml/orchestrators/step_launcher.py,sha256=HGdshxQAwYHLHa_nJyIkMgrSTbxTQKqI63mKaSQ3ZVg,17758
|
683
688
|
zenml/orchestrators/step_run_utils.py,sha256=FjnNGYz8NF-F_WP_8w7Zny0zDd6JanCp3IAFMjKFU9g,19996
|
684
|
-
zenml/orchestrators/step_runner.py,sha256=
|
689
|
+
zenml/orchestrators/step_runner.py,sha256=5bRN_0kAj8Jnk0cKt-xGnLrywmbSsqqkcBaJ3jYgDHc,25084
|
685
690
|
zenml/orchestrators/topsort.py,sha256=D8evz3X47zwpXd90NMLsJD-_uCeXtV6ClzNfDUrq7cM,5784
|
686
|
-
zenml/orchestrators/utils.py,sha256=
|
691
|
+
zenml/orchestrators/utils.py,sha256=DCKQuAzv9Ba2BxAGeexgCc9Q_1nzbqzhr4B4jI9NbeA,12141
|
687
692
|
zenml/orchestrators/wheeled_orchestrator.py,sha256=eOnMcnd3sCzfhA2l6qRAzF0rOXzaojbjvvYvTkqixQo,4791
|
688
693
|
zenml/pipelines/__init__.py,sha256=hpIX7hN8jsQRHT5R-xSXZL88qrHwkmrvGLQeu1rWt4o,873
|
689
694
|
zenml/pipelines/build_utils.py,sha256=Az-zz56WQqD9dUrZ0iQDDRok-GNeglmXQxv40LoiyQk,25440
|
@@ -1268,8 +1273,8 @@ zenml/zen_stores/secrets_stores/sql_secrets_store.py,sha256=Bq1djrUP9saoD7vECjS7
|
|
1268
1273
|
zenml/zen_stores/sql_zen_store.py,sha256=rJT3Uth4J6D1iVfBdNHipgI54jcqIItNVc-IMNEU8Zc,404787
|
1269
1274
|
zenml/zen_stores/template_utils.py,sha256=EKYBgmDLTS_PSMWaIO5yvHPLiQvMqHcsAe6NUCrv-i4,9068
|
1270
1275
|
zenml/zen_stores/zen_store_interface.py,sha256=vf2gKBWfUUPtcGZC35oQB6pPNVzWVyQC8nWxVLjfrxM,92692
|
1271
|
-
zenml_nightly-0.70.0.
|
1272
|
-
zenml_nightly-0.70.0.
|
1273
|
-
zenml_nightly-0.70.0.
|
1274
|
-
zenml_nightly-0.70.0.
|
1275
|
-
zenml_nightly-0.70.0.
|
1276
|
+
zenml_nightly-0.70.0.dev20241128.dist-info/LICENSE,sha256=wbnfEnXnafPbqwANHkV6LUsPKOtdpsd-SNw37rogLtc,11359
|
1277
|
+
zenml_nightly-0.70.0.dev20241128.dist-info/METADATA,sha256=pq6OfIPB-gg_Y1RQMZbo2H-G2BX9FLotTfJqywDuYpw,21208
|
1278
|
+
zenml_nightly-0.70.0.dev20241128.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
1279
|
+
zenml_nightly-0.70.0.dev20241128.dist-info/entry_points.txt,sha256=QK3ETQE0YswAM2mWypNMOv8TLtr7EjnqAFq1br_jEFE,43
|
1280
|
+
zenml_nightly-0.70.0.dev20241128.dist-info/RECORD,,
|
{zenml_nightly-0.70.0.dev20241127.dist-info → zenml_nightly-0.70.0.dev20241128.dist-info}/LICENSE
RENAMED
File without changes
|
{zenml_nightly-0.70.0.dev20241127.dist-info → zenml_nightly-0.70.0.dev20241128.dist-info}/WHEEL
RENAMED
File without changes
|
File without changes
|