zenml-nightly 0.63.0.dev20240801__py3-none-any.whl → 0.64.0.dev20240811__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 +2 -2
- RELEASE_NOTES.md +79 -0
- zenml/VERSION +1 -1
- zenml/__init__.py +0 -4
- zenml/analytics/enums.py +0 -6
- zenml/cli/__init__.py +0 -61
- zenml/cli/base.py +1 -1
- zenml/cli/web_login.py +8 -0
- zenml/client.py +0 -4
- zenml/config/build_configuration.py +43 -17
- zenml/config/docker_settings.py +80 -57
- zenml/config/source.py +58 -0
- zenml/constants.py +9 -2
- zenml/entrypoints/base_entrypoint_configuration.py +53 -8
- zenml/enums.py +1 -1
- zenml/environment.py +25 -9
- zenml/image_builders/base_image_builder.py +1 -1
- zenml/image_builders/build_context.py +25 -72
- zenml/integrations/azure/__init__.py +4 -0
- zenml/integrations/azure/flavors/__init__.py +11 -0
- zenml/integrations/azure/flavors/azureml_orchestrator_flavor.py +263 -0
- zenml/{_hub → integrations/azure/orchestrators}/__init__.py +7 -2
- zenml/integrations/azure/orchestrators/azureml_orchestrator.py +544 -0
- zenml/integrations/azure/orchestrators/azureml_orchestrator_entrypoint_config.py +86 -0
- zenml/integrations/azure/step_operators/azureml_step_operator.py +3 -0
- zenml/integrations/databricks/flavors/databricks_orchestrator_flavor.py +9 -0
- zenml/integrations/gcp/orchestrators/vertex_orchestrator.py +7 -2
- zenml/integrations/gcp/service_connectors/gcp_service_connector.py +123 -6
- zenml/integrations/kaniko/image_builders/kaniko_image_builder.py +1 -1
- zenml/integrations/mlflow/__init__.py +1 -1
- zenml/integrations/mlflow/experiment_trackers/mlflow_experiment_tracker.py +3 -1
- zenml/integrations/mlflow/flavors/mlflow_experiment_tracker_flavor.py +3 -0
- zenml/logger.py +13 -0
- zenml/models/__init__.py +0 -12
- zenml/models/v2/core/pipeline_deployment.py +21 -29
- zenml/models/v2/core/pipeline_run.py +13 -0
- zenml/models/v2/core/server_settings.py +12 -0
- zenml/models/v2/core/user.py +0 -21
- zenml/models/v2/misc/server_models.py +7 -1
- zenml/models/v2/misc/user_auth.py +0 -7
- zenml/new/pipelines/build_utils.py +193 -38
- zenml/new/pipelines/code_archive.py +157 -0
- zenml/new/pipelines/pipeline.py +29 -2
- zenml/new/pipelines/run_utils.py +67 -1
- zenml/service_connectors/service_connector_utils.py +14 -0
- zenml/stack_deployments/aws_stack_deployment.py +26 -3
- zenml/stack_deployments/azure_stack_deployment.py +11 -6
- zenml/stack_deployments/gcp_stack_deployment.py +24 -2
- zenml/stack_deployments/stack_deployment.py +17 -2
- zenml/steps/base_step.py +3 -0
- zenml/utils/archivable.py +149 -0
- zenml/utils/code_utils.py +244 -0
- zenml/utils/notebook_utils.py +122 -0
- zenml/utils/pipeline_docker_image_builder.py +3 -96
- zenml/utils/source_utils.py +109 -1
- zenml/zen_server/dashboard/assets/{404-CI13wQp4.js → 404-CRAA_Lew.js} +1 -1
- zenml/zen_server/dashboard/assets/@radix-BXWm7HOa.js +85 -0
- zenml/zen_server/dashboard/assets/{@react-router-CO-OsFwI.js → @react-router-l3lMcXA2.js} +1 -1
- zenml/zen_server/dashboard/assets/{@reactflow-DIYUhKYX.js → @reactflow-CeVxyqYT.js} +2 -2
- zenml/zen_server/dashboard/assets/{@tanstack-k96lU_C-.js → @tanstack-FmcYZMuX.js} +4 -4
- zenml/zen_server/dashboard/assets/AlertDialogDropdownItem-ErO9aOgK.js +1 -0
- zenml/zen_server/dashboard/assets/{AwarenessChannel-BNg5uWgI.js → AwarenessChannel-CLXo5rKM.js} +1 -1
- zenml/zen_server/dashboard/assets/{CodeSnippet-Cyp7f4dM.js → CodeSnippet-D0VLxT2A.js} +1 -1
- zenml/zen_server/dashboard/assets/{CollapsibleCard-Cu_A9W57.js → CollapsibleCard-BaUPiVg0.js} +1 -1
- zenml/zen_server/dashboard/assets/{Commands-DmQwTXjj.js → Commands-JrcZK-3j.js} +1 -1
- zenml/zen_server/dashboard/assets/CopyButton-Dbo52T1K.js +2 -0
- zenml/zen_server/dashboard/assets/{CsvVizualization-BvqItd-O.js → CsvVizualization-D3kAypDj.js} +3 -3
- zenml/zen_server/dashboard/assets/DisplayDate-DizbSeT-.js +1 -0
- zenml/zen_server/dashboard/assets/EditSecretDialog-Bd7mFLS4.js +1 -0
- zenml/zen_server/dashboard/assets/{EmptyState-BMLnFVlB.js → EmptyState-BHblM39I.js} +1 -1
- zenml/zen_server/dashboard/assets/{Error-DbXCTGua.js → Error-C6LeJSER.js} +1 -1
- zenml/zen_server/dashboard/assets/{ExecutionStatus-9zM7eaLh.js → ExecutionStatus-jH4OrWBq.js} +1 -1
- zenml/zen_server/dashboard/assets/{Helpbox-BIiNc-uH.js → Helpbox-aAB2XP-z.js} +1 -1
- zenml/zen_server/dashboard/assets/{Infobox-iv1Nu1A0.js → Infobox-BQ0aty32.js} +1 -1
- zenml/zen_server/dashboard/assets/{InlineAvatar-BvBtO2Dp.js → InlineAvatar-DpTLgM3Q.js} +1 -1
- zenml/zen_server/dashboard/assets/Lock-CNyJvf2r.js +1 -0
- zenml/zen_server/dashboard/assets/{MarkdownVisualization-xp3hhULl.js → MarkdownVisualization-Bajxn0HY.js} +1 -1
- zenml/zen_server/dashboard/assets/NumberBox-BmKE0qnO.js +1 -0
- zenml/zen_server/dashboard/assets/{PasswordChecker-DUveqlva.js → PasswordChecker-yGGoJSB-.js} +1 -1
- zenml/zen_server/dashboard/assets/{ProviderRadio-pSAvrGRS.js → ProviderRadio-BBqkIuTd.js} +1 -1
- zenml/zen_server/dashboard/assets/RadioItem-xLhXoiFV.js +1 -0
- zenml/zen_server/dashboard/assets/SearchField-C9R0mdaX.js +1 -0
- zenml/zen_server/dashboard/assets/{SetPassword-BOxpgh6N.js → SetPassword-52sNxNiO.js} +1 -1
- zenml/zen_server/dashboard/assets/{SuccessStep-CTSKN2lp.js → SuccessStep-DlkItqYG.js} +1 -1
- zenml/zen_server/dashboard/assets/{Tick-Bnr2TpW6.js → Tick-uxv80Q6a.js} +1 -1
- zenml/zen_server/dashboard/assets/{UpdatePasswordSchemas-BeCeaRW5.js → UpdatePasswordSchemas-oN4G3sKz.js} +1 -1
- zenml/zen_server/dashboard/assets/{aws-BgKTfTfx.js → aws-0_3UsPif.js} +1 -1
- zenml/zen_server/dashboard/assets/{check-circle-i56092KI.js → check-circle-1_I207rW.js} +1 -1
- zenml/zen_server/dashboard/assets/{chevron-down-D_ZlKMqH.js → chevron-down-BpaF8JqM.js} +1 -1
- zenml/zen_server/dashboard/assets/{chevron-right-double-CZBOf6JM.js → chevron-right-double-Dk8e2L99.js} +1 -1
- zenml/zen_server/dashboard/assets/{cloud-only-qelmY92E.js → cloud-only-BkUuI0lZ.js} +1 -1
- zenml/zen_server/dashboard/assets/components-Br2ezRib.js +1 -0
- zenml/zen_server/dashboard/assets/{copy-BXNk6BjL.js → copy-f3XGPPxt.js} +1 -1
- zenml/zen_server/dashboard/assets/{database-1xWSgZfO.js → database-cXYNX9tt.js} +1 -1
- zenml/zen_server/dashboard/assets/{docker-CQMVm_4d.js → docker-8uj__HHK.js} +1 -1
- zenml/zen_server/dashboard/assets/{dots-horizontal-BObFzD5l.js → dots-horizontal-sKQlWEni.js} +1 -1
- zenml/zen_server/dashboard/assets/edit-C0MVvPD2.js +1 -0
- zenml/zen_server/dashboard/assets/{file-text-CqD_iu6l.js → file-text-B9JibxTs.js} +1 -1
- zenml/zen_server/dashboard/assets/{help-bu_DgLKI.js → help-FuHlZwn0.js} +1 -1
- zenml/zen_server/dashboard/assets/index-Bd1xgUQG.js +1 -0
- zenml/zen_server/dashboard/assets/index-DaGknux4.css +1 -0
- zenml/zen_server/dashboard/assets/{index-KsTz2dHG.js → index-DhIZtpxB.js} +5 -5
- zenml/zen_server/dashboard/assets/{index.esm-CbHNSeVw.js → index.esm-DT4uyn2i.js} +1 -1
- zenml/zen_server/dashboard/assets/layout-D6oiSbfd.js +1 -0
- zenml/zen_server/dashboard/assets/{login-mutation-DRpbESS7.js → login-mutation-13A_JSVA.js} +1 -1
- zenml/zen_server/dashboard/assets/{logs-D8k8BVFf.js → logs-CgeE2vZP.js} +1 -1
- zenml/zen_server/dashboard/assets/{not-found-Dfx9hfkf.js → not-found-B0Mmb90p.js} +1 -1
- zenml/zen_server/dashboard/assets/{package-ClbU3KUi.js → package-DdkziX79.js} +1 -1
- zenml/zen_server/dashboard/assets/page-7-v2OBm-.js +1 -0
- zenml/zen_server/dashboard/assets/{page-f3jBVI5Z.js → page-B3ozwdD1.js} +1 -1
- zenml/zen_server/dashboard/assets/{page-DYBNGxJt.js → page-BGwA9B1M.js} +1 -1
- zenml/zen_server/dashboard/assets/{page-C176KxyB.js → page-BkjAUyTA.js} +1 -1
- zenml/zen_server/dashboard/assets/page-BnacgBiy.js +1 -0
- zenml/zen_server/dashboard/assets/{page-CzucfYPo.js → page-BxF_KMQ3.js} +2 -2
- zenml/zen_server/dashboard/assets/page-C4POHC0K.js +1 -0
- zenml/zen_server/dashboard/assets/page-C9kudd44.js +9 -0
- zenml/zen_server/dashboard/assets/page-CA1j3GpJ.js +1 -0
- zenml/zen_server/dashboard/assets/page-CCY6yfmu.js +1 -0
- zenml/zen_server/dashboard/assets/page-CgTe7Bme.js +1 -0
- zenml/zen_server/dashboard/assets/{page-DtpwnNXq.js → page-Cgn-6v2Y.js} +1 -1
- zenml/zen_server/dashboard/assets/page-CxQmQqDw.js +1 -0
- zenml/zen_server/dashboard/assets/page-D2Goey3H.js +1 -0
- zenml/zen_server/dashboard/assets/page-DLpOnf7u.js +1 -0
- zenml/zen_server/dashboard/assets/{page-DVPxY5fT.js → page-DSTQnBk-.js} +1 -1
- zenml/zen_server/dashboard/assets/{page-BoFtUD9H.js → page-DTysUGOy.js} +1 -1
- zenml/zen_server/dashboard/assets/{page-p2hLJdS2.js → page-D_EXUFJb.js} +1 -1
- zenml/zen_server/dashboard/assets/page-Db15QzsM.js +1 -0
- zenml/zen_server/dashboard/assets/{page-Btu39x7k.js → page-DugsjcQ_.js} +1 -1
- zenml/zen_server/dashboard/assets/{page-CZe9GEBF.js → page-OFKSPyN7.js} +1 -1
- zenml/zen_server/dashboard/assets/{page-CDgZmwxP.js → page-RnG-qhv9.js} +1 -1
- zenml/zen_server/dashboard/assets/{page-Cjn97HMv.js → page-T2BtjwPl.js} +1 -1
- zenml/zen_server/dashboard/assets/page-TXe1Eo3Z.js +1 -0
- zenml/zen_server/dashboard/assets/{page-BxiWdeyg.js → page-YiF_fNbe.js} +1 -1
- zenml/zen_server/dashboard/assets/{page-399pVZHU.js → page-hQaiQXfg.js} +1 -1
- zenml/zen_server/dashboard/assets/persist-3-5nOJ6m.js +1 -0
- zenml/zen_server/dashboard/assets/{play-circle-CNtZKDnW.js → play-circle-XSkLR12B.js} +1 -1
- zenml/zen_server/dashboard/assets/{plus-DOeLmm7C.js → plus-FB9-lEq_.js} +1 -1
- zenml/zen_server/dashboard/assets/refresh-COb6KYDi.js +1 -0
- zenml/zen_server/dashboard/assets/sharedSchema-BoYx_B_L.js +14 -0
- zenml/zen_server/dashboard/assets/{stack-detail-query-Ck7j7BP_.js → stack-detail-query-B-US_-wa.js} +1 -1
- zenml/zen_server/dashboard/assets/{terminal-By9cErXc.js → terminal-grtjrIEJ.js} +1 -1
- zenml/zen_server/dashboard/assets/trash-Cd5CSFqA.js +1 -0
- zenml/zen_server/dashboard/assets/{update-server-settings-mutation-f3ZT7psb.js → update-server-settings-mutation-B8GB_ubU.js} +1 -1
- zenml/zen_server/dashboard/assets/{url-rGEp5Umh.js → url-hcMJkz8p.js} +1 -1
- zenml/zen_server/dashboard/assets/{zod-BtSyGx4C.js → zod-CnykDKJj.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.2fa6e528a6e7447caaf35dadfe7514bb.js → precache-manifest.9c473c96a43298343a7ce1256183123b.js} +4 -4
- zenml/zen_server/dashboard_legacy/service-worker.js +1 -1
- zenml/zen_server/dashboard_legacy/static/js/{main.4aab7e98.chunk.js → main.463c90b9.chunk.js} +2 -2
- zenml/zen_server/dashboard_legacy/static/js/{main.4aab7e98.chunk.js.map → main.463c90b9.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/routers/stack_deployment_endpoints.py +6 -0
- zenml/zen_server/routers/users_endpoints.py +0 -7
- zenml/zen_server/utils.py +75 -0
- zenml/zen_server/zen_server_api.py +52 -1
- zenml/zen_stores/base_zen_store.py +7 -1
- zenml/zen_stores/migrations/versions/0.64.0_release.py +23 -0
- zenml/zen_stores/migrations/versions/026d4577b6a0_add_code_path.py +39 -0
- zenml/zen_stores/migrations/versions/3dcc5d20e82f_add_last_user_activity.py +51 -0
- zenml/zen_stores/migrations/versions/909550c7c4da_remove_user_hub_token.py +36 -0
- zenml/zen_stores/rest_zen_store.py +5 -3
- zenml/zen_stores/schemas/pipeline_deployment_schemas.py +3 -0
- zenml/zen_stores/schemas/pipeline_run_schemas.py +3 -0
- zenml/zen_stores/schemas/server_settings_schemas.py +2 -0
- zenml/zen_stores/schemas/user_schemas.py +0 -2
- zenml/zen_stores/sql_zen_store.py +25 -1
- {zenml_nightly-0.63.0.dev20240801.dist-info → zenml_nightly-0.64.0.dev20240811.dist-info}/METADATA +3 -3
- {zenml_nightly-0.63.0.dev20240801.dist-info → zenml_nightly-0.64.0.dev20240811.dist-info}/RECORD +174 -157
- zenml/_hub/client.py +0 -289
- zenml/_hub/constants.py +0 -21
- zenml/_hub/utils.py +0 -79
- zenml/cli/hub.py +0 -1116
- zenml/models/v2/misc/hub_plugin_models.py +0 -79
- zenml/zen_server/dashboard/assets/@radix-CFOkMR_E.js +0 -85
- zenml/zen_server/dashboard/assets/CopyButton-B3sWVJ4Z.js +0 -2
- zenml/zen_server/dashboard/assets/DisplayDate-DYgIjlDF.js +0 -1
- zenml/zen_server/dashboard/assets/SearchField-CXoBknpt.js +0 -1
- zenml/zen_server/dashboard/assets/components-DWe4cTjS.js +0 -1
- zenml/zen_server/dashboard/assets/index-vfjX_fJV.css +0 -1
- zenml/zen_server/dashboard/assets/page-C6tXXjnK.js +0 -1
- zenml/zen_server/dashboard/assets/page-CP9obrnG.js +0 -1
- zenml/zen_server/dashboard/assets/page-CaTOsNNw.js +0 -1
- zenml/zen_server/dashboard/assets/page-CmXmB_5i.js +0 -1
- zenml/zen_server/dashboard/assets/page-CvGAOfad.js +0 -1
- zenml/zen_server/dashboard/assets/page-D0bbc-qr.js +0 -5
- zenml/zen_server/dashboard/assets/page-DLEtD2ex.js +0 -1
- zenml/zen_server/dashboard/assets/page-DupV0aBd.js +0 -1
- zenml/zen_server/dashboard/assets/page-EweAR81y.js +0 -1
- zenml/zen_server/dashboard/assets/page-w-YaL77M.js +0 -9
- zenml/zen_server/dashboard/assets/persist-BReKApOc.js +0 -14
- zenml/zen_server/dashboard/assets/secrets-video-OBJ6irhH.svg +0 -21
- zenml/zen_server/dashboard/assets/stacks-video-7gfxpAq4.svg +0 -21
- {zenml_nightly-0.63.0.dev20240801.dist-info → zenml_nightly-0.64.0.dev20240811.dist-info}/LICENSE +0 -0
- {zenml_nightly-0.63.0.dev20240801.dist-info → zenml_nightly-0.64.0.dev20240811.dist-info}/WHEEL +0 -0
- {zenml_nightly-0.63.0.dev20240801.dist-info → zenml_nightly-0.64.0.dev20240811.dist-info}/entry_points.txt +0 -0
@@ -20,8 +20,8 @@ ZenML is an open-source MLOps framework designed to help you create robust, main
|
|
20
20
|
To install the ZenML chart directly from Amazon ECR, use the following command:
|
21
21
|
|
22
22
|
```bash
|
23
|
-
# example command for version 0.
|
24
|
-
helm install my-zenml oci://public.ecr.aws/zenml/zenml --version 0.
|
23
|
+
# example command for version 0.64.0
|
24
|
+
helm install my-zenml oci://public.ecr.aws/zenml/zenml --version 0.64.0
|
25
25
|
```
|
26
26
|
|
27
27
|
Note: Ensure you have OCI support enabled in your Helm client and that you are authenticated with Amazon ECR.
|
@@ -78,6 +78,7 @@ def get_stack_deployment_config(
|
|
78
78
|
provider: StackDeploymentProvider,
|
79
79
|
stack_name: str,
|
80
80
|
location: Optional[str] = None,
|
81
|
+
terraform: bool = False,
|
81
82
|
auth_context: AuthContext = Security(authorize),
|
82
83
|
) -> StackDeploymentConfig:
|
83
84
|
"""Return the URL to deploy the ZenML stack to the specified cloud provider.
|
@@ -87,6 +88,7 @@ def get_stack_deployment_config(
|
|
87
88
|
provider: The stack deployment provider.
|
88
89
|
stack_name: The name of the stack.
|
89
90
|
location: The location where the stack should be deployed.
|
91
|
+
terraform: Whether the stack should be deployed using Terraform.
|
90
92
|
auth_context: The authentication context.
|
91
93
|
|
92
94
|
Returns:
|
@@ -118,6 +120,7 @@ def get_stack_deployment_config(
|
|
118
120
|
api_token = token.encode(expires=expires)
|
119
121
|
|
120
122
|
return stack_deployment_class(
|
123
|
+
terraform=terraform,
|
121
124
|
stack_name=stack_name,
|
122
125
|
location=location,
|
123
126
|
zenml_server_url=str(url),
|
@@ -134,6 +137,7 @@ def get_deployed_stack(
|
|
134
137
|
stack_name: str,
|
135
138
|
location: Optional[str] = None,
|
136
139
|
date_start: Optional[datetime.datetime] = None,
|
140
|
+
terraform: bool = False,
|
137
141
|
_: AuthContext = Security(authorize),
|
138
142
|
) -> Optional[DeployedStack]:
|
139
143
|
"""Return a matching ZenML stack that was deployed and registered.
|
@@ -143,6 +147,7 @@ def get_deployed_stack(
|
|
143
147
|
stack_name: The name of the stack.
|
144
148
|
location: The location where the stack should be deployed.
|
145
149
|
date_start: The date when the deployment started.
|
150
|
+
terraform: Whether the stack was deployed using Terraform.
|
146
151
|
|
147
152
|
Returns:
|
148
153
|
The ZenML stack that was deployed and registered or None if the stack
|
@@ -150,6 +155,7 @@ def get_deployed_stack(
|
|
150
155
|
"""
|
151
156
|
stack_deployment_class = get_stack_deployment_class(provider)
|
152
157
|
return stack_deployment_class(
|
158
|
+
terraform=terraform,
|
153
159
|
stack_name=stack_name,
|
154
160
|
location=location,
|
155
161
|
# These fields are not needed for this operation
|
@@ -286,7 +286,6 @@ if server_config().auth_scheme != AuthScheme.EXTERNAL:
|
|
286
286
|
# - active
|
287
287
|
# - password
|
288
288
|
# - email_opted_in + email
|
289
|
-
# - hub_token
|
290
289
|
#
|
291
290
|
safe_user_update = user_update.create_copy(
|
292
291
|
exclude={
|
@@ -298,7 +297,6 @@ if server_config().auth_scheme != AuthScheme.EXTERNAL:
|
|
298
297
|
"old_password",
|
299
298
|
"email_opted_in",
|
300
299
|
"email",
|
301
|
-
"hub_token",
|
302
300
|
},
|
303
301
|
)
|
304
302
|
|
@@ -387,7 +385,6 @@ if server_config().auth_scheme != AuthScheme.EXTERNAL:
|
|
387
385
|
if (
|
388
386
|
user_update.email_opted_in is not None
|
389
387
|
or user_update.email is not None
|
390
|
-
or user_update.hub_token is not None
|
391
388
|
):
|
392
389
|
if user.id != auth_context.user.id:
|
393
390
|
raise IllegalOperationError(
|
@@ -399,8 +396,6 @@ if server_config().auth_scheme != AuthScheme.EXTERNAL:
|
|
399
396
|
if safe_user_update.email_opted_in is not None:
|
400
397
|
safe_user_update.email_opted_in = user_update.email_opted_in
|
401
398
|
safe_user_update.email = user_update.email
|
402
|
-
if safe_user_update.hub_token is not None:
|
403
|
-
safe_user_update.hub_token = user_update.hub_token
|
404
399
|
|
405
400
|
updated_user = zen_store().update_user(
|
406
401
|
user_id=user.id,
|
@@ -444,7 +439,6 @@ if server_config().auth_scheme != AuthScheme.EXTERNAL:
|
|
444
439
|
# - is_admin
|
445
440
|
# - active
|
446
441
|
# - old_password
|
447
|
-
# - hub_token
|
448
442
|
#
|
449
443
|
safe_user_update = user_update.create_copy(
|
450
444
|
exclude={
|
@@ -453,7 +447,6 @@ if server_config().auth_scheme != AuthScheme.EXTERNAL:
|
|
453
447
|
"is_admin",
|
454
448
|
"active",
|
455
449
|
"old_password",
|
456
|
-
"hub_token",
|
457
450
|
},
|
458
451
|
)
|
459
452
|
|
zenml/zen_server/utils.py
CHANGED
@@ -17,8 +17,10 @@ import inspect
|
|
17
17
|
import os
|
18
18
|
from functools import wraps
|
19
19
|
from typing import (
|
20
|
+
TYPE_CHECKING,
|
20
21
|
Any,
|
21
22
|
Callable,
|
23
|
+
List,
|
22
24
|
Optional,
|
23
25
|
Tuple,
|
24
26
|
Type,
|
@@ -33,7 +35,10 @@ from pydantic import BaseModel, ValidationError
|
|
33
35
|
from zenml.config.global_config import GlobalConfiguration
|
34
36
|
from zenml.config.server_config import ServerConfiguration
|
35
37
|
from zenml.constants import (
|
38
|
+
API,
|
36
39
|
ENV_ZENML_SERVER,
|
40
|
+
INFO,
|
41
|
+
VERSION_1,
|
37
42
|
)
|
38
43
|
from zenml.enums import ServerProviderType
|
39
44
|
from zenml.exceptions import IllegalOperationError, OAuthError
|
@@ -53,6 +58,9 @@ from zenml.zen_server.template_execution.workload_manager_interface import (
|
|
53
58
|
)
|
54
59
|
from zenml.zen_stores.sql_zen_store import SqlZenStore
|
55
60
|
|
61
|
+
if TYPE_CHECKING:
|
62
|
+
from fastapi import Request
|
63
|
+
|
56
64
|
logger = get_logger(__name__)
|
57
65
|
|
58
66
|
_zen_store: Optional["SqlZenStore"] = None
|
@@ -570,3 +578,70 @@ def verify_admin_status_if_no_rbac(
|
|
570
578
|
"without RBAC enabled.",
|
571
579
|
)
|
572
580
|
return
|
581
|
+
|
582
|
+
|
583
|
+
def is_user_request(request: "Request") -> bool:
|
584
|
+
"""Determine if the incoming request is a user request.
|
585
|
+
|
586
|
+
This function checks various aspects of the request to determine
|
587
|
+
if it's a user-initiated request or a system request.
|
588
|
+
|
589
|
+
Args:
|
590
|
+
request: The incoming FastAPI request object.
|
591
|
+
|
592
|
+
Returns:
|
593
|
+
True if it's a user request, False otherwise.
|
594
|
+
"""
|
595
|
+
# Define system paths that should be excluded
|
596
|
+
system_paths: List[str] = [
|
597
|
+
"/health",
|
598
|
+
"/metrics",
|
599
|
+
"/system",
|
600
|
+
"/docs",
|
601
|
+
"/redoc",
|
602
|
+
"/openapi.json",
|
603
|
+
]
|
604
|
+
|
605
|
+
user_prefix = f"{API}{VERSION_1}"
|
606
|
+
excluded_user_apis = [INFO]
|
607
|
+
# Check if this is not an excluded endpoint
|
608
|
+
if request.url.path in [
|
609
|
+
user_prefix + suffix for suffix in excluded_user_apis
|
610
|
+
]:
|
611
|
+
return False
|
612
|
+
|
613
|
+
# Check if this is other user request
|
614
|
+
if request.url.path.startswith(user_prefix):
|
615
|
+
return True
|
616
|
+
|
617
|
+
# Exclude system paths
|
618
|
+
if any(request.url.path.startswith(path) for path in system_paths):
|
619
|
+
return False
|
620
|
+
|
621
|
+
# Exclude requests with specific headers
|
622
|
+
if request.headers.get("X-System-Request") == "true":
|
623
|
+
return False
|
624
|
+
|
625
|
+
# Exclude requests from certain user agents (e.g., monitoring tools)
|
626
|
+
user_agent = request.headers.get("User-Agent", "").lower()
|
627
|
+
system_agents = ["prometheus", "datadog", "newrelic", "pingdom"]
|
628
|
+
if any(agent in user_agent for agent in system_agents):
|
629
|
+
return False
|
630
|
+
|
631
|
+
# Check for internal IP addresses
|
632
|
+
client_host = request.client.host if request.client else None
|
633
|
+
if client_host and (
|
634
|
+
client_host.startswith("10.") or client_host.startswith("192.168.")
|
635
|
+
):
|
636
|
+
return False
|
637
|
+
|
638
|
+
# Exclude OPTIONS requests (often used for CORS preflight)
|
639
|
+
if request.method == "OPTIONS":
|
640
|
+
return False
|
641
|
+
|
642
|
+
# Exclude specific query parameters that might indicate system requests
|
643
|
+
if request.query_params.get("system_check"):
|
644
|
+
return False
|
645
|
+
|
646
|
+
# If none of the above conditions are met, consider it a user request
|
647
|
+
return True
|
@@ -22,6 +22,7 @@ To run this file locally, execute:
|
|
22
22
|
|
23
23
|
import os
|
24
24
|
from asyncio.log import logger
|
25
|
+
from datetime import datetime, timedelta, timezone
|
25
26
|
from genericpath import isfile
|
26
27
|
from typing import Any, List
|
27
28
|
|
@@ -36,7 +37,11 @@ from starlette.responses import FileResponse
|
|
36
37
|
|
37
38
|
import zenml
|
38
39
|
from zenml.analytics import source_context
|
39
|
-
from zenml.constants import
|
40
|
+
from zenml.constants import (
|
41
|
+
API,
|
42
|
+
DEFAULT_ZENML_SERVER_REPORT_USER_ACTIVITY_TO_DB_SECONDS,
|
43
|
+
HEALTH,
|
44
|
+
)
|
40
45
|
from zenml.enums import AuthScheme, SourceContextTypes
|
41
46
|
from zenml.zen_server.exceptions import error_detail
|
42
47
|
from zenml.zen_server.routers import (
|
@@ -80,8 +85,10 @@ from zenml.zen_server.utils import (
|
|
80
85
|
initialize_secure_headers,
|
81
86
|
initialize_workload_manager,
|
82
87
|
initialize_zen_store,
|
88
|
+
is_user_request,
|
83
89
|
secure_headers,
|
84
90
|
server_config,
|
91
|
+
zen_store,
|
85
92
|
)
|
86
93
|
|
87
94
|
if server_config().use_legacy_dashboard:
|
@@ -109,6 +116,12 @@ app = FastAPI(
|
|
109
116
|
default_response_class=ORJSONResponse,
|
110
117
|
)
|
111
118
|
|
119
|
+
# Initialize last_user_activity
|
120
|
+
last_user_activity: datetime = datetime.now(timezone.utc)
|
121
|
+
last_user_activity_reported: datetime = datetime.now(timezone.utc) + timedelta(
|
122
|
+
seconds=-DEFAULT_ZENML_SERVER_REPORT_USER_ACTIVITY_TO_DB_SECONDS
|
123
|
+
)
|
124
|
+
|
112
125
|
|
113
126
|
# Customize the default request validation handler that comes with FastAPI
|
114
127
|
# to return a JSON response that matches the ZenML API spec.
|
@@ -159,6 +172,44 @@ async def set_secure_headers(request: Request, call_next: Any) -> Any:
|
|
159
172
|
return response
|
160
173
|
|
161
174
|
|
175
|
+
@app.middleware("http")
|
176
|
+
async def track_last_user_activity(request: Request, call_next: Any) -> Any:
|
177
|
+
"""A middleware to track last user activity.
|
178
|
+
|
179
|
+
This middleware checks if the incoming request is a user request and
|
180
|
+
updates the last activity timestamp if it is.
|
181
|
+
|
182
|
+
Args:
|
183
|
+
request: The incoming request object.
|
184
|
+
call_next: A function that will receive the request as a parameter and
|
185
|
+
pass it to the corresponding path operation.
|
186
|
+
|
187
|
+
Returns:
|
188
|
+
The response to the request.
|
189
|
+
"""
|
190
|
+
global last_user_activity
|
191
|
+
global last_user_activity_reported
|
192
|
+
|
193
|
+
try:
|
194
|
+
if is_user_request(request):
|
195
|
+
last_user_activity = datetime.now(timezone.utc)
|
196
|
+
except Exception as e:
|
197
|
+
logger.debug(
|
198
|
+
f"An unexpected error occurred while checking user activity: {e}"
|
199
|
+
)
|
200
|
+
if (
|
201
|
+
(
|
202
|
+
datetime.now(timezone.utc) - last_user_activity_reported
|
203
|
+
).total_seconds()
|
204
|
+
> DEFAULT_ZENML_SERVER_REPORT_USER_ACTIVITY_TO_DB_SECONDS
|
205
|
+
):
|
206
|
+
last_user_activity_reported = datetime.now(timezone.utc)
|
207
|
+
zen_store()._update_last_user_activity_timestamp(
|
208
|
+
last_user_activity=last_user_activity
|
209
|
+
)
|
210
|
+
return await call_next(request)
|
211
|
+
|
212
|
+
|
162
213
|
@app.middleware("http")
|
163
214
|
async def infer_source_context(request: Request, call_next: Any) -> Any:
|
164
215
|
"""A middleware to track the source of an event.
|
@@ -39,6 +39,7 @@ from zenml.constants import (
|
|
39
39
|
DEFAULT_WORKSPACE_NAME,
|
40
40
|
ENV_ZENML_DEFAULT_WORKSPACE_NAME,
|
41
41
|
IS_DEBUG_ENV,
|
42
|
+
ZENML_PRO_CONNECTION_ISSUES_SUSPENDED_PAUSED_TENANT_HINT,
|
42
43
|
)
|
43
44
|
from zenml.enums import (
|
44
45
|
SecretsStoreType,
|
@@ -171,9 +172,14 @@ class BaseZenStore(
|
|
171
172
|
)
|
172
173
|
|
173
174
|
except Exception as e:
|
175
|
+
zenml_pro_extra = ""
|
176
|
+
if ".zenml.io" in self.url:
|
177
|
+
zenml_pro_extra = (
|
178
|
+
ZENML_PRO_CONNECTION_ISSUES_SUSPENDED_PAUSED_TENANT_HINT
|
179
|
+
)
|
174
180
|
raise RuntimeError(
|
175
181
|
f"Error initializing {self.type.value} store with URL "
|
176
|
-
f"'{self.url}': {str(e)}"
|
182
|
+
f"'{self.url}': {str(e)}" + zenml_pro_extra
|
177
183
|
) from e
|
178
184
|
|
179
185
|
if not skip_default_registrations:
|
@@ -0,0 +1,23 @@
|
|
1
|
+
"""Release [0.64.0].
|
2
|
+
|
3
|
+
Revision ID: 0.64.0
|
4
|
+
Revises: 3dcc5d20e82f
|
5
|
+
Create Date: 2024-08-08 12:25:12.058636
|
6
|
+
|
7
|
+
"""
|
8
|
+
|
9
|
+
# revision identifiers, used by Alembic.
|
10
|
+
revision = "0.64.0"
|
11
|
+
down_revision = "3dcc5d20e82f"
|
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,39 @@
|
|
1
|
+
"""Add code path [026d4577b6a0].
|
2
|
+
|
3
|
+
Revision ID: 026d4577b6a0
|
4
|
+
Revises: 909550c7c4da
|
5
|
+
Create Date: 2024-07-30 16:53:32.777594
|
6
|
+
|
7
|
+
"""
|
8
|
+
|
9
|
+
import sqlalchemy as sa
|
10
|
+
import sqlmodel
|
11
|
+
from alembic import op
|
12
|
+
|
13
|
+
# revision identifiers, used by Alembic.
|
14
|
+
revision = "026d4577b6a0"
|
15
|
+
down_revision = "909550c7c4da"
|
16
|
+
branch_labels = None
|
17
|
+
depends_on = None
|
18
|
+
|
19
|
+
|
20
|
+
def upgrade() -> None:
|
21
|
+
"""Upgrade database schema and/or data, creating a new revision."""
|
22
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
23
|
+
with op.batch_alter_table("pipeline_deployment", schema=None) as batch_op:
|
24
|
+
batch_op.add_column(
|
25
|
+
sa.Column(
|
26
|
+
"code_path", sqlmodel.sql.sqltypes.AutoString(), nullable=True
|
27
|
+
)
|
28
|
+
)
|
29
|
+
|
30
|
+
# ### end Alembic commands ###
|
31
|
+
|
32
|
+
|
33
|
+
def downgrade() -> None:
|
34
|
+
"""Downgrade database schema and/or data back to the previous revision."""
|
35
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
36
|
+
with op.batch_alter_table("pipeline_deployment", schema=None) as batch_op:
|
37
|
+
batch_op.drop_column("code_path")
|
38
|
+
|
39
|
+
# ### end Alembic commands ###
|
@@ -0,0 +1,51 @@
|
|
1
|
+
"""add last_user_activity [3dcc5d20e82f].
|
2
|
+
|
3
|
+
Revision ID: 3dcc5d20e82f
|
4
|
+
Revises: 909550c7c4da
|
5
|
+
Create Date: 2024-08-07 14:49:07.623500
|
6
|
+
|
7
|
+
"""
|
8
|
+
|
9
|
+
from datetime import datetime, timezone
|
10
|
+
|
11
|
+
import sqlalchemy as sa
|
12
|
+
import sqlmodel
|
13
|
+
from alembic import op
|
14
|
+
|
15
|
+
# revision identifiers, used by Alembic.
|
16
|
+
revision = "3dcc5d20e82f"
|
17
|
+
down_revision = "026d4577b6a0"
|
18
|
+
branch_labels = None
|
19
|
+
depends_on = None
|
20
|
+
|
21
|
+
|
22
|
+
def upgrade() -> None:
|
23
|
+
"""Upgrade database schema and/or data, creating a new revision."""
|
24
|
+
bind = op.get_bind()
|
25
|
+
session = sqlmodel.Session(bind=bind)
|
26
|
+
|
27
|
+
with op.batch_alter_table("server_settings", schema=None) as batch_op:
|
28
|
+
batch_op.add_column(
|
29
|
+
sa.Column("last_user_activity", sa.DateTime(), nullable=True)
|
30
|
+
)
|
31
|
+
|
32
|
+
session.execute(
|
33
|
+
sa.text(
|
34
|
+
"""
|
35
|
+
UPDATE server_settings
|
36
|
+
SET last_user_activity = :last_user_activity
|
37
|
+
"""
|
38
|
+
),
|
39
|
+
params=(dict(last_user_activity=datetime.now(timezone.utc))),
|
40
|
+
)
|
41
|
+
|
42
|
+
with op.batch_alter_table("server_settings", schema=None) as batch_op:
|
43
|
+
batch_op.alter_column(
|
44
|
+
"last_user_activity", existing_type=sa.DateTime(), nullable=False
|
45
|
+
)
|
46
|
+
|
47
|
+
|
48
|
+
def downgrade() -> None:
|
49
|
+
"""Downgrade database schema and/or data back to the previous revision."""
|
50
|
+
with op.batch_alter_table("server_settings", schema=None) as batch_op:
|
51
|
+
batch_op.drop_column("last_user_activity")
|
@@ -0,0 +1,36 @@
|
|
1
|
+
"""Remove user hub token [909550c7c4da].
|
2
|
+
|
3
|
+
Revision ID: 909550c7c4da
|
4
|
+
Revises: 0.63.0
|
5
|
+
Create Date: 2024-08-05 16:02:48.990897
|
6
|
+
|
7
|
+
"""
|
8
|
+
|
9
|
+
import sqlalchemy as sa
|
10
|
+
from alembic import op
|
11
|
+
|
12
|
+
# revision identifiers, used by Alembic.
|
13
|
+
revision = "909550c7c4da"
|
14
|
+
down_revision = "0.63.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
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
22
|
+
with op.batch_alter_table("user", schema=None) as batch_op:
|
23
|
+
batch_op.drop_column("hub_token")
|
24
|
+
|
25
|
+
# ### end Alembic commands ###
|
26
|
+
|
27
|
+
|
28
|
+
def downgrade() -> None:
|
29
|
+
"""Downgrade database schema and/or data back to the previous revision."""
|
30
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
31
|
+
with op.batch_alter_table("user", schema=None) as batch_op:
|
32
|
+
batch_op.add_column(
|
33
|
+
sa.Column("hub_token", sa.VARCHAR(), nullable=True)
|
34
|
+
)
|
35
|
+
|
36
|
+
# ### end Alembic commands ###
|
@@ -4295,7 +4295,7 @@ class RestZenStore(BaseZenStore):
|
|
4295
4295
|
return self._request(
|
4296
4296
|
"POST",
|
4297
4297
|
self.url + API + VERSION_1 + path,
|
4298
|
-
|
4298
|
+
json=body.model_dump(mode="json"),
|
4299
4299
|
params=params,
|
4300
4300
|
timeout=timeout,
|
4301
4301
|
**kwargs,
|
@@ -4322,11 +4322,13 @@ class RestZenStore(BaseZenStore):
|
|
4322
4322
|
The response body.
|
4323
4323
|
"""
|
4324
4324
|
logger.debug(f"Sending PUT request to {path}...")
|
4325
|
-
|
4325
|
+
json = (
|
4326
|
+
body.model_dump(mode="json", exclude_unset=True) if body else None
|
4327
|
+
)
|
4326
4328
|
return self._request(
|
4327
4329
|
"PUT",
|
4328
4330
|
self.url + API + VERSION_1 + path,
|
4329
|
-
|
4331
|
+
json=json,
|
4330
4332
|
params=params,
|
4331
4333
|
timeout=timeout,
|
4332
4334
|
**kwargs,
|
@@ -84,6 +84,7 @@ class PipelineDeploymentSchema(BaseSchema, table=True):
|
|
84
84
|
nullable=True,
|
85
85
|
)
|
86
86
|
)
|
87
|
+
code_path: Optional[str] = Field(nullable=True)
|
87
88
|
|
88
89
|
# Foreign keys
|
89
90
|
user_id: Optional[UUID] = build_foreign_key_field(
|
@@ -207,6 +208,7 @@ class PipelineDeploymentSchema(BaseSchema, table=True):
|
|
207
208
|
)
|
208
209
|
if request.pipeline_spec
|
209
210
|
else None,
|
211
|
+
code_path=request.code_path,
|
210
212
|
)
|
211
213
|
|
212
214
|
def to_model(
|
@@ -261,6 +263,7 @@ class PipelineDeploymentSchema(BaseSchema, table=True):
|
|
261
263
|
)
|
262
264
|
if self.pipeline_spec
|
263
265
|
else None,
|
266
|
+
code_path=self.code_path,
|
264
267
|
template_id=self.template_id,
|
265
268
|
)
|
266
269
|
return PipelineDeploymentResponse(
|
@@ -322,6 +322,9 @@ class PipelineRunSchema(NamedSchema, table=True):
|
|
322
322
|
client_environment=client_environment,
|
323
323
|
orchestrator_environment=orchestrator_environment,
|
324
324
|
orchestrator_run_id=self.orchestrator_run_id,
|
325
|
+
code_path=self.deployment.code_path
|
326
|
+
if self.deployment
|
327
|
+
else None,
|
325
328
|
template_id=self.deployment.template_id
|
326
329
|
if self.deployment
|
327
330
|
else None,
|
@@ -42,6 +42,7 @@ class ServerSettingsSchema(SQLModel, table=True):
|
|
42
42
|
display_announcements: Optional[bool] = Field(nullable=True)
|
43
43
|
display_updates: Optional[bool] = Field(nullable=True)
|
44
44
|
onboarding_state: Optional[str] = Field(nullable=True)
|
45
|
+
last_user_activity: datetime = Field(default_factory=datetime.utcnow)
|
45
46
|
updated: datetime = Field(default_factory=datetime.utcnow)
|
46
47
|
|
47
48
|
def update(
|
@@ -111,6 +112,7 @@ class ServerSettingsSchema(SQLModel, table=True):
|
|
111
112
|
display_updates=self.display_updates,
|
112
113
|
active=self.active,
|
113
114
|
updated=self.updated,
|
115
|
+
last_user_activity=self.last_user_activity,
|
114
116
|
)
|
115
117
|
|
116
118
|
metadata = None
|
@@ -77,7 +77,6 @@ class UserSchema(NamedSchema, table=True):
|
|
77
77
|
active: bool
|
78
78
|
password: Optional[str] = Field(nullable=True)
|
79
79
|
activation_token: Optional[str] = Field(nullable=True)
|
80
|
-
hub_token: Optional[str] = Field(nullable=True)
|
81
80
|
email_opted_in: Optional[bool] = Field(nullable=True)
|
82
81
|
external_user_id: Optional[UUID] = Field(nullable=True)
|
83
82
|
is_admin: bool = Field(default=False)
|
@@ -281,7 +280,6 @@ class UserSchema(NamedSchema, table=True):
|
|
281
280
|
if include_metadata:
|
282
281
|
metadata = UserResponseMetadata(
|
283
282
|
email=self.email if include_private else None,
|
284
|
-
hub_token=self.hub_token if include_private else None,
|
285
283
|
external_user_id=self.external_user_id,
|
286
284
|
user_metadata=json.loads(self.user_metadata)
|
287
285
|
if self.user_metadata
|
@@ -20,7 +20,7 @@ import math
|
|
20
20
|
import os
|
21
21
|
import re
|
22
22
|
import sys
|
23
|
-
from datetime import datetime
|
23
|
+
from datetime import datetime, timezone
|
24
24
|
from functools import lru_cache
|
25
25
|
from pathlib import Path
|
26
26
|
from typing import (
|
@@ -1587,6 +1587,7 @@ class SqlZenStore(BaseZenStore):
|
|
1587
1587
|
# the one fetched from the global configuration
|
1588
1588
|
model.id = settings.server_id
|
1589
1589
|
model.active = settings.active
|
1590
|
+
model.last_user_activity = settings.last_user_activity
|
1590
1591
|
if not handle_bool_env_var(ENV_ZENML_LOCAL_SERVER):
|
1591
1592
|
model.analytics_enabled = settings.enable_analytics
|
1592
1593
|
return model
|
@@ -1689,6 +1690,29 @@ class SqlZenStore(BaseZenStore):
|
|
1689
1690
|
|
1690
1691
|
return settings.to_model(include_metadata=True)
|
1691
1692
|
|
1693
|
+
def _update_last_user_activity_timestamp(
|
1694
|
+
self, last_user_activity: datetime
|
1695
|
+
) -> None:
|
1696
|
+
"""Update the last user activity timestamp.
|
1697
|
+
|
1698
|
+
Args:
|
1699
|
+
last_user_activity: The timestamp of latest user activity
|
1700
|
+
traced by server instance.
|
1701
|
+
"""
|
1702
|
+
with Session(self.engine) as session:
|
1703
|
+
settings = self._get_server_settings(session=session)
|
1704
|
+
|
1705
|
+
if last_user_activity < settings.last_user_activity.replace(
|
1706
|
+
tzinfo=timezone.utc
|
1707
|
+
):
|
1708
|
+
return
|
1709
|
+
|
1710
|
+
settings.last_user_activity = last_user_activity
|
1711
|
+
# `updated` kept intentionally unchanged here
|
1712
|
+
session.add(settings)
|
1713
|
+
session.commit()
|
1714
|
+
session.refresh(settings)
|
1715
|
+
|
1692
1716
|
def get_onboarding_state(self) -> List[str]:
|
1693
1717
|
"""Get the server onboarding state.
|
1694
1718
|
|
{zenml_nightly-0.63.0.dev20240801.dist-info → zenml_nightly-0.64.0.dev20240811.dist-info}/METADATA
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: zenml-nightly
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.64.0.dev20240811
|
4
4
|
Summary: ZenML: Write production-ready ML code.
|
5
5
|
Home-page: https://zenml.io
|
6
6
|
License: Apache-2.0
|
@@ -140,7 +140,7 @@ Description-Content-Type: text/markdown
|
|
140
140
|
|
141
141
|
<div align="center">
|
142
142
|
<img referrerpolicy="no-referrer-when-downgrade" src="https://static.scarf.sh/a.png?x-pxid=0fcbab94-8fbe-4a38-93e8-c2348450a42e" />
|
143
|
-
<h1 align="center">
|
143
|
+
<h1 align="center">Connecting data science teams seamlessly to cloud infrastructure.
|
144
144
|
</h1>
|
145
145
|
</div>
|
146
146
|
|
@@ -467,7 +467,7 @@ the Apache License Version 2.0.
|
|
467
467
|
<a href="https://github.com/zenml-io/zenml-projects">Projects Showcase</a>
|
468
468
|
<br />
|
469
469
|
<br />
|
470
|
-
🎉 Version 0.
|
470
|
+
🎉 Version 0.64.0 is out. Check out the release notes
|
471
471
|
<a href="https://github.com/zenml-io/zenml/releases">here</a>.
|
472
472
|
<br />
|
473
473
|
🖥️ Download our VS Code Extension <a href="https://marketplace.visualstudio.com/items?itemName=ZenML.zenml-vscode">here</a>.
|