Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save ca-scribner/30216b73ab14effb9cb08b8a81bfe5ec to your computer and use it in GitHub Desktop.
Save ca-scribner/30216b73ab14effb9cb08b8a81bfe5ec to your computer and use it in GitHub Desktop.
An example SeldonDeployment using mlflow to store the model
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "markdown",
"id": "ec61465a-45cf-48da-9009-326865abdb86",
"metadata": {},
"source": [
"Modified from [here](https://github.com/Barteus/kubeflow-examples/blob/main/seldon-mlflow-minio/mlflow-demo.ipynb)"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "7a87f02f-acb3-4370-9a8f-5a8791afbc5b",
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"import warnings\n",
"import sys\n",
"\n",
"import pandas as pd\n",
"import numpy as np\n",
"from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score\n",
"from sklearn.model_selection import train_test_split\n",
"from sklearn.linear_model import ElasticNet\n",
"from urllib.parse import urlparse\n",
"import mlflow\n",
"import mlflow.sklearn\n",
"\n",
"import logging\n",
"\n",
"logging.basicConfig(level=logging.WARN)\n",
"logger = logging.getLogger(__name__)\n",
"\n",
"def eval_metrics(actual, pred):\n",
" rmse = np.sqrt(mean_squared_error(actual, pred))\n",
" mae = mean_absolute_error(actual, pred)\n",
" r2 = r2_score(actual, pred)\n",
" return rmse, mae, r2\n"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "65ced82d-7ec8-4e6d-8917-a6a7cb6dd325",
"metadata": {},
"outputs": [],
"source": [
"from IPython.core.magic import register_line_cell_magic\n",
"\n",
"@register_line_cell_magic\n",
"def writetemplate(line, cell):\n",
" with open(line, \"w\") as f:\n",
" f.write(cell.format(**globals()))"
]
},
{
"cell_type": "markdown",
"id": "f605157f-1480-45ee-96b9-1f815f1a73a2",
"metadata": {},
"source": [
"# Create a model"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "2d24e043-084f-4186-9e37-d6e11f85d636",
"metadata": {},
"outputs": [],
"source": [
"warnings.filterwarnings(\"ignore\")\n",
"np.random.seed(40)\n",
"\n",
"# Read the wine-quality csv file from the URL\n",
"csv_url = (\n",
" \"http://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-red.csv\"\n",
")\n",
"try:\n",
" data = pd.read_csv(csv_url, sep=\";\")\n",
"except Exception as e:\n",
" logger.exception(\n",
" \"Unable to download training & test CSV, check your internet connection. Error: %s\", e\n",
" )\n",
"\n",
"# Split the data into training and test sets. (0.75, 0.25) split.\n",
"train, test = train_test_split(data)\n",
"\n",
"# The predicted column is \"quality\" which is a scalar from [3, 9]\n",
"train_x = train.drop([\"quality\"], axis=1)\n",
"test_x = test.drop([\"quality\"], axis=1)\n",
"train_y = train[[\"quality\"]]\n",
"test_y = test[[\"quality\"]]\n",
"\n",
"alpha = 0.5\n",
"l1_ratio = 0.5"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "1955a5d8-c30b-410d-8f9b-c7e8ce00a47d",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Elasticnet model (alpha=0.500000, l1_ratio=0.500000):\n",
" RMSE: 0.793164022927685\n",
" MAE: 0.6271946374319586\n",
" R2: 0.10862644997792636\n"
]
}
],
"source": [
"lr = ElasticNet(alpha=alpha, l1_ratio=l1_ratio, random_state=42)\n",
"lr.fit(train_x, train_y)\n",
"\n",
"predicted_qualities = lr.predict(test_x)\n",
"\n",
"(rmse, mae, r2) = eval_metrics(test_y, predicted_qualities)\n",
"\n",
"print(\"Elasticnet model (alpha=%f, l1_ratio=%f):\" % (alpha, l1_ratio))\n",
"print(\" RMSE: %s\" % rmse)\n",
"print(\" MAE: %s\" % mae)\n",
"print(\" R2: %s\" % r2)\n"
]
},
{
"cell_type": "markdown",
"id": "4b4c0384-1dd5-4536-b2a2-6b5a029feb82",
"metadata": {},
"source": [
"# Do an example prediction"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "ccc7c314-e352-42f4-99da-0a5f6bb6331c",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[[ 0 1 2 3 4 5 6 7 8 9 10]]\n"
]
}
],
"source": [
"dummy_data = np.array(range(11))[np.newaxis,:]\n",
"print(dummy_data)"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "b67cb495-d2e0-4c18-99b8-da44960cf3fc",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"array([5.73970836])"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"lr.predict(dummy_data)"
]
},
{
"cell_type": "markdown",
"id": "40a5fda4-82ca-44b9-bd53-a0598af538ed",
"metadata": {},
"source": [
"# Store parameters and model in mlflow"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "129d336b-b800-4bc0-aeef-ecc41aafacb6",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"mlflow_tracking_uri='http://10.152.183.193:5000'\n",
"mlflow_s3_endpoint_url='http://10.152.183.66:9000'\n",
"aws_access_key_id='minio'\n",
"aws_secret_access_key='X9KGX9W9C7GVTKPV4T1MMTP4V9ZT60'\n"
]
}
],
"source": [
"mlflow_ip = !kubectl -n kubeflow get svc mlflow-server -o jsonpath='{.spec.clusterIP}'\n",
"mlflow_tracking_uri = f\"http://{mlflow_ip[0]}:5000\"\n",
"minio_ip = !kubectl -n kubeflow get svc minio -o jsonpath='{.spec.clusterIP}'\n",
"mlflow_s3_endpoint_url = f\"http://{minio_ip[0]}:9000\"\n",
"aws_access_key_id = \"minio\"\n",
"# Could get this from `kubectl get secret mlflow-server-seldon-init-container-s3-credentials -o jsonpath='{.data.RCLONE_CONFIG_S3_SECRET_ACCESS_KEY}' | base64 -d`\n",
"aws_secret_access_key = \"X9KGX9W9C7GVTKPV4T1MMTP4V9ZT60\"\n",
"\n",
"print(f\"{mlflow_tracking_uri=}\")\n",
"print(f\"{mlflow_s3_endpoint_url=}\")\n",
"print(f\"{aws_access_key_id=}\")\n",
"print(f\"{aws_secret_access_key=}\")"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "871f9626-a4bf-4d0d-87f4-db330b166d25",
"metadata": {},
"outputs": [],
"source": [
"# From mlflow-server service\n",
"os.environ['MLFLOW_TRACKING_URI'] = mlflow_tracking_uri\n",
"# from minio service\n",
"os.environ[\"MLFLOW_S3_ENDPOINT_URL\"] = mlflow_s3_endpoint_url\n",
"os.environ[\"AWS_ACCESS_KEY_ID\"] = aws_access_key_id\n",
"os.environ[\"AWS_SECRET_ACCESS_KEY\"] = aws_secret_access_key"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "38309cf2-1cea-4095-966f-2d45777cc405",
"metadata": {},
"outputs": [],
"source": [
"# Note: doing this directly without specifying a run will implicitly create a run (in the default experiment?)\n",
"# and set that run as the mlflow.active_run()\n",
"# Could also do this inside a `with mlflow.start_run(run_name='test_run'):` block\n",
"mlflow.log_param(\"alpha\", alpha)\n",
"mlflow.log_param(\"l1_ratio\", l1_ratio)\n",
"mlflow.log_metric(\"rmse\", rmse)\n",
"mlflow.log_metric(\"r2\", r2)\n",
"mlflow.log_metric(\"mae\", mae)\n"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "71f66dd3-4568-4068-b44f-d4e4ddab726e",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Bucket mlflow already exists - using existing bucket\n"
]
}
],
"source": [
"# Patch to create bucket in minio if it doesn't exist already\n",
"import boto3\n",
"import botocore\n",
"import os\n",
"\n",
"def create_default_bucket(default_bucket):\n",
" \"\"\"Checks whether default_bucket exists and is accessible, creating it if it does not\"\"\"\n",
" c = boto3.client(\n",
" 's3',\n",
" endpoint_url=os.environ[\"MLFLOW_S3_ENDPOINT_URL\"],\n",
" aws_access_key_id=os.environ[\"AWS_ACCESS_KEY_ID\"],\n",
" aws_secret_access_key=os.environ[\"AWS_SECRET_ACCESS_KEY\"],\n",
" )\n",
" try:\n",
" # If this succeeded, bucket exists already and we have access to it\n",
" c.head_bucket(Bucket=default_bucket)\n",
" print(f\"Bucket {default_bucket} already exists - using existing bucket\")\n",
" except botocore.exceptions.ClientError:\n",
" # Otherwise bucket is missing or do not have access. Try to create it\n",
" try:\n",
" print(f\"Creating bucket {default_bucket}\")\n",
" c.create_bucket(Bucket=default_bucket)\n",
" except botocore.exceptions.ClientError as e:\n",
" # Cannot create bucket. Block charm\n",
" message = \"Could not access/create the default artifact storage bucket.\" \\\n",
" f\" Got error: {str(e)}\"\n",
" raise ValueError(message)\n",
"\n",
"create_default_bucket(\"mlflow\")"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "76f9722f-1cf9-4da4-9e05-be72b70b6eb8",
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"Registered model 'ElasticnetWineModel' already exists. Creating a new version of this model...\n",
"2022/03/04 15:58:21 INFO mlflow.tracking._model_registry.client: Waiting up to 300 seconds for model version to finish creation. Model name: ElasticnetWineModel, version 2\n",
"Created version '2' of model 'ElasticnetWineModel'.\n"
]
},
{
"data": {
"text/plain": [
"ModelInfo(artifact_path='model', flavors={'python_function': {'model_path': 'model.pkl', 'loader_module': 'mlflow.sklearn', 'python_version': '3.9.7', 'env': 'conda.yaml'}, 'sklearn': {'pickled_model': 'model.pkl', 'sklearn_version': '1.0.2', 'serialization_format': 'cloudpickle'}}, model_uri='runs:/7ea073ed2bc643e7bb5f593e53269d4d/model', model_uuid='5b89ca5b8b554f7c8b0fdd07e3c80d90', run_id='7ea073ed2bc643e7bb5f593e53269d4d', saved_input_example_info=None, signature_dict=None, utc_time_created='2022-03-04 20:58:19.053647')"
]
},
"execution_count": 12,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"mlflow.sklearn.log_model(lr, \"model\", registered_model_name=\"ElasticnetWineModel\", )"
]
},
{
"cell_type": "markdown",
"id": "b1bfb394-2d87-4ff7-abbc-dbcabf44568c",
"metadata": {},
"source": [
"# If needed, create the secrets (if you're using an old mlflow charm, this wont be created for you)"
]
},
{
"cell_type": "code",
"execution_count": 27,
"id": "023bf506-4e93-4e22-af34-5cc82087ab66",
"metadata": {},
"outputs": [],
"source": [
"namespace_for_seldondeployment = \"admin\""
]
},
{
"cell_type": "code",
"execution_count": 55,
"id": "7fd84793-fefc-49b8-a53a-389683cfa9d1",
"metadata": {},
"outputs": [],
"source": [
"from base64 import b64encode\n",
"\n",
"def _b64_encode_dict(d):\n",
" \"\"\"Returns the dict with values being base64 encoded.\"\"\"\n",
" # Why do we encode and decode in utf-8 first?\n",
" return {k: b64encode(v.encode(\"utf-8\")).decode(\"utf-8\") for k, v in d.items()}\n",
"\n",
"def _seldon_credentials_dict(obj_storage):\n",
" \"\"\"Returns a dict of seldon init-container object storage credentials, base64 encoded.\"\"\"\n",
" credentials = {\n",
" \"RCLONE_CONFIG_S3_TYPE\": \"s3\",\n",
" \"RCLONE_CONFIG_S3_PROVIDER\": \"minio\",\n",
" \"RCLONE_CONFIG_S3_ACCESS_KEY_ID\": obj_storage[\"access-key\"],\n",
" \"RCLONE_CONFIG_S3_SECRET_ACCESS_KEY\": obj_storage[\"secret-key\"],\n",
" \"RCLONE_CONFIG_S3_ENDPOINT\": f\"http://{obj_storage['service']}.{obj_storage['namespace']}:{obj_storage['port']}\",\n",
" \"RCLONE_CONFIG_S3_ENV_AUTH\": \"false\",\n",
" }\n",
" return _b64_encode_dict(credentials)\n",
"\n",
"obj_storage = {\n",
" \"access-key\": \"minio\",\n",
" \"secret-key\": \"X9KGX9W9C7GVTKPV4T1MMTP4V9ZT60\",\n",
" \"service\": \"minio\",\n",
" \"namespace\": \"kubeflow\",\n",
" \"port\": \"9000\",\n",
"}\n",
"\n",
"secret_data = _seldon_credentials_dict(obj_storage)"
]
},
{
"cell_type": "code",
"execution_count": 56,
"id": "a8f36ff3-4801-43c2-a8c9-68d9fa4bbe50",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'RCLONE_CONFIG_S3_TYPE': 'czM=',\n",
" 'RCLONE_CONFIG_S3_PROVIDER': 'bWluaW8=',\n",
" 'RCLONE_CONFIG_S3_ACCESS_KEY_ID': 'bWluaW8=',\n",
" 'RCLONE_CONFIG_S3_SECRET_ACCESS_KEY': 'WDlLR1g5VzlDN0dWVEtQVjRUMU1NVFA0VjlaVDYw',\n",
" 'RCLONE_CONFIG_S3_ENDPOINT': 'aHR0cDovL21pbmlvLmt1YmVmbG93OjkwMDA=',\n",
" 'RCLONE_CONFIG_S3_ENV_AUTH': 'ZmFsc2U='}"
]
},
"execution_count": 56,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"secret_data"
]
},
{
"cell_type": "code",
"execution_count": 57,
"id": "12f38d55-eafa-409f-86bd-9c51ef0533c8",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"' RCLONE_CONFIG_S3_TYPE: czM=\\n RCLONE_CONFIG_S3_PROVIDER: bWluaW8=\\n RCLONE_CONFIG_S3_ACCESS_KEY_ID: bWluaW8=\\n RCLONE_CONFIG_S3_SECRET_ACCESS_KEY: WDlLR1g5VzlDN0dWVEtQVjRUMU1NVFA0VjlaVDYw\\n RCLONE_CONFIG_S3_ENDPOINT: aHR0cDovL21pbmlvLmt1YmVmbG93OjkwMDA=\\n RCLONE_CONFIG_S3_ENV_AUTH: ZmFsc2U='"
]
},
"execution_count": 57,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"serialized_secret_data = \"\\n\".join((f\" {k}: {v}\" for k, v in secret_data.items()))\n",
"serialized_secret_data"
]
},
{
"cell_type": "code",
"execution_count": 58,
"id": "0192e7e5-1bf6-4c3d-8b77-64fdd8f76894",
"metadata": {},
"outputs": [],
"source": [
"%%writetemplate seldon_secret.yaml\n",
"apiVersion: v1\n",
"kind: Secret\n",
"metadata:\n",
" name: mlflow-server-seldon-init-container-s3-credentials\n",
"type: bootstrap.kubernetes.io/token\n",
"data:\n",
"{serialized_secret_data}"
]
},
{
"cell_type": "code",
"execution_count": 59,
"id": "c7d59bf6-0b6e-46c8-b88b-8ee3a5d32ad2",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"secret/mlflow-server-seldon-init-container-s3-credentials configured\n"
]
}
],
"source": [
"!kubectl apply -f seldon_secret.yaml -n $namespace_for_seldondeployment"
]
},
{
"cell_type": "markdown",
"id": "2dc185ae-3b10-462a-84e7-feeac70d614c",
"metadata": {
"tags": []
},
"source": [
"# Deploy the model using Seldon"
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "80f9a2d4-fccb-4023-8a8a-a044b07574e5",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"artifact_uri='s3://mlflow/0/7ea073ed2bc643e7bb5f593e53269d4d/artifacts'\n"
]
}
],
"source": [
"run = mlflow.active_run()\n",
"artifact_uri = run.info.artifact_uri\n",
"print(f\"{artifact_uri=}\")"
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "f5f5c5fb-cfaa-4395-adb0-b6a165c7f58b",
"metadata": {},
"outputs": [],
"source": [
"%%writetemplate sd_example.yaml\n",
"\n",
"apiVersion: machinelearning.seldon.io/v1alpha2\n",
"kind: SeldonDeployment\n",
"metadata:\n",
" name: mlflow\n",
"spec:\n",
" name: wines\n",
" predictors:\n",
" - componentSpecs:\n",
" - spec:\n",
" # We are setting high failureThreshold as installing conda dependencies\n",
" # can take long time and we want to avoid k8s killing the container prematurely\n",
" containers:\n",
" - name: classifier\n",
" image: seldonio/mlflowserver:1.14.0-dev # Patches a package conflict in the default version\n",
" livenessProbe:\n",
" initialDelaySeconds: 80\n",
" failureThreshold: 200\n",
" periodSeconds: 5\n",
" successThreshold: 1\n",
" httpGet:\n",
" path: /health/ping\n",
" port: http\n",
" scheme: HTTP\n",
" readinessProbe:\n",
" initialDelaySeconds: 80\n",
" failureThreshold: 200\n",
" periodSeconds: 5\n",
" successThreshold: 1\n",
" httpGet:\n",
" path: /health/ping\n",
" port: http\n",
" scheme: HTTP\n",
" graph:\n",
" children: []\n",
" implementation: MLFLOW_SERVER\n",
" # mlflow/experimentid/runid/artifacts/model\n",
" modelUri: {artifact_uri}/model\n",
" envSecretRefName: mlflow-server-seldon-init-container-s3-credentials\n",
" name: classifier\n",
" name: default\n",
" replicas: 1\n"
]
},
{
"cell_type": "code",
"execution_count": 15,
"id": "14ac443c-45c3-4807-9179-9956a7842ea3",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"seldondeployment.machinelearning.seldon.io/mlflow created\n"
]
}
],
"source": [
"!kubectl create -f sd_example.yaml"
]
},
{
"cell_type": "code",
"execution_count": 16,
"id": "8fabd68e-c2d1-4e1a-853f-a3817fa8b62e",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Waiting for deployment \"mlflow-default-0-classifier\" rollout to finish: 0 of 1 updated replicas are available...\n",
"deployment \"mlflow-default-0-classifier\" successfully rolled out\n"
]
}
],
"source": [
"jsonpath=\"'{.items[0].metadata.name}'\"\n",
"deployment_name = !kubectl get deploy -l seldon-deployment-id=mlflow -o jsonpath=$jsonpath\n",
"deployment_name = deployment_name[0]\n",
"!kubectl rollout status deploy/$deployment_name\n"
]
},
{
"cell_type": "markdown",
"id": "df3df1a7-0330-481b-8cd7-651c293648c9",
"metadata": {},
"source": [
"# Predict with the deployment"
]
},
{
"cell_type": "markdown",
"id": "a557a7bc-4a73-4455-a6cd-70033ea3277a",
"metadata": {},
"source": [
"### Create some dummy data"
]
},
{
"cell_type": "code",
"execution_count": 17,
"id": "a44bd73e-ac5c-48db-a975-6dc17a009cce",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"data_escaped='\\\\{\\\\\"data\\\\\":\\\\ \\\\{\\\\\"ndarray\\\\\":\\\\ \\\\[\\\\[0,\\\\ 1,\\\\ 2,\\\\ 3,\\\\ 4,\\\\ 5,\\\\ 6,\\\\ 7,\\\\ 8,\\\\ 9,\\\\ 10\\\\]\\\\]\\\\}\\\\}'\n"
]
}
],
"source": [
"import json\n",
"# data_json = json.dumps(dummy_data.tolist())\n",
"data_json = json.dumps({\"data\": {\"ndarray\": dummy_data.tolist()}})\n",
"\n",
"# There must be a better way, but I couldn't figure out a better way to get data into the curl statement with proper escaping\n",
"import re\n",
"data_escaped = re.escape(data_json)\n",
"data_escaped = data_escaped.replace('\"', '\\\\\"')\n",
"print(f\"{data_escaped=}\")"
]
},
{
"cell_type": "code",
"execution_count": 18,
"id": "618a074c-7d45-4a02-9c1b-31ffd209d71f",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"seldondeployment_predict_endpoint='http://10.152.183.135:9000/predict'\n"
]
}
],
"source": [
"# # SeldonDeployment Service name\n",
"# !kubectl get svc -l seldon-deployment-id=mlflow,seldon.io/model=true -o jsonpath='{.items[0].metadata.name}'\n",
"\n",
"# SeldonDeployment Service IP\n",
"seldondeployment_predict_ip = !kubectl get svc -l seldon-deployment-id=mlflow,seldon.io/model=true -o jsonpath='{.items[0].spec.clusterIP}'\n",
"seldondeployment_predict_endpoint = f\"http://{seldondeployment_predict_ip[0]}:9000/predict\"\n",
"print(f\"{seldondeployment_predict_endpoint=}\")"
]
},
{
"cell_type": "code",
"execution_count": 19,
"id": "ae931bfa-a25e-46af-b9ad-0cbee699d499",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{\"data\":{\"names\":[],\"ndarray\":[5.739708362528547]},\"meta\":{\"requestPath\":{\"classifier\":\"seldonio/mlflowserver:1.14.0-dev\"}}}\n"
]
}
],
"source": [
"!curl $seldondeployment_predict_endpoint -X POST -H 'Content-Type: application/json' -d $data_escaped"
]
},
{
"cell_type": "markdown",
"id": "dff40e90-44cf-422e-b33c-bcedd0823443",
"metadata": {},
"source": [
"Where we see the ndarray returned is the same as our original prediction above"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "11100d52-eec6-46a9-8f0f-c627c6e55617",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "mlflow-secrets-venv2",
"language": "python",
"name": "mlflow-secrets-venv2"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.7"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment