zenml-nightly 0.72.0.dev20250120__py3-none-any.whl → 0.73.0.dev20250123__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/cli/service_connectors.py +8 -4
- zenml/cli/stack.py +2 -2
- zenml/config/pipeline_configurations.py +2 -2
- zenml/config/server_config.py +20 -0
- zenml/constants.py +5 -0
- zenml/event_hub/base_event_hub.py +2 -2
- zenml/integrations/airflow/orchestrators/airflow_orchestrator.py +4 -2
- zenml/integrations/airflow/orchestrators/dag_generator.py +16 -0
- zenml/integrations/gcp/__init__.py +3 -0
- zenml/integrations/gcp/experiment_trackers/__init__.py +18 -0
- zenml/integrations/gcp/experiment_trackers/vertex_experiment_tracker.py +214 -0
- zenml/integrations/gcp/flavors/__init__.py +6 -0
- zenml/integrations/gcp/flavors/vertex_experiment_tracker_flavor.py +199 -0
- zenml/integrations/kubernetes/orchestrators/kube_utils.py +2 -2
- zenml/integrations/mlflow/experiment_trackers/mlflow_experiment_tracker.py +0 -1
- zenml/integrations/slack/__init__.py +1 -2
- zenml/integrations/slack/alerters/slack_alerter.py +119 -61
- zenml/integrations/slack/flavors/slack_alerter_flavor.py +18 -8
- zenml/integrations/whylogs/data_validators/whylogs_data_validator.py +3 -1
- zenml/materializers/built_in_materializer.py +17 -2
- zenml/models/v2/core/api_key.py +2 -2
- zenml/orchestrators/publish_utils.py +4 -4
- zenml/orchestrators/step_launcher.py +3 -3
- zenml/orchestrators/step_run_utils.py +2 -2
- zenml/pipelines/pipeline_definition.py +1 -1
- zenml/pipelines/run_utils.py +2 -2
- zenml/service_connectors/service_connector.py +2 -2
- zenml/stack/stack.py +3 -3
- zenml/stack_deployments/stack_deployment.py +5 -0
- zenml/utils/git_utils.py +1 -1
- zenml/utils/string_utils.py +2 -2
- zenml/utils/yaml_utils.py +3 -4
- zenml/zen_server/auth.py +13 -6
- zenml/zen_server/dashboard/assets/{404-Dfq64Boz.js → 404-c8OuXDAT.js} +1 -1
- zenml/zen_server/dashboard/assets/{@reactflow-BUNIMFeC.js → @reactflow-6JPoencd.js} +1 -1
- zenml/zen_server/dashboard/assets/{AlertDialogDropdownItem-B73Vs10T.js → AlertDialogDropdownItem-8yPFDxEI.js} +1 -1
- zenml/zen_server/dashboard/assets/{CodeSnippet-DIJRT2NT.js → CodeSnippet-Qh1ae_DJ.js} +1 -1
- zenml/zen_server/dashboard/assets/{CollapsibleCard-BzUHGZOU.js → CollapsibleCard-TiI4lId1.js} +1 -1
- zenml/zen_server/dashboard/assets/{Commands-BEGyld4c.js → Commands-BcR2Arie.js} +1 -1
- zenml/zen_server/dashboard/assets/{ComponentBadge-xyKiek1s.js → ComponentBadge-BqQNUZgb.js} +1 -1
- zenml/zen_server/dashboard/assets/{CopyButton-DhW-mapu.js → CopyButton-DCiXO3JC.js} +1 -1
- zenml/zen_server/dashboard/assets/{CsvVizualization-D8oazBiE.js → CsvVizualization-O9cVIaL8.js} +1 -1
- zenml/zen_server/dashboard/assets/{DeleteAlertDialog-WkSIIgfy.js → DeleteAlertDialog-DrPjHtXX.js} +1 -1
- zenml/zen_server/dashboard/assets/{DialogItem-Bgroeg29.js → DialogItem-BYG7d_M2.js} +1 -1
- zenml/zen_server/dashboard/assets/{Error-CY5tlu17.js → Error-C1zbWr19.js} +1 -1
- zenml/zen_server/dashboard/assets/{ExecutionStatus-G8mjIaeA.js → ExecutionStatus-Ct9srgHC.js} +1 -1
- zenml/zen_server/dashboard/assets/{Helpbox-Bb1ed--O.js → Helpbox-Bm_1Zx9f.js} +1 -1
- zenml/zen_server/dashboard/assets/{Infobox-Da6-76M2.js → Infobox-OQdkCLSP.js} +1 -1
- zenml/zen_server/dashboard/assets/{InlineAvatar-DqnZaBNq.js → InlineAvatar-CQNjKoEQ.js} +1 -1
- zenml/zen_server/dashboard/assets/{NestedCollapsible-aK5ojKoF.js → NestedCollapsible-DDgd2SGb.js} +1 -1
- zenml/zen_server/dashboard/assets/Partials-MD3e95Dk.js +1 -0
- zenml/zen_server/dashboard/assets/{ProBadge-B4tRUYve.js → ProBadge-D784iVNC.js} +1 -1
- zenml/zen_server/dashboard/assets/{ProCta-CZuP29Qz.js → ProCta-W2PEvNow.js} +1 -1
- zenml/zen_server/dashboard/assets/{ProviderIcon-Bd7GUQ1_.js → ProviderIcon-DfDUOeAy.js} +1 -1
- zenml/zen_server/dashboard/assets/{ProviderRadio-mstdqzsS.js → ProviderRadio-B81Elxrc.js} +1 -1
- zenml/zen_server/dashboard/assets/{RunSelector-CsruSB4i.js → RunSelector-DOXgdry5.js} +1 -1
- zenml/zen_server/dashboard/assets/{RunsBody-DxxtWVYz.js → RunsBody-Bnx2fxub.js} +1 -1
- zenml/zen_server/dashboard/assets/SearchField-Yjv-KRW4.js +1 -0
- zenml/zen_server/dashboard/assets/{SecretTooltip-CLzJIYW_.js → SecretTooltip-EKpMlG2f.js} +1 -1
- zenml/zen_server/dashboard/assets/{SetPassword-Yn50ooBC.js → SetPassword-CDLy57PZ.js} +1 -1
- zenml/zen_server/dashboard/assets/StackList-DKQaLDo4.js +1 -0
- zenml/zen_server/dashboard/assets/{Tabs-CNv-eTYM.js → Tabs-B5E-o_h6.js} +1 -1
- zenml/zen_server/dashboard/assets/{Tick-jEIevzVf.js → Tick-DSYBiuXU.js} +1 -1
- zenml/zen_server/dashboard/assets/{UpdatePasswordSchemas-C16GW-kX.js → UpdatePasswordSchemas-HBNOeyoP.js} +1 -1
- zenml/zen_server/dashboard/assets/{UsageReason-Bf2tzhv1.js → UsageReason-DXtPS5nE.js} +1 -1
- zenml/zen_server/dashboard/assets/{WizardFooter-D6i-AP1K.js → WizardFooter-_1VSMZ_c.js} +1 -1
- zenml/zen_server/dashboard/assets/{all-pipeline-runs-query-DUti43aF.js → all-pipeline-runs-query-D0qDLdKB.js} +1 -1
- zenml/zen_server/dashboard/assets/{create-stack-Ch2WPs9U.js → create-stack-7JzgAYAm.js} +1 -1
- zenml/zen_server/dashboard/assets/{delete-run-Byf9hTjA.js → delete-run-CUdtYFLl.js} +1 -1
- zenml/zen_server/dashboard/assets/{form-schemas-BZqKBPBF.js → form-schemas-B6PCV3Y4.js} +1 -1
- zenml/zen_server/dashboard/assets/index-B6U0OkEN.css +1 -0
- zenml/zen_server/dashboard/assets/{index-CyBKZcpO.js → index-CJ5IfeAl.js} +1 -1
- zenml/zen_server/dashboard/assets/{index-CtdYkjUi.js → index-Ceyzb1yI.js} +1 -1
- zenml/zen_server/dashboard/assets/{index-CE0aQlv8.js → index-CxO6541P.js} +3 -3
- zenml/zen_server/dashboard/assets/{index-v6gQjDEo.js → index-D4yoZ_gH.js} +1 -1
- zenml/zen_server/dashboard/assets/{login-mutation-DNDVp_2H.js → login-mutation-BaeJ7MAg.js} +1 -1
- zenml/zen_server/dashboard/assets/{not-found-Bmup4ctE.js → not-found-MGptrNBk.js} +1 -1
- zenml/zen_server/dashboard/assets/{page-DGlm1RVc.js → page-Aeu3v0MQ.js} +1 -1
- zenml/zen_server/dashboard/assets/{page-CltCNL0T.js → page-BCgEdmhP.js} +1 -1
- zenml/zen_server/dashboard/assets/{page-Hn8q9iJZ.js → page-BKwwfTNy.js} +1 -1
- zenml/zen_server/dashboard/assets/{page-BNxYrN0q.js → page-BUjw8Tp1.js} +1 -1
- zenml/zen_server/dashboard/assets/{page-BYJfqgLN.js → page-BXgXP-Qj.js} +1 -1
- zenml/zen_server/dashboard/assets/{page-DN4BVIOL.js → page-BXrtxEbw.js} +1 -1
- zenml/zen_server/dashboard/assets/{page-CHRn1fQm.js → page-BaUDR9Ri.js} +1 -1
- zenml/zen_server/dashboard/assets/{page-DlIi5ThM.js → page-BbljjC-k.js} +1 -1
- zenml/zen_server/dashboard/assets/{page-BrmJp1Wt.js → page-BhOXn-s9.js} +1 -1
- zenml/zen_server/dashboard/assets/{page-Cc8ZEuj4.js → page-C37IDa-Q.js} +1 -1
- zenml/zen_server/dashboard/assets/{page-Dif8CWyZ.js → page-C4JpDeUM.js} +1 -1
- zenml/zen_server/dashboard/assets/{page-BC27C_OI.js → page-CB2_GdBA.js} +1 -1
- zenml/zen_server/dashboard/assets/{page-B5Sr8pib.js → page-CBiT2Ox9.js} +1 -1
- zenml/zen_server/dashboard/assets/{page-IhckKFnD.js → page-CXPc-HN1.js} +1 -1
- zenml/zen_server/dashboard/assets/{page-Dth9X1Ih.js → page-CbwI6emp.js} +1 -1
- zenml/zen_server/dashboard/assets/{page-DweqqCkF.js → page-CeNL9JWi.js} +1 -1
- zenml/zen_server/dashboard/assets/{page-LyZ_l8vR.js → page-CkPwPmLZ.js} +1 -1
- zenml/zen_server/dashboard/assets/{page-C70wZtV2.js → page-CmJU3Gqo.js} +1 -1
- zenml/zen_server/dashboard/assets/{page-D9Oh05fl.js → page-CoFVtzhG.js} +1 -1
- zenml/zen_server/dashboard/assets/{page-PamGpk0j.js → page-D-KPzeQb.js} +1 -1
- zenml/zen_server/dashboard/assets/{page-DoW7YxTu.js → page-DKQ3wZgr.js} +1 -1
- zenml/zen_server/dashboard/assets/page-DWWhxCoF.js +1 -0
- zenml/zen_server/dashboard/assets/{page-CmlYj7Nl.js → page-DbW8MfQ4.js} +1 -1
- zenml/zen_server/dashboard/assets/{page-CWr96ZKN.js → page-Dv5lN2w7.js} +1 -1
- zenml/zen_server/dashboard/assets/{page-ANYGfEUL.js → page-Dvbq1BoF.js} +1 -1
- zenml/zen_server/dashboard/assets/{page-D6Ev5P8V.js → page-DyAuja95.js} +1 -1
- zenml/zen_server/dashboard/assets/{page-DyOJ_pq3.js → page-DzrdL2v1.js} +1 -1
- zenml/zen_server/dashboard/assets/{page-CXAbSyp9.js → page-I2B4Ocv8.js} +1 -1
- zenml/zen_server/dashboard/assets/page-OdjGauvw.js +2 -0
- zenml/zen_server/dashboard/assets/{page-CaeI9ptC.js → page-Ox-eC1ik.js} +1 -1
- zenml/zen_server/dashboard/assets/{page-B_0XkV48.js → page-khp8QJ6b.js} +1 -1
- zenml/zen_server/dashboard/assets/{page--XLMzHrn.js → page-yNh6PQKt.js} +1 -1
- zenml/zen_server/dashboard/assets/{persist-vP0-Xl4f.js → persist-DBTFy--v.js} +1 -1
- zenml/zen_server/dashboard/assets/{persist-DeXRG61d.js → persist-K7AY0ju4.js} +1 -1
- zenml/zen_server/dashboard/assets/{service-DH_oUqQj.js → service-BvOYLH5b.js} +1 -1
- zenml/zen_server/dashboard/assets/{sharedSchema-Bw1_Wa7l.js → sharedSchema-xJDsJNgJ.js} +1 -1
- zenml/zen_server/dashboard/assets/{stack-detail-query-B_0R_fd6.js → stack-detail-query-DMJoxwgv.js} +1 -1
- zenml/zen_server/dashboard/assets/{update-server-settings-mutation-D9qYhfaN.js → update-server-settings-mutation-ATZDNNZk.js} +1 -1
- zenml/zen_server/dashboard/assets/{url-Dh93fvh0.js → url-BWJXzuI4.js} +1 -1
- zenml/zen_server/dashboard/index.html +4 -4
- zenml/zen_server/deploy/helm/Chart.yaml +1 -1
- zenml/zen_server/deploy/helm/README.md +2 -2
- zenml/zen_server/deploy/helm/templates/server-db-job.yaml +5 -3
- zenml/zen_server/deploy/helm/values.yaml +4 -0
- zenml/zen_server/routers/devices_endpoints.py +4 -2
- zenml/zen_stores/migrations/versions/0.73.0_release.py +23 -0
- zenml/zen_stores/migrations/versions/25155145c545_separate_actions_and_triggers.py +2 -2
- zenml/zen_stores/migrations/versions/46506f72f0ed_add_server_settings.py +2 -2
- zenml/zen_stores/migrations/versions/5994f9ad0489_introduce_role_permissions.py +6 -6
- zenml/zen_stores/migrations/versions/7500f434b71c_remove_shared_columns.py +2 -2
- zenml/zen_stores/migrations/versions/a91762e6be36_artifact_version_table.py +3 -3
- zenml/zen_stores/schemas/action_schemas.py +2 -2
- zenml/zen_stores/schemas/api_key_schemas.py +4 -4
- zenml/zen_stores/schemas/artifact_schemas.py +3 -3
- zenml/zen_stores/schemas/base_schemas.py +7 -3
- zenml/zen_stores/schemas/code_repository_schemas.py +2 -2
- zenml/zen_stores/schemas/component_schemas.py +2 -2
- zenml/zen_stores/schemas/device_schemas.py +4 -4
- zenml/zen_stores/schemas/event_source_schemas.py +2 -2
- zenml/zen_stores/schemas/flavor_schemas.py +2 -2
- zenml/zen_stores/schemas/model_schemas.py +3 -3
- zenml/zen_stores/schemas/pipeline_run_schemas.py +3 -3
- zenml/zen_stores/schemas/pipeline_schemas.py +2 -2
- zenml/zen_stores/schemas/run_template_schemas.py +2 -2
- zenml/zen_stores/schemas/schedule_schema.py +2 -2
- zenml/zen_stores/schemas/secret_schemas.py +2 -2
- zenml/zen_stores/schemas/server_settings_schemas.py +9 -5
- zenml/zen_stores/schemas/service_connector_schemas.py +2 -2
- zenml/zen_stores/schemas/service_schemas.py +2 -2
- zenml/zen_stores/schemas/stack_schemas.py +2 -2
- zenml/zen_stores/schemas/step_run_schemas.py +2 -2
- zenml/zen_stores/schemas/tag_schemas.py +2 -2
- zenml/zen_stores/schemas/trigger_schemas.py +2 -2
- zenml/zen_stores/schemas/user_schemas.py +3 -3
- zenml/zen_stores/schemas/workspace_schemas.py +2 -2
- zenml/zen_stores/sql_zen_store.py +10 -1
- {zenml_nightly-0.72.0.dev20250120.dist-info → zenml_nightly-0.73.0.dev20250123.dist-info}/METADATA +2 -2
- {zenml_nightly-0.72.0.dev20250120.dist-info → zenml_nightly-0.73.0.dev20250123.dist-info}/RECORD +159 -155
- zenml/zen_server/dashboard/assets/Partials-CqZp5NMX.js +0 -1
- zenml/zen_server/dashboard/assets/SearchField-D6tPxyqw.js +0 -1
- zenml/zen_server/dashboard/assets/StackList-U537qoYd.js +0 -1
- zenml/zen_server/dashboard/assets/index-DXvT1_Um.css +0 -1
- zenml/zen_server/dashboard/assets/page-C2nU3Gxn.js +0 -1
- zenml/zen_server/dashboard/assets/page-PxOWfKgF.js +0 -2
- {zenml_nightly-0.72.0.dev20250120.dist-info → zenml_nightly-0.73.0.dev20250123.dist-info}/LICENSE +0 -0
- {zenml_nightly-0.72.0.dev20250120.dist-info → zenml_nightly-0.73.0.dev20250123.dist-info}/WHEEL +0 -0
- {zenml_nightly-0.72.0.dev20250120.dist-info → zenml_nightly-0.73.0.dev20250123.dist-info}/entry_points.txt +0 -0
@@ -30,8 +30,7 @@ class SlackIntegration(Integration):
|
|
30
30
|
"""
|
31
31
|
|
32
32
|
NAME = SLACK
|
33
|
-
REQUIREMENTS = ["slack-sdk
|
34
|
-
REQUIREMENTS_IGNORED_ON_UNINSTALL = ["aiohttp"]
|
33
|
+
REQUIREMENTS = ["slack-sdk==3.30.0"]
|
35
34
|
|
36
35
|
@classmethod
|
37
36
|
def flavors(cls) -> List[Type[Flavor]]:
|
@@ -14,12 +14,12 @@
|
|
14
14
|
# or implied. See the License for the specific language governing
|
15
15
|
# permissions and limitations under the License.
|
16
16
|
|
17
|
-
|
17
|
+
import time
|
18
|
+
from typing import Dict, List, Optional, Type, cast
|
18
19
|
|
19
20
|
from pydantic import BaseModel
|
20
21
|
from slack_sdk import WebClient
|
21
22
|
from slack_sdk.errors import SlackApiError
|
22
|
-
from slack_sdk.rtm import RTMClient
|
23
23
|
|
24
24
|
from zenml import get_step_context
|
25
25
|
from zenml.alerter.base_alerter import BaseAlerter, BaseAlerterStepParameters
|
@@ -58,7 +58,7 @@ class SlackAlerterParameters(BaseAlerterStepParameters):
|
|
58
58
|
payload: Optional[SlackAlerterPayload] = None
|
59
59
|
include_format_blocks: Optional[bool] = True
|
60
60
|
|
61
|
-
# Allowing user to use their own custom blocks in the
|
61
|
+
# Allowing user to use their own custom blocks in the Slack post message
|
62
62
|
blocks: Optional[List[Dict]] = None # type: ignore
|
63
63
|
|
64
64
|
|
@@ -96,7 +96,7 @@ class SlackAlerter(BaseAlerter):
|
|
96
96
|
|
97
97
|
Raises:
|
98
98
|
RuntimeError: if config is not of type `BaseAlerterStepConfig`.
|
99
|
-
ValueError: if a
|
99
|
+
ValueError: if a Slack channel was neither defined in the config
|
100
100
|
nor in the slack alerter component.
|
101
101
|
"""
|
102
102
|
if params and not isinstance(params, BaseAlerterStepParameters):
|
@@ -111,24 +111,48 @@ class SlackAlerter(BaseAlerter):
|
|
111
111
|
):
|
112
112
|
return params.slack_channel_id
|
113
113
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
114
|
+
try:
|
115
|
+
settings = cast(
|
116
|
+
SlackAlerterSettings,
|
117
|
+
self.get_settings(get_step_context().step_run),
|
118
|
+
)
|
119
|
+
except RuntimeError:
|
120
|
+
settings = None
|
121
|
+
|
122
|
+
if settings is not None and settings.slack_channel_id is not None:
|
119
123
|
return settings.slack_channel_id
|
120
124
|
|
121
|
-
if self.config.
|
122
|
-
return self.config.
|
125
|
+
if self.config.slack_channel_id is not None:
|
126
|
+
return self.config.slack_channel_id
|
123
127
|
|
124
128
|
raise ValueError(
|
125
|
-
"
|
126
|
-
"
|
127
|
-
"
|
129
|
+
"The `slack_channel_id` is not set either in the runtime settings, "
|
130
|
+
"or the component configuration of the alerter. Please specify at "
|
131
|
+
"least one."
|
128
132
|
)
|
129
133
|
|
134
|
+
def _get_timeout_duration(self) -> int:
|
135
|
+
"""Gets the timeout duration used by the ask method .
|
136
|
+
|
137
|
+
Returns:
|
138
|
+
number of seconds for the timeout to happen.
|
139
|
+
"""
|
140
|
+
try:
|
141
|
+
settings = cast(
|
142
|
+
SlackAlerterSettings,
|
143
|
+
self.get_settings(get_step_context().step_run),
|
144
|
+
)
|
145
|
+
except RuntimeError:
|
146
|
+
settings = None
|
147
|
+
|
148
|
+
if settings is not None:
|
149
|
+
return settings.timeout
|
150
|
+
|
151
|
+
return self.config.timeout
|
152
|
+
|
153
|
+
@staticmethod
|
130
154
|
def _get_approve_msg_options(
|
131
|
-
|
155
|
+
params: Optional[BaseAlerterStepParameters],
|
132
156
|
) -> List[str]:
|
133
157
|
"""Define which messages will lead to approval during ask().
|
134
158
|
|
@@ -146,8 +170,9 @@ class SlackAlerter(BaseAlerter):
|
|
146
170
|
return params.approve_msg_options
|
147
171
|
return DEFAULT_APPROVE_MSG_OPTIONS
|
148
172
|
|
173
|
+
@staticmethod
|
149
174
|
def _get_disapprove_msg_options(
|
150
|
-
|
175
|
+
params: Optional[BaseAlerterStepParameters],
|
151
176
|
) -> List[str]:
|
152
177
|
"""Define which messages will lead to disapproval during ask().
|
153
178
|
|
@@ -165,8 +190,8 @@ class SlackAlerter(BaseAlerter):
|
|
165
190
|
return params.disapprove_msg_options
|
166
191
|
return DEFAULT_DISAPPROVE_MSG_OPTIONS
|
167
192
|
|
193
|
+
@staticmethod
|
168
194
|
def _create_blocks(
|
169
|
-
self,
|
170
195
|
message: Optional[str],
|
171
196
|
params: Optional[BaseAlerterStepParameters],
|
172
197
|
) -> List[Dict]: # type: ignore
|
@@ -185,7 +210,8 @@ class SlackAlerter(BaseAlerter):
|
|
185
210
|
return params.blocks
|
186
211
|
elif hasattr(params, "payload") and params.payload is not None:
|
187
212
|
logger.info(
|
188
|
-
"No custom blocks set. Using default blocks for Slack
|
213
|
+
"No custom blocks set. Using default blocks for Slack "
|
214
|
+
"alerter."
|
189
215
|
)
|
190
216
|
payload = params.payload
|
191
217
|
return [
|
@@ -228,7 +254,8 @@ class SlackAlerter(BaseAlerter):
|
|
228
254
|
return []
|
229
255
|
else:
|
230
256
|
logger.info(
|
231
|
-
"params is not of type SlackAlerterParameters. Returning empty
|
257
|
+
"params is not of type SlackAlerterParameters. Returning empty "
|
258
|
+
"blocks."
|
232
259
|
)
|
233
260
|
return []
|
234
261
|
|
@@ -253,64 +280,95 @@ class SlackAlerter(BaseAlerter):
|
|
253
280
|
response = client.chat_postMessage(
|
254
281
|
channel=slack_channel_id, text=message, blocks=blocks
|
255
282
|
)
|
283
|
+
if not response.get("ok", False):
|
284
|
+
error_details = response.get("error", "Unknown error")
|
285
|
+
logger.error(
|
286
|
+
f"Failed to send message to Slack channel. "
|
287
|
+
f"Error: {error_details}. Full response: {response}"
|
288
|
+
)
|
289
|
+
return False
|
256
290
|
return True
|
257
291
|
except SlackApiError as error:
|
258
|
-
|
259
|
-
logger.error(
|
292
|
+
error_message = error.response.get("error", "Unknown error")
|
293
|
+
logger.error(
|
294
|
+
"SlackAlerter.post() failed with Slack API error: "
|
295
|
+
f"{error_message}. Full response: {error.response}"
|
296
|
+
)
|
297
|
+
return False
|
298
|
+
except Exception as e:
|
299
|
+
logger.error(f"Unexpected error in SlackAlerter.post(): {str(e)}")
|
260
300
|
return False
|
261
301
|
|
262
302
|
def ask(
|
263
|
-
self,
|
303
|
+
self, question: str, params: Optional[BaseAlerterStepParameters] = None
|
264
304
|
) -> bool:
|
265
305
|
"""Post a message to a Slack channel and wait for approval.
|
266
306
|
|
267
307
|
Args:
|
268
|
-
|
308
|
+
question: Initial message to be posted.
|
269
309
|
params: Optional parameters.
|
270
310
|
|
271
311
|
Returns:
|
272
312
|
True if a user approved the operation, else False
|
273
313
|
"""
|
274
|
-
rtm = RTMClient(token=self.config.slack_token)
|
275
314
|
slack_channel_id = self._get_channel_id(params=params)
|
276
315
|
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
def post_initial_message(**payload: Any) -> None:
|
281
|
-
"""Post an initial message in a channel and start listening.
|
316
|
+
client = WebClient(token=self.config.slack_token)
|
317
|
+
approve_options = self._get_approve_msg_options(params)
|
318
|
+
disapprove_options = self._get_disapprove_msg_options(params)
|
282
319
|
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
channel=slack_channel_id, text=message, blocks=blocks
|
320
|
+
try:
|
321
|
+
# Send message to the Slack channel
|
322
|
+
response = client.chat_postMessage(
|
323
|
+
channel=slack_channel_id,
|
324
|
+
text=question,
|
325
|
+
blocks=self._create_blocks(question, params),
|
290
326
|
)
|
291
327
|
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
328
|
+
if not response.get("ok", False):
|
329
|
+
error_details = response.get("error", "Unknown error")
|
330
|
+
logger.error(
|
331
|
+
f"Failed to send the initial message to the Slack channel. "
|
332
|
+
f"Error: {error_details}. Full response: {response}"
|
333
|
+
)
|
334
|
+
return False
|
335
|
+
|
336
|
+
# Retrieve timestamp of sent message
|
337
|
+
timestamp = response["ts"]
|
338
|
+
|
339
|
+
# Wait for a response
|
340
|
+
start_time = time.time()
|
341
|
+
|
342
|
+
while time.time() - start_time < self._get_timeout_duration():
|
343
|
+
history = client.conversations_history(
|
344
|
+
channel=slack_channel_id, oldest=timestamp
|
345
|
+
)
|
346
|
+
for msg in history["messages"]:
|
347
|
+
if "ts" in msg and "user" in msg:
|
348
|
+
user_message = msg["text"].strip().lower()
|
349
|
+
if user_message in [
|
350
|
+
opt.lower() for opt in approve_options
|
351
|
+
]:
|
352
|
+
logger.info("User approved the operation.")
|
353
|
+
return True
|
354
|
+
elif user_message in [
|
355
|
+
opt.lower() for opt in disapprove_options
|
356
|
+
]:
|
357
|
+
logger.info("User disapproved the operation.")
|
358
|
+
return False
|
359
|
+
|
360
|
+
time.sleep(1) # Polling interval
|
361
|
+
|
362
|
+
logger.warning("No response received within the timeout period.")
|
363
|
+
return False
|
364
|
+
|
365
|
+
except SlackApiError as error:
|
366
|
+
error_message = error.response.get("error", "Unknown error")
|
367
|
+
logger.error(
|
368
|
+
f"SlackAlerter.ask() failed with Slack API error: "
|
369
|
+
f"{error_message}. Full response: {error.response}"
|
370
|
+
)
|
371
|
+
return False
|
372
|
+
except Exception as e:
|
373
|
+
logger.error(f"Unexpected error in SlackAlerter.ask(): {str(e)}")
|
374
|
+
return False
|
@@ -19,6 +19,7 @@ from zenml.alerter.base_alerter import BaseAlerterConfig, BaseAlerterFlavor
|
|
19
19
|
from zenml.config.base_settings import BaseSettings
|
20
20
|
from zenml.integrations.slack import SLACK_ALERTER_FLAVOR
|
21
21
|
from zenml.logger import get_logger
|
22
|
+
from zenml.utils.deprecation_utils import deprecate_pydantic_attributes
|
22
23
|
from zenml.utils.secret_utils import SecretField
|
23
24
|
|
24
25
|
logger = get_logger(__name__)
|
@@ -32,21 +33,28 @@ class SlackAlerterSettings(BaseSettings):
|
|
32
33
|
|
33
34
|
Attributes:
|
34
35
|
slack_channel_id: The ID of the Slack channel to use for communication.
|
36
|
+
timeout: The amount of seconds to wait for the ask method.
|
35
37
|
"""
|
36
38
|
|
37
39
|
slack_channel_id: Optional[str] = None
|
40
|
+
timeout: int = 300
|
38
41
|
|
39
42
|
|
40
|
-
class SlackAlerterConfig(BaseAlerterConfig):
|
43
|
+
class SlackAlerterConfig(BaseAlerterConfig, SlackAlerterSettings):
|
41
44
|
"""Slack alerter config.
|
42
45
|
|
43
46
|
Attributes:
|
44
47
|
slack_token: The Slack token tied to the Slack account to be used.
|
45
|
-
default_slack_channel_id: The ID of the default Slack
|
48
|
+
default_slack_channel_id: (deprecated) The ID of the default Slack
|
49
|
+
channel to use for communication.
|
46
50
|
"""
|
47
51
|
|
48
52
|
slack_token: str = SecretField()
|
53
|
+
|
49
54
|
default_slack_channel_id: Optional[str] = None
|
55
|
+
_deprecation_validator = deprecate_pydantic_attributes(
|
56
|
+
("default_slack_channel_id", "slack_channel_id")
|
57
|
+
)
|
50
58
|
|
51
59
|
@property
|
52
60
|
def is_valid(self) -> bool:
|
@@ -60,9 +68,11 @@ class SlackAlerterConfig(BaseAlerterConfig):
|
|
60
68
|
from slack_sdk.errors import SlackApiError
|
61
69
|
except ImportError:
|
62
70
|
logger.warning(
|
63
|
-
"Unable to validate
|
71
|
+
"Unable to validate the slack alerter, because the Slack "
|
72
|
+
"integration is not installed."
|
64
73
|
)
|
65
74
|
return True
|
75
|
+
|
66
76
|
client = WebClient(token=self.slack_token)
|
67
77
|
try:
|
68
78
|
# Check slack token validity
|
@@ -70,10 +80,10 @@ class SlackAlerterConfig(BaseAlerterConfig):
|
|
70
80
|
if not response["ok"]:
|
71
81
|
return False
|
72
82
|
|
73
|
-
if self.
|
83
|
+
if self.slack_channel_id:
|
74
84
|
# Check channel validity
|
75
85
|
response = client.conversations_info(
|
76
|
-
channel=self.
|
86
|
+
channel=self.slack_channel_id
|
77
87
|
)
|
78
88
|
valid: bool = response["ok"]
|
79
89
|
return valid
|
@@ -97,7 +107,7 @@ class SlackAlerterFlavor(BaseAlerterFlavor):
|
|
97
107
|
|
98
108
|
@property
|
99
109
|
def docs_url(self) -> Optional[str]:
|
100
|
-
"""A
|
110
|
+
"""A URL to point at docs explaining this flavor.
|
101
111
|
|
102
112
|
Returns:
|
103
113
|
A flavor docs url.
|
@@ -106,7 +116,7 @@ class SlackAlerterFlavor(BaseAlerterFlavor):
|
|
106
116
|
|
107
117
|
@property
|
108
118
|
def sdk_docs_url(self) -> Optional[str]:
|
109
|
-
"""A
|
119
|
+
"""A URL to point at SDK docs explaining this flavor.
|
110
120
|
|
111
121
|
Returns:
|
112
122
|
A flavor SDK docs url.
|
@@ -115,7 +125,7 @@ class SlackAlerterFlavor(BaseAlerterFlavor):
|
|
115
125
|
|
116
126
|
@property
|
117
127
|
def logo_url(self) -> str:
|
118
|
-
"""A
|
128
|
+
"""A URL to represent the flavor in the dashboard.
|
119
129
|
|
120
130
|
Returns:
|
121
131
|
The flavor logo.
|
@@ -97,7 +97,9 @@ class WhylogsDataValidator(BaseDataValidator, AuthenticationMixin):
|
|
97
97
|
"""
|
98
98
|
results = why.log(pandas=dataset)
|
99
99
|
profile = results.profile()
|
100
|
-
dataset_timestamp = dataset_timestamp or datetime.datetime.
|
100
|
+
dataset_timestamp = dataset_timestamp or datetime.datetime.now(
|
101
|
+
datetime.timezone.utc
|
102
|
+
)
|
101
103
|
profile.set_dataset_timestamp(dataset_timestamp=dataset_timestamp)
|
102
104
|
return profile.view()
|
103
105
|
|
@@ -28,6 +28,10 @@ from typing import (
|
|
28
28
|
)
|
29
29
|
|
30
30
|
from zenml.artifact_stores.base_artifact_store import BaseArtifactStore
|
31
|
+
from zenml.constants import (
|
32
|
+
ENV_ZENML_MATERIALIZER_ALLOW_NON_ASCII_JSON_DUMPS,
|
33
|
+
handle_bool_env_var,
|
34
|
+
)
|
31
35
|
from zenml.enums import ArtifactType, VisualizationType
|
32
36
|
from zenml.logger import get_logger
|
33
37
|
from zenml.materializers.base_materializer import BaseMaterializer
|
@@ -48,6 +52,9 @@ BASIC_TYPES = (
|
|
48
52
|
str,
|
49
53
|
type(None),
|
50
54
|
) # complex/bytes are not JSON serializable
|
55
|
+
ZENML_MATERIALIZER_ALLOW_NON_ASCII_JSON_DUMPS = handle_bool_env_var(
|
56
|
+
ENV_ZENML_MATERIALIZER_ALLOW_NON_ASCII_JSON_DUMPS, False
|
57
|
+
)
|
51
58
|
|
52
59
|
|
53
60
|
class BuiltInMaterializer(BaseMaterializer):
|
@@ -94,7 +101,11 @@ class BuiltInMaterializer(BaseMaterializer):
|
|
94
101
|
Args:
|
95
102
|
data: The data to store.
|
96
103
|
"""
|
97
|
-
yaml_utils.write_json(
|
104
|
+
yaml_utils.write_json(
|
105
|
+
self.data_path,
|
106
|
+
data,
|
107
|
+
ensure_ascii=not ZENML_MATERIALIZER_ALLOW_NON_ASCII_JSON_DUMPS,
|
108
|
+
)
|
98
109
|
|
99
110
|
def extract_metadata(
|
100
111
|
self, data: Union[bool, float, int, str]
|
@@ -371,7 +382,11 @@ class BuiltInContainerMaterializer(BaseMaterializer):
|
|
371
382
|
|
372
383
|
# If the data is serializable, just write it into a single JSON file.
|
373
384
|
if _is_serializable(data):
|
374
|
-
yaml_utils.write_json(
|
385
|
+
yaml_utils.write_json(
|
386
|
+
self.data_path,
|
387
|
+
data,
|
388
|
+
ensure_ascii=not ZENML_MATERIALIZER_ALLOW_NON_ASCII_JSON_DUMPS,
|
389
|
+
)
|
375
390
|
return
|
376
391
|
|
377
392
|
# non-serializable dict: Handle as non-serializable list of lists.
|
zenml/models/v2/core/api_key.py
CHANGED
@@ -13,7 +13,7 @@
|
|
13
13
|
# permissions and limitations under the License.
|
14
14
|
"""Models representing API keys."""
|
15
15
|
|
16
|
-
from datetime import datetime, timedelta
|
16
|
+
from datetime import datetime, timedelta, timezone
|
17
17
|
from typing import TYPE_CHECKING, ClassVar, List, Optional, Type, Union
|
18
18
|
from uuid import UUID
|
19
19
|
|
@@ -319,7 +319,7 @@ class APIKeyInternalResponse(APIKeyResponse):
|
|
319
319
|
and self.retain_period_minutes > 0
|
320
320
|
):
|
321
321
|
# check if the previous key is still valid
|
322
|
-
if datetime.
|
322
|
+
if datetime.now(timezone.utc) - self.last_rotated < timedelta(
|
323
323
|
minutes=self.retain_period_minutes
|
324
324
|
):
|
325
325
|
key_hash = self.previous_key
|
@@ -13,7 +13,7 @@
|
|
13
13
|
# permissions and limitations under the License.
|
14
14
|
"""Utilities to publish pipeline and step runs."""
|
15
15
|
|
16
|
-
from datetime import datetime
|
16
|
+
from datetime import datetime, timezone
|
17
17
|
from typing import TYPE_CHECKING, Dict, List
|
18
18
|
|
19
19
|
from zenml.client import Client
|
@@ -48,7 +48,7 @@ def publish_successful_step_run(
|
|
48
48
|
step_run_id=step_run_id,
|
49
49
|
step_run_update=StepRunUpdate(
|
50
50
|
status=ExecutionStatus.COMPLETED,
|
51
|
-
end_time=datetime.
|
51
|
+
end_time=datetime.now(timezone.utc),
|
52
52
|
outputs=output_artifact_ids,
|
53
53
|
),
|
54
54
|
)
|
@@ -67,7 +67,7 @@ def publish_failed_step_run(step_run_id: "UUID") -> "StepRunResponse":
|
|
67
67
|
step_run_id=step_run_id,
|
68
68
|
step_run_update=StepRunUpdate(
|
69
69
|
status=ExecutionStatus.FAILED,
|
70
|
-
end_time=datetime.
|
70
|
+
end_time=datetime.now(timezone.utc),
|
71
71
|
),
|
72
72
|
)
|
73
73
|
|
@@ -87,7 +87,7 @@ def publish_failed_pipeline_run(
|
|
87
87
|
run_id=pipeline_run_id,
|
88
88
|
run_update=PipelineRunUpdate(
|
89
89
|
status=ExecutionStatus.FAILED,
|
90
|
-
end_time=datetime.
|
90
|
+
end_time=datetime.now(timezone.utc),
|
91
91
|
),
|
92
92
|
)
|
93
93
|
|
@@ -16,7 +16,7 @@
|
|
16
16
|
import os
|
17
17
|
import time
|
18
18
|
from contextlib import nullcontext
|
19
|
-
from datetime import datetime
|
19
|
+
from datetime import datetime, timezone
|
20
20
|
from functools import partial
|
21
21
|
from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple
|
22
22
|
|
@@ -201,7 +201,7 @@ class StepLauncher:
|
|
201
201
|
f"Failed preparing step `{self._step_name}`."
|
202
202
|
)
|
203
203
|
step_run_request.status = ExecutionStatus.FAILED
|
204
|
-
step_run_request.end_time = datetime.
|
204
|
+
step_run_request.end_time = datetime.now(timezone.utc)
|
205
205
|
raise
|
206
206
|
finally:
|
207
207
|
step_run = Client().zen_store.create_run_step(
|
@@ -305,7 +305,7 @@ class StepLauncher:
|
|
305
305
|
The created or existing pipeline run,
|
306
306
|
and a boolean indicating whether the run was created or reused.
|
307
307
|
"""
|
308
|
-
start_time = datetime.
|
308
|
+
start_time = datetime.now(timezone.utc)
|
309
309
|
run_name = string_utils.format_name_template(
|
310
310
|
name_template=self._deployment.run_name_template,
|
311
311
|
substitutions=self._deployment.pipeline_configuration._get_full_substitutions(
|
@@ -13,7 +13,7 @@
|
|
13
13
|
# permissions and limitations under the License.
|
14
14
|
"""Utilities for creating step runs."""
|
15
15
|
|
16
|
-
from datetime import datetime
|
16
|
+
from datetime import datetime, timezone
|
17
17
|
from typing import Dict, List, Optional, Set, Tuple
|
18
18
|
|
19
19
|
from zenml.client import Client
|
@@ -75,7 +75,7 @@ class StepRunRequestFactory:
|
|
75
75
|
pipeline_run_id=self.pipeline_run.id,
|
76
76
|
deployment=self.deployment.id,
|
77
77
|
status=ExecutionStatus.RUNNING,
|
78
|
-
start_time=datetime.
|
78
|
+
start_time=datetime.now(timezone.utc),
|
79
79
|
user=Client().active_user.id,
|
80
80
|
workspace=Client().active_workspace.id,
|
81
81
|
)
|
@@ -474,7 +474,7 @@ def pipeline_(param_name: str):
|
|
474
474
|
step_name()
|
475
475
|
|
476
476
|
if __name__=="__main__":
|
477
|
-
pipeline_.with_options(
|
477
|
+
pipeline_.with_options(config_path="config.yaml")(param_name="value2")
|
478
478
|
```
|
479
479
|
To avoid this consider setting pipeline parameters only in one place (config or code).
|
480
480
|
"""
|
zenml/pipelines/run_utils.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
"""Utility functions for running pipelines."""
|
2
2
|
|
3
3
|
import time
|
4
|
-
from datetime import datetime
|
4
|
+
from datetime import datetime, timezone
|
5
5
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Union
|
6
6
|
from uuid import UUID
|
7
7
|
|
@@ -65,7 +65,7 @@ def create_placeholder_run(
|
|
65
65
|
|
66
66
|
if deployment.schedule:
|
67
67
|
return None
|
68
|
-
start_time = datetime.
|
68
|
+
start_time = datetime.now(timezone.utc)
|
69
69
|
run_request = PipelineRunRequest(
|
70
70
|
name=string_utils.format_name_template(
|
71
71
|
name_template=deployment.run_name_template,
|
@@ -799,8 +799,8 @@ class ServiceConnector(BaseModel, metaclass=ServiceConnectorMeta):
|
|
799
799
|
name=name,
|
800
800
|
body=ServiceConnectorResponseBody(
|
801
801
|
user=user,
|
802
|
-
created=datetime.
|
803
|
-
updated=datetime.
|
802
|
+
created=datetime.now(timezone.utc),
|
803
|
+
updated=datetime.now(timezone.utc),
|
804
804
|
description=description,
|
805
805
|
connector_type=self.get_type(),
|
806
806
|
auth_method=self.auth_method,
|
zenml/stack/stack.py
CHANGED
@@ -16,7 +16,7 @@
|
|
16
16
|
import itertools
|
17
17
|
import json
|
18
18
|
import os
|
19
|
-
from datetime import datetime
|
19
|
+
from datetime import datetime, timezone
|
20
20
|
from typing import (
|
21
21
|
TYPE_CHECKING,
|
22
22
|
AbstractSet,
|
@@ -751,8 +751,8 @@ class Stack:
|
|
751
751
|
config=LocalImageBuilderConfig(),
|
752
752
|
user=Client().active_user.id,
|
753
753
|
workspace=Client().active_workspace.id,
|
754
|
-
created=datetime.
|
755
|
-
updated=datetime.
|
754
|
+
created=datetime.now(timezone.utc),
|
755
|
+
updated=datetime.now(timezone.utc),
|
756
756
|
)
|
757
757
|
|
758
758
|
self._image_builder = image_builder
|
@@ -197,6 +197,11 @@ class ZenMLCloudStackDeployment(BaseModel):
|
|
197
197
|
# that was registered after this deployment was created.
|
198
198
|
|
199
199
|
# Get all stacks created after the start date
|
200
|
+
|
201
|
+
if date_start and date_start.tzinfo:
|
202
|
+
date_start = date_start.astimezone(datetime.timezone.utc).replace(
|
203
|
+
tzinfo=None
|
204
|
+
)
|
200
205
|
stacks = client.list_stacks(
|
201
206
|
created=f"gt:{str(date_start.replace(microsecond=0))}"
|
202
207
|
if date_start
|
zenml/utils/git_utils.py
CHANGED
zenml/utils/string_utils.py
CHANGED
@@ -17,7 +17,7 @@ import base64
|
|
17
17
|
import functools
|
18
18
|
import random
|
19
19
|
import string
|
20
|
-
from datetime import datetime
|
20
|
+
from datetime import datetime, timezone
|
21
21
|
from typing import Any, Callable, Dict, Optional, TypeVar, cast
|
22
22
|
|
23
23
|
from pydantic import BaseModel
|
@@ -180,7 +180,7 @@ def format_name_template(
|
|
180
180
|
start_time = None
|
181
181
|
|
182
182
|
if start_time is None:
|
183
|
-
start_time = datetime.
|
183
|
+
start_time = datetime.now(timezone.utc)
|
184
184
|
substitutions.setdefault("date", start_time.strftime("%Y_%m_%d"))
|
185
185
|
substitutions.setdefault("time", start_time.strftime("%H_%M_%S_%f"))
|
186
186
|
|
zenml/utils/yaml_utils.py
CHANGED
@@ -122,6 +122,7 @@ def write_json(
|
|
122
122
|
file_path: str,
|
123
123
|
contents: Any,
|
124
124
|
encoder: Optional[Type[json.JSONEncoder]] = None,
|
125
|
+
**json_dump_args: Any,
|
125
126
|
) -> None:
|
126
127
|
"""Write contents as JSON format to file_path.
|
127
128
|
|
@@ -129,6 +130,7 @@ def write_json(
|
|
129
130
|
file_path: Path to JSON file.
|
130
131
|
contents: Contents of JSON file.
|
131
132
|
encoder: Custom JSON encoder to use when saving json.
|
133
|
+
**json_dump_args: Extra arguments to pass to json.dumps.
|
132
134
|
|
133
135
|
Raises:
|
134
136
|
FileNotFoundError: if directory does not exist.
|
@@ -140,10 +142,7 @@ def write_json(
|
|
140
142
|
raise FileNotFoundError(f"Directory {dir_} does not exist.")
|
141
143
|
io_utils.write_file_contents_as_string(
|
142
144
|
file_path,
|
143
|
-
json.dumps(
|
144
|
-
contents,
|
145
|
-
cls=encoder,
|
146
|
-
),
|
145
|
+
json.dumps(contents, cls=encoder, **json_dump_args),
|
147
146
|
)
|
148
147
|
|
149
148
|
|