zenml-nightly 0.58.2.dev20240623__py3-none-any.whl → 0.61.0.dev20240712__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.
- README.md +30 -9
- RELEASE_NOTES.md +240 -0
- zenml/VERSION +1 -1
- zenml/actions/base_action.py +177 -174
- zenml/actions/pipeline_run/pipeline_run_action.py +28 -23
- zenml/analytics/enums.py +3 -0
- zenml/artifact_stores/base_artifact_store.py +7 -1
- zenml/artifacts/utils.py +13 -10
- zenml/cli/__init__.py +28 -0
- zenml/cli/artifact.py +1 -2
- zenml/cli/integration.py +9 -8
- zenml/cli/server.py +6 -0
- zenml/cli/service_connectors.py +1 -0
- zenml/cli/stack.py +946 -39
- zenml/cli/stack_components.py +7 -0
- zenml/cli/text_utils.py +35 -1
- zenml/cli/utils.py +127 -10
- zenml/client.py +257 -72
- zenml/config/compiler.py +10 -9
- zenml/config/docker_settings.py +33 -14
- zenml/constants.py +11 -2
- zenml/container_registries/base_container_registry.py +1 -0
- zenml/enums.py +7 -0
- zenml/event_hub/base_event_hub.py +5 -5
- zenml/event_hub/event_hub.py +20 -14
- zenml/event_sources/base_event.py +0 -11
- zenml/event_sources/base_event_source.py +7 -0
- zenml/event_sources/webhooks/base_webhook_event_source.py +1 -4
- zenml/exceptions.py +4 -0
- zenml/hooks/hook_validators.py +2 -3
- zenml/integrations/aws/__init__.py +1 -0
- zenml/integrations/azure/__init__.py +1 -0
- zenml/integrations/bitbucket/plugins/event_sources/bitbucket_webhook_event_source.py +3 -3
- zenml/integrations/deepchecks/__init__.py +1 -0
- zenml/integrations/discord/__init__.py +1 -0
- zenml/integrations/evidently/__init__.py +1 -0
- zenml/integrations/facets/__init__.py +1 -0
- zenml/integrations/feast/__init__.py +1 -0
- zenml/integrations/gcp/__init__.py +3 -1
- zenml/integrations/gcp/google_credentials_mixin.py +1 -1
- zenml/integrations/gcp/service_connectors/gcp_service_connector.py +320 -64
- zenml/integrations/huggingface/__init__.py +1 -0
- zenml/integrations/integration.py +24 -0
- zenml/integrations/kubeflow/__init__.py +3 -0
- zenml/integrations/kubeflow/flavors/kubeflow_orchestrator_flavor.py +1 -1
- zenml/integrations/kubeflow/orchestrators/kubeflow_orchestrator.py +0 -1
- zenml/integrations/kubernetes/__init__.py +3 -1
- zenml/integrations/kubernetes/orchestrators/kube_utils.py +4 -1
- zenml/integrations/label_studio/annotators/label_studio_annotator.py +1 -0
- zenml/integrations/langchain/__init__.py +1 -0
- zenml/integrations/mlflow/__init__.py +4 -2
- zenml/integrations/neural_prophet/__init__.py +1 -0
- zenml/integrations/polars/__init__.py +1 -0
- zenml/integrations/prodigy/__init__.py +1 -0
- zenml/integrations/pycaret/__init__.py +6 -0
- zenml/integrations/registry.py +37 -0
- zenml/integrations/s3/artifact_stores/s3_artifact_store.py +93 -9
- zenml/integrations/seldon/__init__.py +1 -0
- zenml/integrations/seldon/model_deployers/seldon_model_deployer.py +1 -0
- zenml/integrations/skypilot/flavors/skypilot_orchestrator_base_vm_config.py +2 -2
- zenml/integrations/skypilot/orchestrators/skypilot_base_vm_orchestrator.py +1 -1
- zenml/integrations/skypilot/orchestrators/skypilot_orchestrator_entrypoint.py +2 -2
- zenml/integrations/skypilot_aws/__init__.py +2 -1
- zenml/integrations/skypilot_azure/__init__.py +1 -1
- zenml/integrations/skypilot_gcp/__init__.py +1 -1
- zenml/integrations/skypilot_lambda/__init__.py +1 -1
- zenml/integrations/skypilot_lambda/flavors/skypilot_orchestrator_lambda_vm_flavor.py +1 -1
- zenml/integrations/slack/__init__.py +1 -0
- zenml/integrations/tekton/__init__.py +1 -0
- zenml/integrations/tensorboard/__init__.py +0 -1
- zenml/integrations/tensorflow/__init__.py +18 -6
- zenml/integrations/wandb/__init__.py +1 -0
- zenml/logging/step_logging.py +54 -51
- zenml/models/__init__.py +28 -0
- zenml/models/v2/core/action.py +276 -0
- zenml/models/v2/core/component.py +18 -0
- zenml/models/v2/core/model.py +1 -2
- zenml/models/v2/core/service_connector.py +17 -0
- zenml/models/v2/core/stack.py +31 -0
- zenml/models/v2/core/trigger.py +182 -141
- zenml/models/v2/misc/full_stack.py +97 -0
- zenml/models/v2/misc/stack_deployment.py +86 -0
- zenml/new/pipelines/pipeline.py +14 -4
- zenml/new/pipelines/pipeline_decorator.py +1 -2
- zenml/new/pipelines/run_utils.py +1 -12
- zenml/new/steps/step_decorator.py +2 -3
- zenml/orchestrators/input_utils.py +3 -6
- zenml/pipelines/base_pipeline.py +0 -2
- zenml/pipelines/pipeline_decorator.py +1 -2
- zenml/stack/stack.py +3 -6
- zenml/stack/stack_component.py +4 -0
- zenml/stack_deployments/__init__.py +14 -0
- zenml/stack_deployments/aws_stack_deployment.py +254 -0
- zenml/stack_deployments/gcp_stack_deployment.py +260 -0
- zenml/stack_deployments/stack_deployment.py +208 -0
- zenml/stack_deployments/utils.py +44 -0
- zenml/steps/base_step.py +1 -2
- zenml/steps/step_decorator.py +1 -2
- zenml/types.py +10 -1
- zenml/utils/function_utils.py +1 -1
- zenml/utils/pagination_utils.py +7 -5
- zenml/utils/pipeline_docker_image_builder.py +117 -73
- zenml/utils/pydantic_utils.py +6 -5
- zenml/zen_server/cloud_utils.py +18 -3
- zenml/zen_server/dashboard/assets/{404-CDPQCl4D.js → 404-DpJaNHKF.js} +1 -1
- zenml/zen_server/dashboard/assets/@radix-CFOkMR_E.js +85 -0
- zenml/zen_server/dashboard/assets/{@react-router-DYovave8.js → @react-router-CO-OsFwI.js} +2 -2
- zenml/zen_server/dashboard/assets/{@reactflow-CHBapDaj.js → @reactflow-DJfzkHO1.js} +2 -2
- zenml/zen_server/dashboard/assets/@tanstack-DYiOyJUL.js +22 -0
- zenml/zen_server/dashboard/assets/AwarenessChannel-BYDLT2xC.js +1 -0
- zenml/zen_server/dashboard/assets/{CodeSnippet-BidtnWOi.js → CodeSnippet-BkOuRmyq.js} +2 -2
- zenml/zen_server/dashboard/assets/Commands-ZvWR1BRs.js +1 -0
- zenml/zen_server/dashboard/assets/CopyButton-DVwLkafa.js +2 -0
- zenml/zen_server/dashboard/assets/{CsvVizualization-BOuez-fG.js → CsvVizualization-C2IiqX4I.js} +7 -7
- zenml/zen_server/dashboard/assets/DisplayDate-DYgIjlDF.js +1 -0
- zenml/zen_server/dashboard/assets/EmptyState-BMLnFVlB.js +1 -0
- zenml/zen_server/dashboard/assets/Error-CqX0VqW_.js +1 -0
- zenml/zen_server/dashboard/assets/ExecutionStatus-BoLUXR9t.js +1 -0
- zenml/zen_server/dashboard/assets/Helpbox-LFydyVwh.js +1 -0
- zenml/zen_server/dashboard/assets/Infobox-DnENC0sh.js +1 -0
- zenml/zen_server/dashboard/assets/InlineAvatar-CbJtYr0t.js +1 -0
- zenml/zen_server/dashboard/assets/{MarkdownVisualization-DsB2QZiK.js → MarkdownVisualization-xp3hhULl.js} +2 -2
- zenml/zen_server/dashboard/assets/Pagination-DEbVUupy.js +1 -0
- zenml/zen_server/dashboard/assets/PasswordChecker-DUveqlva.js +1 -0
- zenml/zen_server/dashboard/assets/SetPassword-BYBdbQDo.js +1 -0
- zenml/zen_server/dashboard/assets/SuccessStep-Nx743hll.js +1 -0
- zenml/zen_server/dashboard/assets/{UpdatePasswordSchemas-DnM-c11H.js → UpdatePasswordSchemas-DF9gSzE0.js} +1 -1
- zenml/zen_server/dashboard/assets/{aws-t0gKCj_R.js → aws-BgKTfTfx.js} +1 -1
- zenml/zen_server/dashboard/assets/{check-circle-BVvhm5dy.js → check-circle-i56092KI.js} +1 -1
- zenml/zen_server/dashboard/assets/{chevron-down-zcvCWmyP.js → chevron-down-D_ZlKMqH.js} +1 -1
- zenml/zen_server/dashboard/assets/{chevron-right-double-CJ50E9Gr.js → chevron-right-double-BiEMg7rd.js} +1 -1
- zenml/zen_server/dashboard/assets/cloud-only-DVbIeckv.js +1 -0
- zenml/zen_server/dashboard/assets/{copy-BRhQz3j-.js → copy-BXNk6BjL.js} +1 -1
- zenml/zen_server/dashboard/assets/{database-CRRnyFWh.js → database-1xWSgZfO.js} +1 -1
- zenml/zen_server/dashboard/assets/{docker-BAonhm6G.js → docker-CQMVm_4d.js} +1 -1
- zenml/zen_server/dashboard/assets/{file-text-CbVERUON.js → file-text-CqD_iu6l.js} +1 -1
- zenml/zen_server/dashboard/assets/{help-B8rqCvqn.js → help-bu_DgLKI.js} +1 -1
- zenml/zen_server/dashboard/assets/index-C_CrU4vI.js +1 -0
- zenml/zen_server/dashboard/assets/index-DK1ynKjA.js +55 -0
- zenml/zen_server/dashboard/assets/index-inApY3KQ.css +1 -0
- zenml/zen_server/dashboard/assets/index-rK_Wuy2W.js +1 -0
- zenml/zen_server/dashboard/assets/index.esm-Corw4lXQ.js +1 -0
- zenml/zen_server/dashboard/assets/{login-mutation-wzzl23C6.js → login-mutation-BUnVASxp.js} +1 -1
- zenml/zen_server/dashboard/assets/not-found-B4VnX8gK.js +1 -0
- zenml/zen_server/dashboard/assets/package-CsUhPmou.js +1 -0
- zenml/zen_server/dashboard/assets/{page-BmkSiYeQ.js → page-3efNCDeb.js} +2 -2
- zenml/zen_server/dashboard/assets/page-7zTHbhhI.js +1 -0
- zenml/zen_server/dashboard/assets/page-BEs6jK71.js +1 -0
- zenml/zen_server/dashboard/assets/page-BpSqIf4B.js +1 -0
- zenml/zen_server/dashboard/assets/{page-AQKopn_4.js → page-Bx6o0ARS.js} +1 -1
- zenml/zen_server/dashboard/assets/page-C43QGHTt.js +9 -0
- zenml/zen_server/dashboard/assets/page-CR0OG7ss.js +1 -0
- zenml/zen_server/dashboard/assets/page-CRTJ0UuR.js +1 -0
- zenml/zen_server/dashboard/assets/page-CUZIGO-3.js +1 -0
- zenml/zen_server/dashboard/assets/page-CaopxiU1.js +1 -0
- zenml/zen_server/dashboard/assets/{page-CuT1SUik.js → page-Cx67M0QT.js} +1 -1
- zenml/zen_server/dashboard/assets/page-D7Z399xy.js +1 -0
- zenml/zen_server/dashboard/assets/page-D93kd7Xj.js +1 -0
- zenml/zen_server/dashboard/assets/{page-BzVZGExK.js → page-DKlIdAe5.js} +1 -1
- zenml/zen_server/dashboard/assets/{page-Bi5AI0S7.js → page-DMOYZppS.js} +1 -1
- zenml/zen_server/dashboard/assets/page-DMsSn3dv.js +2 -0
- zenml/zen_server/dashboard/assets/{page-BW6Ket3a.js → page-Dc_7KMQE.js} +1 -1
- zenml/zen_server/dashboard/assets/page-DvCvroOM.js +1 -0
- zenml/zen_server/dashboard/assets/page-Hus2pr9T.js +1 -0
- zenml/zen_server/dashboard/assets/page-JyfeDUfu.js +1 -0
- zenml/zen_server/dashboard/assets/{page-yN4rZ-ZS.js → page-Sxn82W-5.js} +1 -1
- zenml/zen_server/dashboard/assets/page-TKXERe16.js +1 -0
- zenml/zen_server/dashboard/assets/page-Xu8JEjSU.js +1 -0
- zenml/zen_server/dashboard/assets/{play-circle-DK5QMJyp.js → play-circle-CNtZKDnW.js} +1 -1
- zenml/zen_server/dashboard/assets/plus-DOeLmm7C.js +1 -0
- zenml/zen_server/dashboard/assets/{terminal-B2ovgWuz.js → terminal-By9cErXc.js} +1 -1
- zenml/zen_server/dashboard/assets/{update-server-settings-mutation-0Wgz8pUE.js → update-server-settings-mutation-CR8e3Sir.js} +1 -1
- zenml/zen_server/dashboard/assets/{url-6_xv0WJS.js → url-DuQMeqYA.js} +1 -1
- zenml/zen_server/dashboard/assets/{zod-DrZvVLjd.js → zod-BhoGpZ63.js} +1 -1
- zenml/zen_server/dashboard/index.html +7 -7
- zenml/zen_server/dashboard_legacy/asset-manifest.json +4 -4
- zenml/zen_server/dashboard_legacy/index.html +1 -1
- zenml/zen_server/dashboard_legacy/{precache-manifest.f4abc5b7cfa7d90c1caf5521918e29a8.js → precache-manifest.c8c57fb0d2132b1d3c2119e776b7dfb3.js} +4 -4
- zenml/zen_server/dashboard_legacy/service-worker.js +1 -1
- zenml/zen_server/dashboard_legacy/static/js/{main.ac2f17d0.chunk.js → main.382439a7.chunk.js} +2 -2
- zenml/zen_server/dashboard_legacy/static/js/{main.ac2f17d0.chunk.js.map → main.382439a7.chunk.js.map} +1 -1
- zenml/zen_server/deploy/helm/Chart.yaml +1 -1
- zenml/zen_server/deploy/helm/README.md +2 -2
- zenml/zen_server/feature_gate/zenml_cloud_feature_gate.py +11 -5
- zenml/zen_server/pipeline_deployment/utils.py +57 -44
- zenml/zen_server/rbac/models.py +1 -0
- zenml/zen_server/rbac/utils.py +22 -1
- zenml/zen_server/rbac/zenml_cloud_rbac.py +11 -5
- zenml/zen_server/routers/actions_endpoints.py +324 -0
- zenml/zen_server/routers/stack_deployment_endpoints.py +158 -0
- zenml/zen_server/routers/triggers_endpoints.py +30 -158
- zenml/zen_server/routers/workspaces_endpoints.py +64 -0
- zenml/zen_server/zen_server_api.py +4 -0
- zenml/zen_stores/migrations/utils.py +1 -1
- zenml/zen_stores/migrations/versions/0.60.0_release.py +23 -0
- zenml/zen_stores/migrations/versions/0.61.0_release.py +23 -0
- zenml/zen_stores/migrations/versions/0d707865f404_adding_labels_to_stacks.py +30 -0
- zenml/zen_stores/migrations/versions/25155145c545_separate_actions_and_triggers.py +228 -0
- zenml/zen_stores/rest_zen_store.py +248 -8
- zenml/zen_stores/schemas/__init__.py +2 -0
- zenml/zen_stores/schemas/action_schemas.py +192 -0
- zenml/zen_stores/schemas/stack_schemas.py +10 -0
- zenml/zen_stores/schemas/step_run_schemas.py +27 -11
- zenml/zen_stores/schemas/trigger_schemas.py +43 -50
- zenml/zen_stores/schemas/user_schemas.py +10 -2
- zenml/zen_stores/schemas/workspace_schemas.py +5 -0
- zenml/zen_stores/sql_zen_store.py +540 -36
- zenml/zen_stores/zen_store_interface.py +165 -0
- {zenml_nightly-0.58.2.dev20240623.dist-info → zenml_nightly-0.61.0.dev20240712.dist-info}/METADATA +33 -11
- {zenml_nightly-0.58.2.dev20240623.dist-info → zenml_nightly-0.61.0.dev20240712.dist-info}/RECORD +213 -193
- zenml/zen_server/dashboard/assets/@radix-C9DBgJhe.js +0 -77
- zenml/zen_server/dashboard/assets/@tanstack-CEbkxrhX.js +0 -30
- zenml/zen_server/dashboard/assets/AwarenessChannel-nXGpmj_f.js +0 -1
- zenml/zen_server/dashboard/assets/Cards-nwsvQLVS.js +0 -1
- zenml/zen_server/dashboard/assets/Commands-DuIWKg_Q.js +0 -1
- zenml/zen_server/dashboard/assets/CopyButton-B_YSm-Ds.js +0 -2
- zenml/zen_server/dashboard/assets/DisplayDate-BdguISQF.js +0 -1
- zenml/zen_server/dashboard/assets/EmptyState-BkooiGtL.js +0 -1
- zenml/zen_server/dashboard/assets/Error-B6M0dPph.js +0 -1
- zenml/zen_server/dashboard/assets/Helpbox-BQoqCm04.js +0 -1
- zenml/zen_server/dashboard/assets/Infobox-Ce9mefqU.js +0 -1
- zenml/zen_server/dashboard/assets/InlineAvatar-DGf3dVhV.js +0 -1
- zenml/zen_server/dashboard/assets/PageHeader-DGaemzjc.js +0 -1
- zenml/zen_server/dashboard/assets/Pagination-DVYfBCCc.js +0 -1
- zenml/zen_server/dashboard/assets/PasswordChecker-DSLBp7Vl.js +0 -1
- zenml/zen_server/dashboard/assets/SetPassword-B5s7DJug.js +0 -1
- zenml/zen_server/dashboard/assets/SuccessStep-ZzczaM7g.js +0 -1
- zenml/zen_server/dashboard/assets/cloud-only-Ba_ShBR5.js +0 -1
- zenml/zen_server/dashboard/assets/index-CWJ3xbIf.css +0 -1
- zenml/zen_server/dashboard/assets/index-QORVVTMN.js +0 -55
- zenml/zen_server/dashboard/assets/index.esm-F7nqy9zY.js +0 -1
- zenml/zen_server/dashboard/assets/not-found-Dh2la7kh.js +0 -1
- zenml/zen_server/dashboard/assets/page-B-5jAKoO.js +0 -1
- zenml/zen_server/dashboard/assets/page-B-vWk8a6.js +0 -1
- zenml/zen_server/dashboard/assets/page-B0BrqfS8.js +0 -1
- zenml/zen_server/dashboard/assets/page-BQxVFlUl.js +0 -1
- zenml/zen_server/dashboard/assets/page-ByrHy6Ss.js +0 -1
- zenml/zen_server/dashboard/assets/page-CPtY4Kv_.js +0 -1
- zenml/zen_server/dashboard/assets/page-CmmukLsl.js +0 -1
- zenml/zen_server/dashboard/assets/page-D2D-7qyr.js +0 -9
- zenml/zen_server/dashboard/assets/page-DAQQyLxT.js +0 -1
- zenml/zen_server/dashboard/assets/page-DHkUMl_E.js +0 -1
- zenml/zen_server/dashboard/assets/page-DZCbwOEs.js +0 -2
- zenml/zen_server/dashboard/assets/page-DdaIt20-.js +0 -1
- zenml/zen_server/dashboard/assets/page-LqLs24Ot.js +0 -1
- zenml/zen_server/dashboard/assets/page-lebv0c7C.js +0 -1
- {zenml_nightly-0.58.2.dev20240623.dist-info → zenml_nightly-0.61.0.dev20240712.dist-info}/LICENSE +0 -0
- {zenml_nightly-0.58.2.dev20240623.dist-info → zenml_nightly-0.61.0.dev20240712.dist-info}/WHEEL +0 -0
- {zenml_nightly-0.58.2.dev20240623.dist-info → zenml_nightly-0.61.0.dev20240712.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,158 @@
|
|
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
|
+
"""Endpoint definitions for stack deployments."""
|
15
|
+
|
16
|
+
import datetime
|
17
|
+
from typing import Optional
|
18
|
+
|
19
|
+
from fastapi import APIRouter, Request, Security
|
20
|
+
|
21
|
+
from zenml.constants import (
|
22
|
+
API,
|
23
|
+
CONFIG,
|
24
|
+
INFO,
|
25
|
+
STACK,
|
26
|
+
STACK_DEPLOYMENT,
|
27
|
+
STACK_DEPLOYMENT_API_TOKEN_EXPIRATION,
|
28
|
+
VERSION_1,
|
29
|
+
)
|
30
|
+
from zenml.enums import StackDeploymentProvider
|
31
|
+
from zenml.models import (
|
32
|
+
DeployedStack,
|
33
|
+
StackDeploymentConfig,
|
34
|
+
StackDeploymentInfo,
|
35
|
+
)
|
36
|
+
from zenml.stack_deployments.utils import get_stack_deployment_class
|
37
|
+
from zenml.zen_server.auth import AuthContext, authorize
|
38
|
+
from zenml.zen_server.exceptions import error_response
|
39
|
+
from zenml.zen_server.rbac.models import Action, ResourceType
|
40
|
+
from zenml.zen_server.rbac.utils import verify_permission
|
41
|
+
from zenml.zen_server.utils import (
|
42
|
+
handle_exceptions,
|
43
|
+
)
|
44
|
+
|
45
|
+
router = APIRouter(
|
46
|
+
prefix=API + VERSION_1 + STACK_DEPLOYMENT,
|
47
|
+
tags=["stacks"],
|
48
|
+
responses={401: error_response, 403: error_response},
|
49
|
+
)
|
50
|
+
|
51
|
+
|
52
|
+
@router.get(
|
53
|
+
INFO,
|
54
|
+
)
|
55
|
+
@handle_exceptions
|
56
|
+
def get_stack_deployment_info(
|
57
|
+
provider: StackDeploymentProvider,
|
58
|
+
_: AuthContext = Security(authorize),
|
59
|
+
) -> StackDeploymentInfo:
|
60
|
+
"""Get information about a stack deployment provider.
|
61
|
+
|
62
|
+
Args:
|
63
|
+
provider: The stack deployment provider.
|
64
|
+
|
65
|
+
Returns:
|
66
|
+
Information about the stack deployment provider.
|
67
|
+
"""
|
68
|
+
stack_deployment_class = get_stack_deployment_class(provider)
|
69
|
+
return stack_deployment_class.get_deployment_info()
|
70
|
+
|
71
|
+
|
72
|
+
@router.get(
|
73
|
+
CONFIG,
|
74
|
+
)
|
75
|
+
@handle_exceptions
|
76
|
+
def get_stack_deployment_config(
|
77
|
+
request: Request,
|
78
|
+
provider: StackDeploymentProvider,
|
79
|
+
stack_name: str,
|
80
|
+
location: Optional[str] = None,
|
81
|
+
auth_context: AuthContext = Security(authorize),
|
82
|
+
) -> StackDeploymentConfig:
|
83
|
+
"""Return the URL to deploy the ZenML stack to the specified cloud provider.
|
84
|
+
|
85
|
+
Args:
|
86
|
+
request: The FastAPI request object.
|
87
|
+
provider: The stack deployment provider.
|
88
|
+
stack_name: The name of the stack.
|
89
|
+
location: The location where the stack should be deployed.
|
90
|
+
auth_context: The authentication context.
|
91
|
+
|
92
|
+
Returns:
|
93
|
+
The cloud provider console URL where the stack will be deployed and
|
94
|
+
the configuration for the stack deployment.
|
95
|
+
"""
|
96
|
+
verify_permission(
|
97
|
+
resource_type=ResourceType.SERVICE_CONNECTOR, action=Action.CREATE
|
98
|
+
)
|
99
|
+
verify_permission(
|
100
|
+
resource_type=ResourceType.STACK_COMPONENT,
|
101
|
+
action=Action.CREATE,
|
102
|
+
)
|
103
|
+
verify_permission(resource_type=ResourceType.STACK, action=Action.CREATE)
|
104
|
+
|
105
|
+
stack_deployment_class = get_stack_deployment_class(provider)
|
106
|
+
# Get the base server URL used to call this FastAPI endpoint
|
107
|
+
url = request.url.replace(path="").replace(query="")
|
108
|
+
# Use HTTPS for the URL
|
109
|
+
url = url.replace(scheme="https")
|
110
|
+
|
111
|
+
token = auth_context.access_token
|
112
|
+
assert token is not None
|
113
|
+
|
114
|
+
# A new API token is generated for the stack deployment
|
115
|
+
expires = datetime.datetime.utcnow() + datetime.timedelta(
|
116
|
+
minutes=STACK_DEPLOYMENT_API_TOKEN_EXPIRATION
|
117
|
+
)
|
118
|
+
api_token = token.encode(expires=expires)
|
119
|
+
|
120
|
+
return stack_deployment_class(
|
121
|
+
stack_name=stack_name,
|
122
|
+
location=location,
|
123
|
+
zenml_server_url=str(url),
|
124
|
+
zenml_server_api_token=api_token,
|
125
|
+
).get_deployment_config()
|
126
|
+
|
127
|
+
|
128
|
+
@router.get(
|
129
|
+
STACK,
|
130
|
+
)
|
131
|
+
@handle_exceptions
|
132
|
+
def get_deployed_stack(
|
133
|
+
provider: StackDeploymentProvider,
|
134
|
+
stack_name: str,
|
135
|
+
location: Optional[str] = None,
|
136
|
+
date_start: Optional[datetime.datetime] = None,
|
137
|
+
_: AuthContext = Security(authorize),
|
138
|
+
) -> Optional[DeployedStack]:
|
139
|
+
"""Return a matching ZenML stack that was deployed and registered.
|
140
|
+
|
141
|
+
Args:
|
142
|
+
provider: The stack deployment provider.
|
143
|
+
stack_name: The name of the stack.
|
144
|
+
location: The location where the stack should be deployed.
|
145
|
+
date_start: The date when the deployment started.
|
146
|
+
|
147
|
+
Returns:
|
148
|
+
The ZenML stack that was deployed and registered or None if the stack
|
149
|
+
was not found.
|
150
|
+
"""
|
151
|
+
stack_deployment_class = get_stack_deployment_class(provider)
|
152
|
+
return stack_deployment_class(
|
153
|
+
stack_name=stack_name,
|
154
|
+
location=location,
|
155
|
+
# These fields are not needed for this operation
|
156
|
+
zenml_server_url="",
|
157
|
+
zenml_server_api_token="",
|
158
|
+
).get_stack(date_start=date_start)
|
@@ -18,7 +18,6 @@ from uuid import UUID
|
|
18
18
|
from fastapi import APIRouter, Depends, Security
|
19
19
|
|
20
20
|
from zenml import TriggerRequest
|
21
|
-
from zenml.actions.base_action import BaseActionHandler
|
22
21
|
from zenml.constants import API, TRIGGER_EXECUTIONS, TRIGGERS, VERSION_1
|
23
22
|
from zenml.enums import PluginType
|
24
23
|
from zenml.event_sources.base_event_source import BaseEventSourceHandler
|
@@ -81,54 +80,11 @@ def list_triggers(
|
|
81
80
|
Returns:
|
82
81
|
All triggers.
|
83
82
|
"""
|
84
|
-
|
85
|
-
def list_triggers_fn(
|
86
|
-
filter_model: TriggerFilter,
|
87
|
-
) -> Page[TriggerResponse]:
|
88
|
-
"""List triggers through their associated plugins.
|
89
|
-
|
90
|
-
Args:
|
91
|
-
filter_model: Filter model used for pagination, sorting,
|
92
|
-
filtering.
|
93
|
-
|
94
|
-
Returns:
|
95
|
-
All triggers.
|
96
|
-
|
97
|
-
Raises:
|
98
|
-
ValueError: If the plugin for a trigger action is not a valid action
|
99
|
-
plugin.
|
100
|
-
"""
|
101
|
-
triggers = zen_store().list_triggers(
|
102
|
-
trigger_filter_model=filter_model, hydrate=hydrate
|
103
|
-
)
|
104
|
-
|
105
|
-
# Process the triggers through their associated plugins
|
106
|
-
for idx, trigger in enumerate(triggers.items):
|
107
|
-
action_handler = plugin_flavor_registry().get_plugin(
|
108
|
-
name=trigger.action_flavor,
|
109
|
-
_type=PluginType.ACTION,
|
110
|
-
subtype=trigger.action_subtype,
|
111
|
-
)
|
112
|
-
|
113
|
-
# Validate that the flavor and plugin_type correspond to an action
|
114
|
-
# handler implementation
|
115
|
-
if not isinstance(action_handler, BaseActionHandler):
|
116
|
-
raise ValueError(
|
117
|
-
f"Action handler plugin {trigger.action_subtype} "
|
118
|
-
f"for flavor {trigger.action_flavor} is not a valid action "
|
119
|
-
"handler plugin."
|
120
|
-
)
|
121
|
-
|
122
|
-
triggers.items[idx] = action_handler.get_trigger(
|
123
|
-
trigger, hydrate=hydrate
|
124
|
-
)
|
125
|
-
|
126
|
-
return triggers
|
127
|
-
|
128
83
|
return verify_permissions_and_list_entities(
|
129
84
|
filter_model=trigger_filter_model,
|
130
85
|
resource_type=ResourceType.TRIGGER,
|
131
|
-
list_method=
|
86
|
+
list_method=zen_store().list_triggers,
|
87
|
+
hydrate=hydrate,
|
132
88
|
)
|
133
89
|
|
134
90
|
|
@@ -152,31 +108,9 @@ def get_trigger(
|
|
152
108
|
|
153
109
|
Returns:
|
154
110
|
The requested trigger.
|
155
|
-
|
156
|
-
Raises:
|
157
|
-
ValueError: If the action flavor/subtype combination is not actually a webhook event source
|
158
111
|
"""
|
159
112
|
trigger = zen_store().get_trigger(trigger_id=trigger_id, hydrate=hydrate)
|
160
|
-
|
161
113
|
verify_permission_for_model(trigger, action=Action.READ)
|
162
|
-
|
163
|
-
action_handler = plugin_flavor_registry().get_plugin(
|
164
|
-
name=trigger.action_flavor,
|
165
|
-
_type=PluginType.ACTION,
|
166
|
-
subtype=trigger.action_subtype,
|
167
|
-
)
|
168
|
-
|
169
|
-
# Validate that the flavor and plugin_type correspond to an action
|
170
|
-
# handler implementation
|
171
|
-
if not isinstance(action_handler, BaseActionHandler):
|
172
|
-
raise ValueError(
|
173
|
-
f"Action handler plugin {trigger.action_subtype} "
|
174
|
-
f"for flavor {trigger.action_flavor} is not a valid action "
|
175
|
-
"handler plugin."
|
176
|
-
)
|
177
|
-
|
178
|
-
trigger = action_handler.get_trigger(trigger, hydrate=hydrate)
|
179
|
-
|
180
114
|
return dehydrate_response_model(trigger)
|
181
115
|
|
182
116
|
|
@@ -201,55 +135,35 @@ def create_trigger(
|
|
201
135
|
Raises:
|
202
136
|
ValueError: If the action flavor/subtype combination is not actually a webhook event source
|
203
137
|
"""
|
204
|
-
if trigger.
|
205
|
-
|
206
|
-
|
138
|
+
if trigger.event_source_id and trigger.event_filter:
|
139
|
+
event_source = zen_store().get_event_source(
|
140
|
+
event_source_id=trigger.event_source_id
|
207
141
|
)
|
208
|
-
verify_permission_for_model(service_account, action=Action.READ)
|
209
|
-
|
210
|
-
event_source = zen_store().get_event_source(
|
211
|
-
event_source_id=trigger.event_source_id
|
212
|
-
)
|
213
142
|
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
)
|
219
|
-
|
220
|
-
# Validate that the flavor and plugin_type correspond to an event source
|
221
|
-
# implementation
|
222
|
-
if not isinstance(event_source_handler, BaseEventSourceHandler):
|
223
|
-
raise ValueError(
|
224
|
-
f"Event source plugin {event_source.plugin_subtype} "
|
225
|
-
f"for flavor {event_source.flavor} is not a valid event source "
|
226
|
-
"handler implementation."
|
143
|
+
event_source_handler = plugin_flavor_registry().get_plugin(
|
144
|
+
name=event_source.flavor,
|
145
|
+
_type=PluginType.EVENT_SOURCE,
|
146
|
+
subtype=event_source.plugin_subtype,
|
227
147
|
)
|
228
148
|
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
subtype=trigger.action_subtype,
|
238
|
-
)
|
149
|
+
# Validate that the flavor and plugin_type correspond to an event source
|
150
|
+
# implementation
|
151
|
+
if not isinstance(event_source_handler, BaseEventSourceHandler):
|
152
|
+
raise ValueError(
|
153
|
+
f"Event source plugin {event_source.plugin_subtype} "
|
154
|
+
f"for flavor {event_source.flavor} is not a valid event source "
|
155
|
+
"handler implementation."
|
156
|
+
)
|
239
157
|
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
raise ValueError(
|
244
|
-
f"Action handler plugin {trigger.action_subtype} "
|
245
|
-
f"for flavor {trigger.action_flavor} is not a valid action "
|
246
|
-
"handler plugin."
|
158
|
+
# Validate the trigger event filter
|
159
|
+
event_source_handler.validate_event_filter_configuration(
|
160
|
+
trigger.event_filter
|
247
161
|
)
|
248
162
|
|
249
163
|
return verify_permissions_and_create_entity(
|
250
164
|
request_model=trigger,
|
251
165
|
resource_type=ResourceType.TRIGGER,
|
252
|
-
create_method=
|
166
|
+
create_method=zen_store().create_trigger,
|
253
167
|
)
|
254
168
|
|
255
169
|
|
@@ -278,13 +192,12 @@ def update_trigger(
|
|
278
192
|
"""
|
279
193
|
trigger = zen_store().get_trigger(trigger_id=trigger_id)
|
280
194
|
|
281
|
-
if trigger_update.service_account_id:
|
282
|
-
service_account = zen_store().get_service_account(
|
283
|
-
service_account_name_or_id=trigger_update.service_account_id
|
284
|
-
)
|
285
|
-
verify_permission_for_model(service_account, action=Action.READ)
|
286
|
-
|
287
195
|
if trigger_update.event_filter:
|
196
|
+
if not trigger.event_source:
|
197
|
+
raise ValueError(
|
198
|
+
"Trying to set event filter for trigger without event source."
|
199
|
+
)
|
200
|
+
|
288
201
|
event_source = zen_store().get_event_source(
|
289
202
|
event_source_id=trigger.event_source.id
|
290
203
|
)
|
@@ -306,29 +219,13 @@ def update_trigger(
|
|
306
219
|
|
307
220
|
# Validate the trigger event filter
|
308
221
|
event_source_handler.validate_event_filter_configuration(
|
309
|
-
|
222
|
+
trigger_update.event_filter
|
310
223
|
)
|
311
224
|
|
312
225
|
verify_permission_for_model(trigger, action=Action.UPDATE)
|
313
226
|
|
314
|
-
|
315
|
-
|
316
|
-
_type=PluginType.ACTION,
|
317
|
-
subtype=trigger.action_subtype,
|
318
|
-
)
|
319
|
-
|
320
|
-
# Validate that the flavor and plugin_type correspond to an action
|
321
|
-
# handler implementation
|
322
|
-
if not isinstance(action_handler, BaseActionHandler):
|
323
|
-
raise ValueError(
|
324
|
-
f"Action handler plugin {trigger.action_subtype} "
|
325
|
-
f"for flavor {trigger.action_flavor} is not a valid action "
|
326
|
-
"handler plugin."
|
327
|
-
)
|
328
|
-
|
329
|
-
updated_trigger = action_handler.update_trigger(
|
330
|
-
trigger=trigger,
|
331
|
-
trigger_update=trigger_update,
|
227
|
+
updated_trigger = zen_store().update_trigger(
|
228
|
+
trigger_id=trigger_id, trigger_update=trigger_update
|
332
229
|
)
|
333
230
|
|
334
231
|
return dehydrate_response_model(updated_trigger)
|
@@ -341,41 +238,16 @@ def update_trigger(
|
|
341
238
|
@handle_exceptions
|
342
239
|
def delete_trigger(
|
343
240
|
trigger_id: UUID,
|
344
|
-
force: bool = False,
|
345
241
|
_: AuthContext = Security(authorize),
|
346
242
|
) -> None:
|
347
243
|
"""Deletes a trigger.
|
348
244
|
|
349
245
|
Args:
|
350
246
|
trigger_id: Name of the trigger.
|
351
|
-
force: Flag deciding whether to force delete the trigger.
|
352
|
-
|
353
|
-
Raises:
|
354
|
-
ValueError: If the action flavor/subtype combination is not actually a webhook event source
|
355
247
|
"""
|
356
248
|
trigger = zen_store().get_trigger(trigger_id=trigger_id)
|
357
|
-
|
358
249
|
verify_permission_for_model(trigger, action=Action.DELETE)
|
359
|
-
|
360
|
-
action_handler = plugin_flavor_registry().get_plugin(
|
361
|
-
name=trigger.action_flavor,
|
362
|
-
_type=PluginType.ACTION,
|
363
|
-
subtype=trigger.action_subtype,
|
364
|
-
)
|
365
|
-
|
366
|
-
# Validate that the flavor and plugin_type correspond to an action
|
367
|
-
# handler implementation
|
368
|
-
if not isinstance(action_handler, BaseActionHandler):
|
369
|
-
raise ValueError(
|
370
|
-
f"Action handler plugin {trigger.action_subtype} "
|
371
|
-
f"for flavor {trigger.action_flavor} is not a valid action "
|
372
|
-
"handler plugin."
|
373
|
-
)
|
374
|
-
|
375
|
-
action_handler.delete_trigger(
|
376
|
-
trigger=trigger,
|
377
|
-
force=force,
|
378
|
-
)
|
250
|
+
zen_store().delete_trigger(trigger_id=trigger_id)
|
379
251
|
|
380
252
|
|
381
253
|
executions_router = APIRouter(
|
@@ -22,6 +22,7 @@ from zenml.constants import (
|
|
22
22
|
API,
|
23
23
|
ARTIFACTS,
|
24
24
|
CODE_REPOSITORIES,
|
25
|
+
FULL_STACK,
|
25
26
|
GET_OR_CREATE,
|
26
27
|
MODEL_VERSIONS,
|
27
28
|
MODELS,
|
@@ -51,6 +52,7 @@ from zenml.models import (
|
|
51
52
|
ComponentFilter,
|
52
53
|
ComponentRequest,
|
53
54
|
ComponentResponse,
|
55
|
+
FullStackRequest,
|
54
56
|
ModelRequest,
|
55
57
|
ModelResponse,
|
56
58
|
ModelVersionArtifactRequest,
|
@@ -349,6 +351,68 @@ def create_stack(
|
|
349
351
|
)
|
350
352
|
|
351
353
|
|
354
|
+
@router.post(
|
355
|
+
WORKSPACES + "/{workspace_name_or_id}" + FULL_STACK,
|
356
|
+
response_model=StackResponse,
|
357
|
+
responses={401: error_response, 409: error_response, 422: error_response},
|
358
|
+
)
|
359
|
+
@handle_exceptions
|
360
|
+
def create_full_stack(
|
361
|
+
workspace_name_or_id: Union[str, UUID],
|
362
|
+
full_stack: FullStackRequest,
|
363
|
+
auth_context: AuthContext = Security(authorize),
|
364
|
+
) -> StackResponse:
|
365
|
+
"""Creates a stack for a particular workspace.
|
366
|
+
|
367
|
+
Args:
|
368
|
+
workspace_name_or_id: Name or ID of the workspace.
|
369
|
+
full_stack: Stack to register.
|
370
|
+
auth_context: Authentication context.
|
371
|
+
|
372
|
+
Returns:
|
373
|
+
The created stack.
|
374
|
+
"""
|
375
|
+
workspace = zen_store().get_workspace(workspace_name_or_id)
|
376
|
+
|
377
|
+
is_connector_create_needed = False
|
378
|
+
for connector_id_or_info in full_stack.service_connectors:
|
379
|
+
if isinstance(connector_id_or_info, UUID):
|
380
|
+
service_connector = zen_store().get_service_connector(
|
381
|
+
connector_id_or_info, hydrate=False
|
382
|
+
)
|
383
|
+
verify_permission_for_model(
|
384
|
+
model=service_connector, action=Action.READ
|
385
|
+
)
|
386
|
+
else:
|
387
|
+
is_connector_create_needed = True
|
388
|
+
if is_connector_create_needed:
|
389
|
+
verify_permission(
|
390
|
+
resource_type=ResourceType.SERVICE_CONNECTOR, action=Action.CREATE
|
391
|
+
)
|
392
|
+
|
393
|
+
is_component_create_needed = False
|
394
|
+
for component_id_or_info in full_stack.components.values():
|
395
|
+
if isinstance(component_id_or_info, UUID):
|
396
|
+
component = zen_store().get_stack_component(
|
397
|
+
component_id_or_info, hydrate=False
|
398
|
+
)
|
399
|
+
verify_permission_for_model(model=component, action=Action.READ)
|
400
|
+
else:
|
401
|
+
is_component_create_needed = True
|
402
|
+
if is_component_create_needed:
|
403
|
+
verify_permission(
|
404
|
+
resource_type=ResourceType.STACK_COMPONENT,
|
405
|
+
action=Action.CREATE,
|
406
|
+
)
|
407
|
+
|
408
|
+
verify_permission(resource_type=ResourceType.STACK, action=Action.CREATE)
|
409
|
+
|
410
|
+
full_stack.user = auth_context.user.id
|
411
|
+
full_stack.workspace = workspace.id
|
412
|
+
|
413
|
+
return zen_store().create_full_stack(full_stack)
|
414
|
+
|
415
|
+
|
352
416
|
@router.get(
|
353
417
|
WORKSPACES + "/{workspace_name_or_id}" + STACK_COMPONENTS,
|
354
418
|
response_model=Page[ComponentResponse],
|
@@ -40,6 +40,7 @@ from zenml.constants import API, HEALTH
|
|
40
40
|
from zenml.enums import AuthScheme, SourceContextTypes
|
41
41
|
from zenml.zen_server.exceptions import error_detail
|
42
42
|
from zenml.zen_server.routers import (
|
43
|
+
actions_endpoints,
|
43
44
|
artifact_endpoint,
|
44
45
|
artifact_version_endpoints,
|
45
46
|
auth_endpoints,
|
@@ -62,6 +63,7 @@ from zenml.zen_server.routers import (
|
|
62
63
|
service_connectors_endpoints,
|
63
64
|
service_endpoints,
|
64
65
|
stack_components_endpoints,
|
66
|
+
stack_deployment_endpoints,
|
65
67
|
stacks_endpoints,
|
66
68
|
steps_endpoints,
|
67
69
|
tags_endpoints,
|
@@ -262,6 +264,7 @@ async def dashboard(request: Request) -> Any:
|
|
262
264
|
return templates.TemplateResponse("index.html", {"request": request})
|
263
265
|
|
264
266
|
|
267
|
+
app.include_router(actions_endpoints.router)
|
265
268
|
app.include_router(artifact_endpoint.artifact_router)
|
266
269
|
app.include_router(artifact_version_endpoints.artifact_version_router)
|
267
270
|
app.include_router(auth_endpoints.router)
|
@@ -288,6 +291,7 @@ app.include_router(service_accounts_endpoints.router)
|
|
288
291
|
app.include_router(service_connectors_endpoints.router)
|
289
292
|
app.include_router(service_connectors_endpoints.types_router)
|
290
293
|
app.include_router(service_endpoints.router)
|
294
|
+
app.include_router(stack_deployment_endpoints.router)
|
291
295
|
app.include_router(stacks_endpoints.router)
|
292
296
|
app.include_router(stack_components_endpoints.router)
|
293
297
|
app.include_router(stack_components_endpoints.types_router)
|
@@ -364,7 +364,7 @@ class MigrationUtils(BaseModel):
|
|
364
364
|
# Convert column values to the correct type
|
365
365
|
for column in table.columns:
|
366
366
|
# Blob columns are stored as binary strings
|
367
|
-
if column.type.python_type
|
367
|
+
if column.type.python_type is bytes and isinstance(
|
368
368
|
row[column.name], str
|
369
369
|
):
|
370
370
|
# Convert the string to bytes
|
@@ -0,0 +1,23 @@
|
|
1
|
+
"""Release [0.60.0].
|
2
|
+
|
3
|
+
Revision ID: 0.60.0
|
4
|
+
Revises: 25155145c545
|
5
|
+
Create Date: 2024-06-25 14:26:48.867005
|
6
|
+
|
7
|
+
"""
|
8
|
+
|
9
|
+
# revision identifiers, used by Alembic.
|
10
|
+
revision = "0.60.0"
|
11
|
+
down_revision = "25155145c545"
|
12
|
+
branch_labels = None
|
13
|
+
depends_on = None
|
14
|
+
|
15
|
+
|
16
|
+
def upgrade() -> None:
|
17
|
+
"""Upgrade database schema and/or data, creating a new revision."""
|
18
|
+
pass
|
19
|
+
|
20
|
+
|
21
|
+
def downgrade() -> None:
|
22
|
+
"""Downgrade database schema and/or data back to the previous revision."""
|
23
|
+
pass
|
@@ -0,0 +1,23 @@
|
|
1
|
+
"""Release [0.61.0].
|
2
|
+
|
3
|
+
Revision ID: 0.61.0
|
4
|
+
Revises: 0d707865f404
|
5
|
+
Create Date: 2024-07-08 15:52:54.765307
|
6
|
+
|
7
|
+
"""
|
8
|
+
|
9
|
+
# revision identifiers, used by Alembic.
|
10
|
+
revision = "0.61.0"
|
11
|
+
down_revision = "0d707865f404"
|
12
|
+
branch_labels = None
|
13
|
+
depends_on = None
|
14
|
+
|
15
|
+
|
16
|
+
def upgrade() -> None:
|
17
|
+
"""Upgrade database schema and/or data, creating a new revision."""
|
18
|
+
pass
|
19
|
+
|
20
|
+
|
21
|
+
def downgrade() -> None:
|
22
|
+
"""Downgrade database schema and/or data back to the previous revision."""
|
23
|
+
pass
|
@@ -0,0 +1,30 @@
|
|
1
|
+
"""adding labels to stacks [0d707865f404].
|
2
|
+
|
3
|
+
Revision ID: 0d707865f404
|
4
|
+
Revises: 0.60.0
|
5
|
+
Create Date: 2024-07-04 16:10:07.709184
|
6
|
+
|
7
|
+
"""
|
8
|
+
|
9
|
+
import sqlalchemy as sa
|
10
|
+
from alembic import op
|
11
|
+
|
12
|
+
# revision identifiers, used by Alembic.
|
13
|
+
revision = "0d707865f404"
|
14
|
+
down_revision = "0.60.0"
|
15
|
+
branch_labels = None
|
16
|
+
depends_on = None
|
17
|
+
|
18
|
+
|
19
|
+
def upgrade() -> None:
|
20
|
+
"""Upgrade database schema and/or data, creating a new revision."""
|
21
|
+
with op.batch_alter_table("stack", schema=None) as batch_op:
|
22
|
+
batch_op.add_column(
|
23
|
+
sa.Column("labels", sa.LargeBinary(), nullable=True)
|
24
|
+
)
|
25
|
+
|
26
|
+
|
27
|
+
def downgrade() -> None:
|
28
|
+
"""Downgrade database schema and/or data back to the previous revision."""
|
29
|
+
with op.batch_alter_table("stack", schema=None) as batch_op:
|
30
|
+
batch_op.drop_column("labels")
|