Repository: SeldonIO/tempo (language: python)

Issue:
Save class definition (instead of instance)
For decorated classes, it's interesting in some cases to store the class definition instead of the class instance. This means that instead of doing,

```python
myc = MyClass()
myc.save()
``` 

One could do

```python
MyClass.save()
```

This in turn would require the MLServer runtime to call `MyClass()` when it loads the artifact from the remote URI.


Discussion / hints:

Fix patch:
diff --git a/docs/examples/explainer/README.ipynb b/docs/examples/explainer/README.ipynb
index 35d20d66..3b4679d3 100644
--- a/docs/examples/explainer/README.ipynb
+++ b/docs/examples/explainer/README.ipynb
@@ -45,7 +45,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 1,
+   "execution_count": 10,
    "id": "b8ca70f9",
    "metadata": {
     "scrolled": true
@@ -58,6 +58,7 @@
       "\u001b[01;34m.\u001b[00m\r\n",
       "├── \u001b[01;34martifacts\u001b[00m\r\n",
       "│   ├── \u001b[01;34mexplainer\u001b[00m\r\n",
+      "│   ├── \u001b[01;34mincome_explainer\u001b[00m\r\n",
       "│   └── \u001b[01;34mmodel\u001b[00m\r\n",
       "├── \u001b[01;34mk8s\u001b[00m\r\n",
       "│   └── \u001b[01;34mrbac\u001b[00m\r\n",
@@ -68,7 +69,7 @@
       "    ├── model.py\r\n",
       "    └── tempo.py\r\n",
       "\r\n",
-      "6 directories, 5 files\r\n"
+      "7 directories, 5 files\r\n"
      ]
     }
    ],
@@ -89,35 +90,40 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 2,
+   "execution_count": 11,
    "id": "42c20ffa",
    "metadata": {},
    "outputs": [],
    "source": [
     "import os\n",
-    "from tempo.utils import logger\n",
     "import logging\n",
     "import numpy as np\n",
     "import json\n",
+    "import tempo\n",
+    "\n",
+    "from tempo.utils import logger\n",
+    "\n",
+    "from src.constants import ARTIFACTS_FOLDER\n",
+    "\n",
     "logger.setLevel(logging.ERROR)\n",
-    "logging.basicConfig(level=logging.ERROR)\n",
-    "ARTIFACTS_FOLDER = os.getcwd()+\"/artifacts\""
+    "logging.basicConfig(level=logging.ERROR)"
    ]
   },
   {
    "cell_type": "code",
-   "execution_count": 3,
+   "execution_count": 12,
    "id": "aa35a350",
    "metadata": {},
    "outputs": [],
    "source": [
     "from src.data import AdultData\n",
+    "\n",
     "data = AdultData()"
    ]
   },
   {
    "cell_type": "code",
-   "execution_count": 4,
+   "execution_count": 13,
    "id": "9bc17ab0",
    "metadata": {},
    "outputs": [
@@ -132,12 +138,13 @@
    ],
    "source": [
     "from src.model import train_model\n",
+    "\n",
     "adult_model = train_model(ARTIFACTS_FOLDER, data)"
    ]
   },
   {
    "cell_type": "code",
-   "execution_count": 5,
+   "execution_count": 14,
    "id": "7afce019",
    "metadata": {},
    "outputs": [
@@ -152,13 +159,14 @@
        ")"
       ]
      },
-     "execution_count": 5,
+     "execution_count": 14,
      "metadata": {},
      "output_type": "execute_result"
     }
    ],
    "source": [
     "from src.explainer import train_explainer\n",
+    "\n",
     "train_explainer(ARTIFACTS_FOLDER, data, adult_model)"
    ]
   },
@@ -172,12 +180,13 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 6,
+   "execution_count": 15,
    "id": "a8345fb6",
    "metadata": {},
    "outputs": [],
    "source": [
     "from src.tempo import create_tempo_artifacts\n",
+    "\n",
     "adult_model, explainer = create_tempo_artifacts(ARTIFACTS_FOLDER)"
    ]
   },
@@ -194,42 +203,42 @@
    "source": [
     "# %load src/tempo.py\n",
     "import os\n",
-    "from typing import Any, Tuple\n",
-    "\n",
     "import dill\n",
     "import numpy as np\n",
+    "\n",
+    "from typing import Any, Tuple\n",
     "from alibi.utils.wrappers import ArgmaxTransformer\n",
-    "from src.constants import EXPLAINER_FOLDER, MODEL_FOLDER\n",
     "\n",
     "from tempo.serve.metadata import ModelFramework\n",
     "from tempo.serve.model import Model\n",
     "from tempo.serve.pipeline import PipelineModels\n",
     "from tempo.serve.utils import pipeline, predictmethod\n",
     "\n",
+    "from src.constants import ARTIFACTS_FOLDER, EXPLAINER_FOLDER, MODEL_FOLDER\n",
+    "\n",
     "\n",
     "def create_tempo_artifacts(artifacts_folder: str) -> Tuple[Model, Any]:\n",
     "    sklearn_model = Model(\n",
     "        name=\"income-sklearn\",\n",
     "        platform=ModelFramework.SKLearn,\n",
-    "        local_folder=f\"{artifacts_folder}/{MODEL_FOLDER}\",\n",
+    "        local_folder=os.path.join(ARTIFACTS_FOLDER, MODEL_FOLDER),\n",
     "        uri=\"gs://seldon-models/test/income/model\",\n",
     "    )\n",
     "\n",
     "    @pipeline(\n",
     "        name=\"income-explainer\",\n",
     "        uri=\"s3://tempo/explainer/pipeline\",\n",
-    "        local_folder=f\"{artifacts_folder}/{EXPLAINER_FOLDER}\",\n",
+    "        local_folder=os.path.join(ARTIFACTS_FOLDER, EXPLAINER_FOLDER),\n",
     "        models=PipelineModels(sklearn=sklearn_model),\n",
     "    )\n",
     "    class ExplainerPipeline(object):\n",
     "        def __init__(self):\n",
-    "            if \"MLSERVER_MODELS_DIR\" in os.environ:\n",
-    "                models_folder = \"\"\n",
-    "            else:\n",
-    "                models_folder = f\"{artifacts_folder}/{EXPLAINER_FOLDER}\"\n",
-    "            with open(models_folder + \"/explainer.dill\", \"rb\") as f:\n",
+    "            pipeline = self.get_tempo()\n",
+    "            models_folder = pipeline.details.local_folder\n",
+    "\n",
+    "            explainer_path = os.path.join(models_folder, \"explainer.dill\")\n",
+    "            with open(explainer_path, \"rb\") as f:\n",
     "                self.explainer = dill.load(f)\n",
-    "            self.ran_init = True\n",
     "\n",
     "        def update_predict_fn(self, x):\n",
     "            if np.argmax(self.models.sklearn(x).shape) == 0:\n",
@@ -237,14 +246,13 @@
     "                self.explainer.samplers[0].predictor = self.models.sklearn\n",
     "            else:\n",
     "                self.explainer.predictor = ArgmaxTransformer(self.models.sklearn)\n",
-    "                self.explainer.samplers[0].predictor = ArgmaxTransformer(self.models.sklearn)\n",
+    "                self.explainer.samplers[0].predictor = ArgmaxTransformer(\n",
+    "                    self.models.sklearn\n",
+    "                )\n",
     "\n",
     "        @predictmethod\n",
     "        def explain(self, payload: np.ndarray, parameters: dict) -> str:\n",
     "            print(\"Explain called with \", parameters)\n",
-    "            if not self.ran_init:\n",
-    "                print(\"Loading explainer\")\n",
-    "                self.__init__()\n",
     "            self.update_predict_fn(payload)\n",
     "            explanation = self.explainer.explain(payload, **parameters)\n",
     "            return explanation.to_json()\n",
@@ -258,12 +266,12 @@
    "id": "18dc6a4b",
    "metadata": {},
    "source": [
-    "## Save Outlier and Svc Environments\n"
+    "## Save Explainer\n"
    ]
   },
   {
    "cell_type": "code",
-   "execution_count": 8,
+   "execution_count": 17,
    "id": "3e1f9017",
    "metadata": {},
    "outputs": [
@@ -277,10 +285,10 @@
       "dependencies:\r\n",
       "  - python=3.7.9\r\n",
       "  - pip:\r\n",
-      "    - alibi\r\n",
-      "    - dill\r\n",
-      "    - mlops-tempo @ file:///home/clive/work/mlops/fork-tempo\r\n",
-      "    - mlserver==0.3.1.dev7\r\n"
+      "      - alibi\r\n",
+      "      - dill\r\n",
+      "      - mlops-tempo @ file:///home/agm/Seldon/tempo\r\n",
+      "      - mlserver==0.3.1.dev7\r\n"
      ]
     }
    ],
@@ -290,7 +298,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 9,
+   "execution_count": 18,
    "id": "3c23ab3b",
    "metadata": {},
    "outputs": [
@@ -299,14 +307,13 @@
      "output_type": "stream",
      "text": [
       "Collecting packages...\n",
-      "Packing environment at '/home/clive/anaconda3/envs/tempo-79a239fd-9e5f-4984-ad1a-24bc618b9d4a' to '/home/clive/work/mlops/fork-tempo/docs/examples/explainer/artifacts/explainer/environment.tar.gz'\n",
-      "[########################################] | 100% Completed |  1min  6.6s\n"
+      "Packing environment at '/home/agm/.conda/envs/tempo-e58f8b3d-a52d-4523-9afa-29dab8195823' to '/home/agm/Seldon/tempo/docs/examples/explainer/artifacts/explainer/environment.tar.gz'\n",
+      "[########################################] | 100% Completed | 58.2s\n"
      ]
     }
    ],
    "source": [
-    "from tempo.serve.loader import save\n",
-    "save(explainer)"
+    "tempo.save(explainer)"
    ]
   },
   {
@@ -321,12 +328,13 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 10,
+   "execution_count": 19,
    "id": "a36bfb9f",
    "metadata": {},
    "outputs": [],
    "source": [
-    "from tempo.seldon.docker import SeldonDockerRuntime\n",
+    "from tempo.seldon import SeldonDockerRuntime\n",
+    "\n",
     "docker_runtime = SeldonDockerRuntime()\n",
     "docker_runtime.deploy(explainer)\n",
     "docker_runtime.wait_ready(explainer)"
@@ -334,7 +342,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 11,
+   "execution_count": 20,
    "id": "fb09a516",
    "metadata": {},
    "outputs": [
@@ -354,7 +362,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 13,
+   "execution_count": 21,
    "id": "b22014e7",
    "metadata": {},
    "outputs": [
@@ -362,7 +370,7 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "['Marital Status = Separated', 'Sex = Female', 'Capital Gain <= 0.00', 'Education = Associates', 'Age > 28.00']\n"
+      "['Marital Status = Separated', 'Sex = Female', 'Capital Gain <= 0.00', 'Education = Associates', 'Country = United-States']\n"
      ]
     }
    ],
@@ -373,7 +381,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 14,
+   "execution_count": 22,
    "id": "6c6ea7c7",
    "metadata": {},
    "outputs": [],
@@ -439,9 +447,8 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "from tempo.serve.loader import upload\n",
-    "upload(adult_model)\n",
-    "upload(explainer)"
+    "tempo.upload(adult_model)\n",
+    "tempo.upload(explainer)"
    ]
   },
   {
@@ -452,6 +459,7 @@
    "outputs": [],
    "source": [
     "from tempo.serve.metadata import RuntimeOptions, KubernetesOptions\n",
+    "\n",
     "runtime_options = RuntimeOptions(\n",
     "        k8s_options=KubernetesOptions(\n",
     "            namespace=\"production\",\n",
@@ -468,6 +476,7 @@
    "outputs": [],
    "source": [
     "from tempo.seldon.k8s import SeldonKubernetesRuntime\n",
+    "\n",
     "k8s_runtime = SeldonKubernetesRuntime(runtime_options)\n",
     "k8s_runtime.deploy(explainer)\n",
     "k8s_runtime.wait_ready(explainer)"
@@ -521,8 +530,10 @@
    "outputs": [],
    "source": [
     "from tempo.seldon.k8s import SeldonKubernetesRuntime\n",
+    "\n",
     "k8s_runtime = SeldonKubernetesRuntime(runtime_options)\n",
     "yaml_str = k8s_runtime.to_k8s_yaml(explainer)\n",
+    "\n",
     "with open(os.getcwd()+\"/k8s/tempo.yaml\",\"w\") as f:\n",
     "    f.write(yaml_str)"
    ]
diff --git a/docs/examples/explainer/README.md b/docs/examples/explainer/README.md
index 961ae4c5..b077dbcb 100644
--- a/docs/examples/explainer/README.md
+++ b/docs/examples/explainer/README.md
@@ -36,30 +36,37 @@ conda env create --name tempo-examples --file conda/tempo-examples.yaml
 
 ```python
 import os
-from tempo.utils import logger
 import logging
 import numpy as np
 import json
+import tempo
+
+from tempo.utils import logger
+
+from src.constants import ARTIFACTS_FOLDER
+
 logger.setLevel(logging.ERROR)
 logging.basicConfig(level=logging.ERROR)
-ARTIFACTS_FOLDER = os.getcwd()+"/artifacts"
 ```
 
 
 ```python
 from src.data import AdultData
+
 data = AdultData()
 ```
 
 
 ```python
 from src.model import train_model
+
 adult_model = train_model(ARTIFACTS_FOLDER, data)
 ```
 
 
 ```python
 from src.explainer import train_explainer
+
 train_explainer(ARTIFACTS_FOLDER, data, adult_model)
 ```
 
@@ -69,6 +76,7 @@ train_explainer(ARTIFACTS_FOLDER, data, adult_model)
 
 ```python
 from src.tempo import create_tempo_artifacts
+
 adult_model, explainer = create_tempo_artifacts(ARTIFACTS_FOLDER)
 ```
 
@@ -76,42 +84,42 @@ adult_model, explainer = create_tempo_artifacts(ARTIFACTS_FOLDER)
 ```python
 # %load src/tempo.py
 import os
-from typing import Any, Tuple
-
 import dill
 import numpy as np
+
+from typing import Any, Tuple
 from alibi.utils.wrappers import ArgmaxTransformer
-from src.constants import EXPLAINER_FOLDER, MODEL_FOLDER
 
 from tempo.serve.metadata import ModelFramework
 from tempo.serve.model import Model
 from tempo.serve.pipeline import PipelineModels
 from tempo.serve.utils import pipeline, predictmethod
 
+from src.constants import ARTIFACTS_FOLDER, EXPLAINER_FOLDER, MODEL_FOLDER
+
 
 def create_tempo_artifacts(artifacts_folder: str) -> Tuple[Model, Any]:
     sklearn_model = Model(
         name="income-sklearn",
         platform=ModelFramework.SKLearn,
-        local_folder=f"{artifacts_folder}/{MODEL_FOLDER}",
+        local_folder=os.path.join(ARTIFACTS_FOLDER, MODEL_FOLDER),
         uri="gs://seldon-models/test/income/model",
     )
 
     @pipeline(
         name="income-explainer",
         uri="s3://tempo/explainer/pipeline",
-        local_folder=f"{artifacts_folder}/{EXPLAINER_FOLDER}",
+        local_folder=os.path.join(ARTIFACTS_FOLDER, EXPLAINER_FOLDER),
         models=PipelineModels(sklearn=sklearn_model),
     )
     class ExplainerPipeline(object):
         def __init__(self):
-            if "MLSERVER_MODELS_DIR" in os.environ:
-                models_folder = ""
-            else:
-                models_folder = f"{artifacts_folder}/{EXPLAINER_FOLDER}"
-            with open(models_folder + "/explainer.dill", "rb") as f:
+            pipeline = self.get_tempo()
+            models_folder = pipeline.details.local_folder
+
+            explainer_path = os.path.join(models_folder, "explainer.dill")
+            with open(explainer_path, "rb") as f:
                 self.explainer = dill.load(f)
-            self.ran_init = True
 
         def update_predict_fn(self, x):
             if np.argmax(self.models.sklearn(x).shape) == 0:
@@ -119,14 +127,13 @@ def create_tempo_artifacts(artifacts_folder: str) -> Tuple[Model, Any]:
                 self.explainer.samplers[0].predictor = self.models.sklearn
             else:
                 self.explainer.predictor = ArgmaxTransformer(self.models.sklearn)
-                self.explainer.samplers[0].predictor = ArgmaxTransformer(self.models.sklearn)
+                self.explainer.samplers[0].predictor = ArgmaxTransformer(
+                    self.models.sklearn
+                )
 
         @predictmethod
         def explain(self, payload: np.ndarray, parameters: dict) -> str:
             print("Explain called with ", parameters)
-            if not self.ran_init:
-                print("Loading explainer")
-                self.__init__()
             self.update_predict_fn(payload)
             explanation = self.explainer.explain(payload, **parameters)
             return explanation.to_json()
@@ -136,7 +143,7 @@ def create_tempo_artifacts(artifacts_folder: str) -> Tuple[Model, Any]:
 
 ```
 
-## Save Outlier and Svc Environments
+## Save Explainer
 
 
 
@@ -146,8 +153,7 @@ def create_tempo_artifacts(artifacts_folder: str) -> Tuple[Model, Any]:
 
 
 ```python
-from tempo.serve.loader import save
-save(explainer)
+tempo.save(explainer)
 ```
 
 ## Test Locally on Docker
@@ -156,7 +162,8 @@ Here we test our models using production images but running locally on Docker. T
 
 
 ```python
-from tempo.seldon.docker import SeldonDockerRuntime
+from tempo.seldon import SeldonDockerRuntime
+
 docker_runtime = SeldonDockerRuntime()
 docker_runtime.deploy(explainer)
 docker_runtime.wait_ready(explainer)
@@ -205,14 +212,14 @@ create_minio_rclone(os.getcwd()+"/rclone-minio.conf")
 
 
 ```python
-from tempo.serve.loader import upload
-upload(adult_model)
-upload(explainer)
+tempo.upload(adult_model)
+tempo.upload(explainer)
 ```
 
 
 ```python
 from tempo.serve.metadata import RuntimeOptions, KubernetesOptions
+
 runtime_options = RuntimeOptions(
         k8s_options=KubernetesOptions(
             namespace="production",
@@ -224,6 +231,7 @@ runtime_options = RuntimeOptions(
 
 ```python
 from tempo.seldon.k8s import SeldonKubernetesRuntime
+
 k8s_runtime = SeldonKubernetesRuntime(runtime_options)
 k8s_runtime.deploy(explainer)
 k8s_runtime.wait_ready(explainer)
@@ -248,8 +256,10 @@ k8s_runtime.undeploy(explainer)
 
 ```python
 from tempo.seldon.k8s import SeldonKubernetesRuntime
+
 k8s_runtime = SeldonKubernetesRuntime(runtime_options)
 yaml_str = k8s_runtime.to_k8s_yaml(explainer)
+
 with open(os.getcwd()+"/k8s/tempo.yaml","w") as f:
     f.write(yaml_str)
 ```
diff --git a/docs/examples/explainer/src/constants.py b/docs/examples/explainer/src/constants.py
index dffcc4d3..eadc451d 100644
--- a/docs/examples/explainer/src/constants.py
+++ b/docs/examples/explainer/src/constants.py
@@ -1,2 +1,7 @@
+import os
+
 MODEL_FOLDER = "model"
 EXPLAINER_FOLDER = "explainer"
+
+ROOT_FOLDER = os.path.dirname(os.path.dirname(__file__))
+ARTIFACTS_FOLDER = os.path.join(ROOT_FOLDER, "artifacts")
diff --git a/docs/examples/explainer/src/tempo.py b/docs/examples/explainer/src/tempo.py
index 73fb8e1c..5e6d002c 100644
--- a/docs/examples/explainer/src/tempo.py
+++ b/docs/examples/explainer/src/tempo.py
@@ -4,7 +4,7 @@
 import dill
 import numpy as np
 from alibi.utils.wrappers import ArgmaxTransformer
-from src.constants import EXPLAINER_FOLDER, MODEL_FOLDER
+from src.constants import ARTIFACTS_FOLDER, EXPLAINER_FOLDER, MODEL_FOLDER
 
 from tempo.serve.metadata import ModelFramework
 from tempo.serve.model import Model
@@ -16,25 +16,24 @@ def create_tempo_artifacts(artifacts_folder: str) -> Tuple[Model, Any]:
     sklearn_model = Model(
         name="income-sklearn",
         platform=ModelFramework.SKLearn,
-        local_folder=f"{artifacts_folder}/{MODEL_FOLDER}",
+        local_folder=os.path.join(ARTIFACTS_FOLDER, MODEL_FOLDER),
         uri="gs://seldon-models/test/income/model",
     )
 
     @pipeline(
         name="income-explainer",
         uri="s3://tempo/explainer/pipeline",
-        local_folder=f"{artifacts_folder}/{EXPLAINER_FOLDER}",
+        local_folder=os.path.join(ARTIFACTS_FOLDER, EXPLAINER_FOLDER),
         models=PipelineModels(sklearn=sklearn_model),
     )
     class ExplainerPipeline(object):
         def __init__(self):
-            if "MLSERVER_MODELS_DIR" in os.environ:
-                models_folder = ""
-            else:
-                models_folder = f"{artifacts_folder}/{EXPLAINER_FOLDER}"
-            with open(models_folder + "/explainer.dill", "rb") as f:
+            pipeline = self.get_tempo()
+            models_folder = pipeline.details.local_folder
+
+            explainer_path = os.path.join(models_folder, "explainer.dill")
+            with open(explainer_path, "rb") as f:
                 self.explainer = dill.load(f)
-            self.ran_init = True
 
         def update_predict_fn(self, x):
             if np.argmax(self.models.sklearn(x).shape) == 0:
@@ -47,9 +46,6 @@ def update_predict_fn(self, x):
         @predictmethod
         def explain(self, payload: np.ndarray, parameters: dict) -> str:
             print("Explain called with ", parameters)
-            if not self.ran_init:
-                print("Loading explainer")
-                self.__init__()
             self.update_predict_fn(payload)
             explanation = self.explainer.explain(payload, **parameters)
             return explanation.to_json()
diff --git a/docs/examples/outlier/README.ipynb b/docs/examples/outlier/README.ipynb
index c792e05d..87a3e4ca 100644
--- a/docs/examples/outlier/README.ipynb
+++ b/docs/examples/outlier/README.ipynb
@@ -57,6 +57,7 @@
      "text": [
       "\u001b[01;34m.\u001b[00m\r\n",
       "├── \u001b[01;34martifacts\u001b[00m\r\n",
+      "│   ├── \u001b[01;34mcifar10_outlier\u001b[00m\r\n",
       "│   ├── \u001b[01;34mmodel\u001b[00m\r\n",
       "│   ├── \u001b[01;34moutlier\u001b[00m\r\n",
       "│   └── \u001b[01;34msvc\u001b[00m\r\n",
@@ -71,7 +72,7 @@
       "└── \u001b[01;34mtests\u001b[00m\r\n",
       "    └── test_tempo.py\r\n",
       "\r\n",
-      "8 directories, 6 files\r\n"
+      "9 directories, 6 files\r\n"
      ]
     }
    ],
@@ -98,12 +99,15 @@
    "outputs": [],
    "source": [
     "import os\n",
-    "from tempo.utils import logger\n",
     "import logging\n",
     "import numpy as np\n",
+    "import tempo\n",
+    "\n",
+    "from tempo.utils import logger\n",
+    "from src.constants import ARTIFACTS_FOLDER\n",
+    "\n",
     "logger.setLevel(logging.ERROR)\n",
-    "logging.basicConfig(level=logging.ERROR)\n",
-    "ARTIFACTS_FOLDER = os.getcwd()+\"/artifacts\""
+    "logging.basicConfig(level=logging.ERROR)"
    ]
   },
   {
@@ -176,16 +180,25 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 6,
+   "execution_count": 4,
    "id": "a8345fb6",
    "metadata": {},
-   "outputs": [],
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Loading from /home/agm/Seldon/tempo/docs/examples/outlier/artifacts/outlier\n"
+     ]
+    }
+   ],
    "source": [
     "from src.tempo import create_outlier_cls, create_model, create_svc_cls\n",
-    "cifar10_model = create_model(ARTIFACTS_FOLDER)\n",
-    "OutlierModel = create_outlier_cls(ARTIFACTS_FOLDER)\n",
+    "\n",
+    "cifar10_model = create_model()\n",
+    "OutlierModel = create_outlier_cls()\n",
     "outlier = OutlierModel()\n",
-    "Cifar10Svc = create_svc_cls(outlier, cifar10_model, ARTIFACTS_FOLDER)\n",
+    "Cifar10Svc = create_svc_cls(outlier, cifar10_model)\n",
     "svc = Cifar10Svc()"
    ]
   },
@@ -203,48 +216,35 @@
     "import os\n",
     "\n",
     "import numpy as np\n",
-    "from src.constants import MODEL_FOLDER, OUTLIER_FOLDER\n",
+    "from alibi_detect.base import NumpyEncoder\n",
+    "from src.constants import ARTIFACTS_FOLDER, MODEL_FOLDER, OUTLIER_FOLDER\n",
     "\n",
     "from tempo.kfserving.protocol import KFServingV1Protocol, KFServingV2Protocol\n",
     "from tempo.serve.metadata import ModelFramework\n",
     "from tempo.serve.model import Model\n",
     "from tempo.serve.pipeline import PipelineModels\n",
     "from tempo.serve.utils import model, pipeline, predictmethod\n",
-    "from alibi_detect.base import NumpyEncoder\n",
     "\n",
     "\n",
-    "def create_outlier_cls(artifacts_folder: str):\n",
+    "def create_outlier_cls():\n",
     "    @model(\n",
     "        name=\"outlier\",\n",
-    "        platform=ModelFramework.TempoPipeline,\n",
+    "        platform=ModelFramework.Custom,\n",
     "        protocol=KFServingV2Protocol(),\n",
     "        uri=\"s3://tempo/outlier/cifar10/outlier\",\n",
-    "        local_folder=f\"{artifacts_folder}/{OUTLIER_FOLDER}\",\n",
+    "        local_folder=os.path.join(ARTIFACTS_FOLDER, OUTLIER_FOLDER),\n",
     "    )\n",
     "    class OutlierModel(object):\n",
-    "\n",
     "        def __init__(self):\n",
-    "            self.loaded = False\n",
-    "\n",
-    "        def load(self):\n",
     "            from alibi_detect.utils.saving import load_detector\n",
     "\n",
-    "            if \"MLSERVER_MODELS_DIR\" in os.environ:\n",
-    "                models_folder = \"/mnt/models\"\n",
-    "            else:\n",
-    "                models_folder = f\"{artifacts_folder}/{OUTLIER_FOLDER}\"\n",
+    "            model = self.get_tempo()\n",
+    "            models_folder = model.details.local_folder\n",
     "            print(f\"Loading from {models_folder}\")\n",
-    "            self.od = load_detector(f\"{models_folder}/cifar10\")\n",
-    "            self.loaded = True\n",
-    "\n",
-    "        def unload(self):\n",
-    "            self.od = None\n",
-    "            self.loaded = False\n",
+    "            self.od = load_detector(os.path.join(models_folder, \"cifar10\"))\n",
     "\n",
     "        @predictmethod\n",
     "        def outlier(self, payload: np.ndarray) -> dict:\n",
-    "            if not self.loaded:\n",
-    "                self.load()\n",
     "            od_preds = self.od.predict(\n",
     "                payload,\n",
     "                outlier_type=\"instance\",  # use 'feature' or 'instance' level\n",
@@ -258,25 +258,25 @@
     "    return OutlierModel\n",
     "\n",
     "\n",
-    "def create_model(arifacts_folder: str):\n",
+    "def create_model():\n",
     "\n",
     "    cifar10_model = Model(\n",
     "        name=\"resnet32\",\n",
     "        protocol=KFServingV1Protocol(),\n",
     "        platform=ModelFramework.Tensorflow,\n",
     "        uri=\"gs://seldon-models/tfserving/cifar10/resnet32\",\n",
-    "        local_folder=f\"{arifacts_folder}/{MODEL_FOLDER}\",\n",
+    "        local_folder=os.path.join(ARTIFACTS_FOLDER, MODEL_FOLDER),\n",
     "    )\n",
     "\n",
     "    return cifar10_model\n",
     "\n",
     "\n",
-    "def create_svc_cls(outlier, model, arifacts_folder: str):\n",
+    "def create_svc_cls(outlier, model):\n",
     "    @pipeline(\n",
     "        name=\"cifar10-service\",\n",
     "        protocol=KFServingV2Protocol(),\n",
     "        uri=\"s3://tempo/outlier/cifar10/svc\",\n",
-    "        local_folder=f\"{arifacts_folder}/svc\",\n",
+    "        local_folder=os.path.join(ARTIFACTS_FOLDER, \"svc\"),\n",
     "        models=PipelineModels(outlier=outlier, cifar10=model),\n",
     "    )\n",
     "    class Cifar10Svc(object):\n",
@@ -313,32 +313,32 @@
    "outputs": [],
    "source": [
     "# %load tests/test_tempo.py\n",
-    "from src.tempo import create_outlier_cls, create_model, create_svc_cls\n",
     "import numpy as np\n",
+    "from src.tempo import create_model, create_outlier_cls, create_svc_cls\n",
     "\n",
     "\n",
     "def test_svc_outlier():\n",
-    "    model = create_model(\"\")\n",
-    "    OutlierModel = create_outlier_cls(\"\")\n",
+    "    model = create_model()\n",
+    "    OutlierModel = create_outlier_cls()\n",
     "    outlier = OutlierModel()\n",
-    "    Cifar10Svc = create_svc_cls(outlier, model, \"\")\n",
+    "    Cifar10Svc = create_svc_cls(outlier, model)\n",
     "    svc = Cifar10Svc()\n",
-    "    svc.models.outlier = lambda payload: {\"data\":{\"is_outlier\":[1]}}\n",
+    "    svc.models.outlier = lambda payload: {\"data\": {\"is_outlier\": [1]}}\n",
     "    svc.models.cifar10 = lambda input: np.array([[0.2]])\n",
     "    res = svc(np.array([1]))\n",
     "    assert res.shape[0] == 0\n",
     "\n",
     "\n",
     "def test_svc_inlier():\n",
-    "    model = create_model(\"\")\n",
-    "    OutlierModel = create_outlier_cls(\"\")\n",
+    "    model = create_model()\n",
+    "    OutlierModel = create_outlier_cls()\n",
     "    outlier = OutlierModel()\n",
-    "    Cifar10Svc = create_svc_cls(outlier, model, \"\")\n",
+    "    Cifar10Svc = create_svc_cls(outlier, model)\n",
     "    svc = Cifar10Svc()\n",
-    "    svc.models.outlier = lambda payload: {\"data\":{\"is_outlier\":[0]}}\n",
+    "    svc.models.outlier = lambda payload: {\"data\": {\"is_outlier\": [0]}}\n",
     "    svc.models.cifar10 = lambda input: np.array([[0.2]])\n",
     "    res = svc(np.array([1]))\n",
-    "    assert res.shape[0] == 1"
+    "    assert res.shape[0] == 1\n"
    ]
   },
   {
@@ -353,23 +353,33 @@
      "text": [
       "\u001b[1m============================= test session starts ==============================\u001b[0m\n",
       "platform linux -- Python 3.7.9, pytest-6.2.0, py-1.10.0, pluggy-0.13.1\n",
-      "rootdir: /home/clive/work/mlops/fork-tempo, configfile: setup.cfg\n",
+      "rootdir: /home/agm/Seldon/tempo, configfile: setup.cfg\n",
       "plugins: asyncio-0.14.0\n",
       "collected 2 items                                                              \u001b[0m\u001b[1m\n",
       "\n",
       "tests/test_tempo.py \u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[33m                                                   [100%]\u001b[0m\n",
       "\n",
       "\u001b[33m=============================== warnings summary ===============================\u001b[0m\n",
-      "../../../../../../anaconda3/envs/tempo-examples/lib/python3.7/site-packages/tensorflow/python/autograph/impl/api.py:22\n",
-      "  /home/clive/anaconda3/envs/tempo-examples/lib/python3.7/site-packages/tensorflow/python/autograph/impl/api.py:22: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses\n",
+      "../../../../../.conda/envs/tempo-conda/lib/python3.7/site-packages/tensorflow/python/autograph/impl/api.py:22\n",
+      "  /home/agm/.conda/envs/tempo-conda/lib/python3.7/site-packages/tensorflow/python/autograph/impl/api.py:22: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses\n",
       "    import imp\n",
       "\n",
-      "../../../../../../anaconda3/envs/tempo-examples/lib/python3.7/site-packages/packaging/version.py:130\n",
-      "  /home/clive/anaconda3/envs/tempo-examples/lib/python3.7/site-packages/packaging/version.py:130: DeprecationWarning: Creating a LegacyVersion has been deprecated and will be removed in the next major release\n",
+      "../../../../../.conda/envs/tempo-conda/lib/python3.7/site-packages/packaging/version.py:130\n",
+      "  /home/agm/.conda/envs/tempo-conda/lib/python3.7/site-packages/packaging/version.py:130: DeprecationWarning: Creating a LegacyVersion has been deprecated and will be removed in the next major release\n",
       "    DeprecationWarning,\n",
       "\n",
       "-- Docs: https://docs.pytest.org/en/stable/warnings.html\n",
-      "\u001b[33m======================== \u001b[32m2 passed\u001b[0m, \u001b[33m\u001b[1m2 warnings\u001b[0m\u001b[33m in 4.48s\u001b[0m\u001b[33m =========================\u001b[0m\n"
+      "\u001b[33m======================== \u001b[32m2 passed\u001b[0m, \u001b[33m\u001b[1m2 warnings\u001b[0m\u001b[33m in 2.99s\u001b[0m\u001b[33m =========================\u001b[0m\n",
+      "Unresolved object in checkpoint: (root).encoder.fc_mean.kernel\n",
+      "Unresolved object in checkpoint: (root).encoder.fc_mean.bias\n",
+      "Unresolved object in checkpoint: (root).encoder.fc_log_var.kernel\n",
+      "Unresolved object in checkpoint: (root).encoder.fc_log_var.bias\n",
+      "A checkpoint was restored (e.g. tf.train.Checkpoint.restore or tf.keras.Model.load_weights) but not all checkpointed values were used. See above for specific issues. Use expect_partial() on the load status object, e.g. tf.train.Checkpoint.restore(...).expect_partial(), to silence these warnings, or use assert_consumed() to make the check explicit. See https://www.tensorflow.org/guide/checkpoint#loading_mechanics for details.\n",
+      "Unresolved object in checkpoint: (root).encoder.fc_mean.kernel\n",
+      "Unresolved object in checkpoint: (root).encoder.fc_mean.bias\n",
+      "Unresolved object in checkpoint: (root).encoder.fc_log_var.kernel\n",
+      "Unresolved object in checkpoint: (root).encoder.fc_log_var.bias\n",
+      "A checkpoint was restored (e.g. tf.train.Checkpoint.restore or tf.keras.Model.load_weights) but not all checkpointed values were used. See above for specific issues. Use expect_partial() on the load status object, e.g. tf.train.Checkpoint.restore(...).expect_partial(), to silence these warnings, or use assert_consumed() to make the check explicit. See https://www.tensorflow.org/guide/checkpoint#loading_mechanics for details.\n"
      ]
     }
    ],
@@ -387,7 +397,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 9,
+   "execution_count": 8,
    "id": "3e1f9017",
    "metadata": {},
    "outputs": [
@@ -401,11 +411,11 @@
       "dependencies:\r\n",
       "  - python=3.7.9\r\n",
       "  - pip:\r\n",
-      "    - alibi-detect\r\n",
-      "    - dill\r\n",
-      "    - opencv-python-headless\r\n",
-      "    - mlops-tempo\r\n",
-      "    - mlserver==0.3.1.dev7\r\n"
+      "      - alibi-detect\r\n",
+      "      - dill\r\n",
+      "      - opencv-python-headless\r\n",
+      "      - mlops-tempo @ file:///home/agm/Seldon/tempo\r\n",
+      "      - mlserver==0.3.1.dev7\r\n"
      ]
     }
    ],
@@ -415,7 +425,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 10,
+   "execution_count": 9,
    "id": "9a515728",
    "metadata": {},
    "outputs": [
@@ -429,8 +439,8 @@
       "dependencies:\r\n",
       "  - python=3.7.9\r\n",
       "  - pip:\r\n",
-      "    - mlops-tempo\r\n",
-      "    - mlserver==0.3.1.dev7\r\n"
+      "      - mlops-tempo @ file:///home/agm/Seldon/tempo\r\n",
+      "      - mlserver==0.3.1.dev7\r\n"
      ]
     }
    ],
@@ -440,14 +450,26 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 10,
    "id": "3c23ab3b",
    "metadata": {},
-   "outputs": [],
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Collecting packages...\n",
+      "Packing environment at '/home/agm/.conda/envs/tempo-709d491d-cac5-49d8-9978-8fb4c1a231af' to '/home/agm/Seldon/tempo/docs/examples/outlier/artifacts/outlier/environment.tar.gz'\n",
+      "[########################################] | 100% Completed | 59.3s\n",
+      "Collecting packages...\n",
+      "Packing environment at '/home/agm/.conda/envs/tempo-1550fd3d-02f7-4560-83aa-a36a50d7598b' to '/home/agm/Seldon/tempo/docs/examples/outlier/artifacts/svc/environment.tar.gz'\n",
+      "[########################################] | 100% Completed | 10.9s\n"
+     ]
+    }
+   ],
    "source": [
-    "from tempo.serve.loader import save\n",
-    "save(outlier)\n",
-    "save(svc)"
+    "tempo.save(OutlierModel)\n",
+    "tempo.save(Cifar10Svc)"
    ]
   },
   {
@@ -462,12 +484,13 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 11,
    "id": "a36bfb9f",
    "metadata": {},
    "outputs": [],
    "source": [
     "from tempo.seldon.docker import SeldonDockerRuntime\n",
+    "\n",
     "docker_runtime = SeldonDockerRuntime()\n",
     "docker_runtime.deploy(svc)\n",
     "docker_runtime.wait_ready(svc)"
@@ -475,22 +498,73 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 12,
    "id": "fb09a516",
    "metadata": {},
-   "outputs": [],
+   "outputs": [
+    {
+     "data": {
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAOcAAADnCAYAAADl9EEgAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/Z1A+gAAAACXBIWXMAAAsTAAALEwEAmpwYAAAU4UlEQVR4nO2dW49k51WG1z7Wubqqp49z6HbPyZPYgOPERo6MSRBIIQiJCyS45Adww+/gHyDhIHGDLEQUiUQIgaVEyA6OPfYY5uBxe7p7pk/Th+qq7jrs2rUPXITL713S3CRL0ftc7qWvatfe+60trfdba3llWQohxB7+r/sECCFuKE5CjEJxEmIUipMQo1CchBgl1II/+JvXYSrXKwu4Lo7cH+v5+L8gTacwluUz/F1xDGN54T7HssAZas/PYcwPYEjKWQN/puDPjOLEeTxQbo3n4/PPiwzGZhm+Z0XhgS/D55HlYI2ITNHniQiOiBTgufI8vCpN8fOR58p1VJ5hX7lnKXiuRvjSyzjFn/e37z1x/ji+OQkxCsVJiFEoTkKMQnESYhSKkxCjUJyEGEW1UlJFu2U5wQtBqrki2G7wBfsUYajYG9rfC3AcvAgvmqYpjGWFco4l/sxAsWBCsMwrsD0gGbadNAugUM4/9arO43lQwWu0z8vx9fAKfI4esIKqyj0LPRzzQ8V2minX2MO+SAmucamYREHw4u9BvjkJMQrFSYhRKE5CjEJxEmIUipMQo1CchBhFtVJKpcJBSpzOL3P3Oi/Hqfdihi2MoKak5QVXFiALo1BS+XEUwVhW4lgxU36b8n1Z5o55Sm8nX7FtvABX6ZSB2y4REZnkbsvk8BTbDaMUn+NwiNcFJb4erar7OsYevs/teg3GahX8DBc+fuZ81RZxnyN+OkRmSiUUPgdCiEkoTkKMQnESYhSKkxCjUJyEGEXN1oY5zshKoGQTwabtSqBkf0Ols4yyu93XNhSDU8y0zJmPzyOKcVZw5aXbMHbeP4Gxk9Ox+7tCnHX1RdmMnuFbOinx+T/ccZ9jWZmHa2YBLmRImzgzPBz0YGzvqO883qzg35UfuteIiKwt4+t4qYWvYzXUeg+5n+NYeYRzJUON4JuTEKNQnIQYheIkxCgUJyFGoTgJMQrFSYhRVCtFa5zvhR0cA63zM639vY9tljTDG5RjpcdNnoNeL8pGdFHa/sdKH5vf/cM/grFPPvgQxvb7p87jI8USyXJsYezsHsPY1t4ejFU6q87jV5c34Jqy0oKxNMT3JWouwliWDJ3HT4/24Zp6B9s9u8PnMJaAXlciIsstvI29Hrk3vuczty0mIqJM0MBrXnwJIeRXAcVJiFEoTkKMQnESYhSKkxCjUJyEGEW1UqY+TpUPxnUYy8G4gG4T2yXtANsbodJPp1BsFg8s03ojaVUu4/EZjL3/rz+Csed9XN3zfOj+vp09/F07B89gLKg2YSwP2jDWaC84j0d1/HlhFVe5VJQRCVUfW0EnqXvMx+rVNbgmmYxgbGsLWym9gXuquIhI4OHf/dKiOxbl2JrxQF8tDb45CTEKxUmIUShOQoxCcRJiFIqTEKNQnIQYRbVSjid4xEBv1oGxn33wU+fxr93CKfTvvuJO5YuIdJVmYgWoPBER8UHbfN/HFQd5iccIKO6AbO1swVhvgis0ynrXeTxo4lS+372AsVpnDsbSBFsHKRh30O7ie9Zu4tjR4SGMnZ/hBl+t2P1IVmvYtnl6hhuoRa0lGDs+fApjzef4Gq+03edS85RKIm1SOYBvTkKMQnESYhSKkxCjUJyEGIXiJMQoFCchRtFnpczh5k7jU6zrWexu4NQbY2tmnOLZGu0YV54UYG7F/wedh4MAV9QkKU7ZHyujY04usKWjNaDqLrqrLUbFOVyzIPgcA6VSJI3wdUxGbusgGeLzWF++BGNjYImIiByByhMRES9y206DHm6eJUrDtskIV6wEMX4Ojs5xVdABqGZZX8DPt48LVvCaF19CCPlVQHESYhSKkxCjUJyEGIXiJMQoarb25d9+E8Z2f/4FjDXn3NnaN9/Cn1cPdmAsBZlEERE/xJvYvciduczLDlzTWroGY599vgljzQ7OXF5ZfwXGSt+dnYyUzGoxdY9wEBFJU2XkhXKtArBp+/69z+GadkUZWdDAm+IbSl+i/UN3zx9tGnkAMrwiIt0Wzl4PcrwZ/ayHY1uHA+fxy8srcE2oOA4IvjkJMQrFSYhRKE5CjEJxEmIUipMQo1CchBhFtVLqc9geWL9+G8YmIAu9tnETrlmY4VR5fwvbLDNl43ueuTc2v/nOn8E1a9e/BWMbv7UNY598eg/Guk2cYt8/cve/CcsYrqlE2MIQZYLyUNkEPgB9fboN/F3asOZcsT4WFvFk6+nMfT9Pztz2hYiIp4zQaCl9jsIAP/5pgjfaP3m26zy+2MG2za2reLQJgm9OQoxCcRJiFIqTEKNQnIQYheIkxCgUJyFGUa2UoKJUDzx/CGOvffMN5/HGHO7ZElzswVie4bR8qPSqefLMXc3ydhf3RpL6VRhqNXB6vRria1VTetVUY1BRofTFuXJ5FcYefPUVjMUx7tN0fuG+Vi9dvQXX3L7zdRjr9XAPnma7A2P7h0fO456P+/N0urhH00DpBRQoFkyt3oGxyYX7OdgEz5uISC1+8fcg35yEGIXiJMQoFCchRqE4CTEKxUmIUShOQoyiWilRtQ1jSYIbFk2n7rKUSLEU6g38XQ1lxEAlwFUpzdA9P+Ef/u7v4Zo//Yu/hrFohKc1xxX8P+f7+Bw3rl9xHj/q7cM1yRBXl6ws4QnhvXNsBU1T9/28fhNXEt24iSuTBp/ehbHRxRDGzkfuc8xy3LhsMsETuzvKpO+8xNZHu4OrcbLUfT8DH8/r2D1wW0QafHMSYhSKkxCjUJyEGIXiJMQoFCchRqE4CTGKaqV4AU4nj5V0fjJ2Ty6OlJkWF6e4CkMCbKVEghs/rXbclQxfPsQzT/Z3cUzG2N7Y2d2GsW+s4BkxV9bdzb8uHy3DNaNN3PBsvtKBsVYH2yxPnmw7j69edls9IiL9czz1eqZYH8+P8ayXovScxz2lGddYsVI8Hz9X7m/6JQ2lMZgU7iqY2MMTu9NTbMMh+OYkxCgUJyFGoTgJMQrFSYhRKE5CjEJxEmIU1UoRbdR3iVPlqwvuGSv1KrZS3v8cN6bqZvi7bs1ju6dacafR4xCn3o+PtmGsmOJmUWs3cNOwQPnd9XbXeXxhGTcaO+3hqo6BUnmSK27VIphfEir2VwKqM0REUjDzRERkkuDqjQycJDouIpJMcYVUluH3z6WFJRjzPPxcxZ77+al4ytyeEldkIfjmJMQoFCchRqE4CTEKxUmIUShOQoyi9xAKcQv8uSbejN5puWNegbNZ5yXeaHxyhrcoL7TwT2jE7oxb7oPR2yKyvb8NY8td3I9m/SYeTZDgr5OPPnGPtdg7wJnhVtOd4RURiSI8cuH+5lN8IuB/ulD+v6dKtnY4wpvAO/N4fEIGNr4fPMc9eBotfF/CADsO9TrOoMZoTIaIyMy9cT8f9eGS5SVOtibkNwaKkxCjUJyEGIXiJMQoFCchRqE4CTGKPtnawxbGypK7980vPxSk5ZUNz6tX8cbxjxV7o+9hC6YM3H2O5hbwJuq5Nt7wHFVxOvwlxUppzrkLAUREfvDuPzqPj5VrdT7pwdh4gns7RcrdXum6f3fSw/2KRqCwQERkro3vy6MvvoSx58+PncfPlREOnQ7+Ye0GnjgelNjjilJ8HQPQS2qxgT9vrqp1LHLDNychRqE4CTEKxUmIUShOQoxCcRJiFIqTEKOoVoq2M7/dxVZKlrs/thLiz7u9sQZjH3+CLYzzCE9eLjz35OLlK9guefDw5zD27d//Kxj78AO8bjRSxhakJ87jR4fP4BrtP3U4w7FQcKq/67urYK7U8LkPjrElkgW4cmZ5Ccfy3F3pok2vTia4b9JI6YGUFdiemSV7MLYUuStuLjdxlcs0w1U6CL45CTEKxUmIUShOQoxCcRJiFIqTEKNQnIQYRbVStOm+3QU8JTnz3B+b+DFcU222YazTwQ2cnj7DE4PffuMV93kM8XiHestdFSEicrC3C2Objx/DWJbjcQE+6KE2OscTu1uXVmFsMMC2wlwTN/96+farzuO/uPcIrrn7aBvG3v7OH8NYFGPL4cmme7L44AL/Lq0JWTLBdsn6Mrboag3cwG5+3r2uDHHDsyzFjcYQfHMSYhSKkxCjUJyEGIXiJMQoFCchRqE4CTGKaqUUmZKWn8eNk0YTd+Onca5Myg7w/8TaNTzl+fF9XBkxGLstk2YDV8BcuwFDsvMYN7va2z+AsbfeegPGxmN3qr91+QpcM38ZN0N72sPWx2SKLaS44Z5f0l68Btd8o4Xvy/Gxe56IiMj2zj0YG03ctlN/gC0RNJVbRGSuxPdlvYktrqU2nhMUee5KnXSGK08aSrM8BN+chBiF4iTEKBQnIUahOAkxCsVJiFHUbO3FKc501ZTeLNPEnQXzCvx1noczuQvzeJzBY/8JjB313C31TwOctZxr4t5Id17FG/Cf7OCePzM8tUD65+6M+K1bt+CaWxs4pbxzgDfM37//PzB2euLejB5XcFa+28Qbx3fv46zx4SnuS+SB4ohAGYWhjfJYV5Kkay1cCFD18Sb2aeJ+fooC96aaZfjzEHxzEmIUipMQo1CchBiF4iTEKBQnIUahOAkximqlPNnENsXara/BWNV3WylFijcGh1Ulra3EWi2c6m+23X2J7tx5Ga75j3//CYyNB7hfUX1+CcY2d49g7NpV9yb8jZdfh2sqMb5t19fwpv5+zz1yQUTkwUN3AUFRYh9or483jp+D4gcRkSTHNtx5320tLa3gTfZPT3GBxvw1bH+dVvB5SIF/Wz9z/7YyxM/pVPk8BN+chBiF4iTEKBQnIUahOAkxCsVJiFEoTkKMolopn21iC2Dt1TdhrBB3NYin7cwvcFXK+YV7QrWISL/vngwtInJp/jXn8e9/77twzWu/cwfG3vuXH8KY5+GeM3NzeJLzlctui6DZ7sA1Qea+viIi8yv4lq5u4MnWg5rbBvj0Hu73czDEJR9lhMdrzK3gKqOFG27rI1BsirzE5/FFiUeKbB5iuycO8GdOEveU7bHyeGcFfj4QfHMSYhSKkxCjUJyEGIXiJMQoFCchRqE4CTGKaqU8HuDpvic5brhURu5Us5/i5lOlkmr20fhnEbm8iqtBfu/b7sqOaoRT6BvreAzCn/z5X8LYP//wxzB2coh/98HA3SwqSdwTnkVEYsE5+94ExzZ3cFWNpG6bpVzAFTzdJTyhuhBsjXkeboRVVN2fWXh4KvpMGfMxyPF3VSNl0nqIrZSR566CmUX4u8oC21gIvjkJMQrFSYhRKE5CjEJxEmIUipMQo1CchBhFt1L6WLs/+i88d+O19QXn8ZUYVwjUI6WaYgXPL1ldwNUPN66DplAlbrZ0oExkfvefsF1y97MHMIZmx4iIwEKdEl/7Msefl1fw9ch9nOoPxW2bZUq1TeZjq62qPVlKFUmSun936eM1oVKxEhR4Lk6ZYNspE7wuKtznGHj4nqUzTrYm5DcGipMQo1CchBiF4iTEKBQnIUZRs7VDMGVYROQ/7z6GsS+/co9x+N43vw7X3LiM2+ZvPXGPChAReeeNV2GsCjYiX6Q4A/nev/0Cxj59sA9j40xp7a9kE/3I/f9YKD2VfA9nGbWsZl7gDf9TkIGc5XiN5+HN3FNRNoGX+LeFIciEBvg9Uq/j5zQWfP45TshK7mFp5GBhNsP3JW518JcB+OYkxCgUJyFGoTgJMQrFSYhRKE5CjEJxEmIU1Uq5tLAIY70znA4/OOs7j39w7xFck8/WlTPBqfJFZeKxF7jtjY8+/l+45sfvfwhj0wL3zJEQWym+/+L/gfkUb24vFZulUOwSzcJAIw2iED8iXqCMGAjwPQuVdUHg/j5tgnmgXF+/xHZPrhQXFIoVhDyYlRVsB7baOIbgm5MQo1CchBiF4iTEKBQnIUahOAkxCsVJiFFUK0VLeUcRtg6yxJ1G335+DtdMRw9h7J3Xb8NYrbMKY4PEnfL+6X9/DNckJa4smGU4LV+p4MqTQuljMx67W/trBErFhKe1qsFOilSAheH5yiOixLwKtp1qNdx7KATWzUyp+LgY4UnfuWI7TTN8X+a67j5YIiLLq+5YU2mcNFGmsyP45iTEKBQnIUahOAkxCsVJiFEoTkKMQnESYhTVSikyXOGgjQsoAretkAq2Zo6GUxi7+wVurPX9MU6VX5Tu9PXeGU5rV5q4+iEb4/NPpvj863XFOgBjKLTP85RJ374yPkGrMCmBLVIq/9+RYh8NZ/jZSTNsfSCbRauo0SyRkTIKo9nBdklnEY8ASTP3Z37xCFddRUq1EIJvTkKMQnESYhSKkxCjUJyEGIXiJMQoFCchRlGtFFF29EuJ09dB4G6OVJQ4za9NXd4+wtbHu+/9BMb+4Dvfch7f2j+Ga8a51vRJsRWquKFVEONYHcwAiWvYpphcYCtCq94oFcshAhUVQYjvmfZdgVLRpM2BmYyHL7xG+65Odx7GLi3jiqaT0x6M9U8O3cef4pk+Nzc2YAzBNychRqE4CTEKxUmIUShOQoxCcRJiFIqTEKOoVsp8pwNjSYLtjdHEvWs/DnB1Rqak+X2lmdjPPvocxrb23dUsgxFu1NUbTmAMFCOIiEijoVSzKA2+KhX3bwsV+6VawxUOgVKxEkb4M3PwP50pFoanxMpSGfc+w9c/nbkvcq2KraWFS5dgrLuA7ZJUqayaxkqzror7OhYhtgNHCX6uEHxzEmIUipMQo1CchBiF4iTEKBQnIUZRs7VTJcNUUWQ9zd3ZuEiZdpwpQ5JLbXJxDWdJd8AGd1/ZzJ3NcAZSyygnSQJjI2VcAJp6jbK4IiKNGGcFa8qGed/H5x9X3d9Xq+Prm6Z44/tJD28cLwSvCyP39ei2G3DN8nwHxlZW8Mb3/gj3abron8HYcNB3Hu/M4+86OT6BMQTfnIQYheIkxCgUJyFGoTgJMQrFSYhRKE5CjKJbKRNsD1QCPEK5Dj61mGFrRpkiIIVgC6BQehkVYPxDliobtnP8u7SRAFpMm2yNrJSzM5zK7ynXsd3ElsOc0k+nDXoZVQVbM3mBrYjQUzbnV/DNnibuz6yE+L5o35WNB0oMn/+wfwpjBdicX61giytR+hwh+OYkxCgUJyFGoTgJMQrFSYhRKE5CjEJxEmIUT7MACCG/PvjmJMQoFCchRqE4CTEKxUmIUShOQoxCcRJilP8DKnTF0srourIAAAAASUVORK5CYII=\n",
+      "text/plain": [
+       "<Figure size 432x288 with 1 Axes>"
+      ]
+     },
+     "metadata": {
+      "needs_background": "light"
+     },
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/plain": [
+       "array([[3.92254496e-09, 1.20455460e-11, 2.66010169e-09, 9.99992609e-01,\n",
+       "        2.52212834e-10, 5.40860242e-07, 6.75951833e-06, 4.75118165e-12,\n",
+       "        6.90873403e-09, 1.07275378e-11]])"
+      ]
+     },
+     "execution_count": 12,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
    "source": [
     "from src.utils import show_image\n",
+    "\n",
     "show_image(data.X_test[0:1])\n",
     "svc(payload=data.X_test[0:1])"
    ]
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 14,
    "id": "b22014e7",
    "metadata": {},
-   "outputs": [],
+   "outputs": [
+    {
+     "data": {
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAOcAAADnCAYAAADl9EEgAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/Z1A+gAAAACXBIWXMAAAsTAAALEwEAmpwYAAAU4UlEQVR4nO2dW49k51WG1z7Wubqqp49z6HbPyZPYgOPERo6MSRBIIQiJCyS45Adww+/gHyDhIHGDLEQUiUQIgaVEyA6OPfYY5uBxe7p7pk/Th+qq7jrs2rUPXITL713S3CRL0ftc7qWvatfe+60trfdba3llWQohxB7+r/sECCFuKE5CjEJxEmIUipMQo1CchBgl1II/+JvXYSrXKwu4Lo7cH+v5+L8gTacwluUz/F1xDGN54T7HssAZas/PYcwPYEjKWQN/puDPjOLEeTxQbo3n4/PPiwzGZhm+Z0XhgS/D55HlYI2ITNHniQiOiBTgufI8vCpN8fOR58p1VJ5hX7lnKXiuRvjSyzjFn/e37z1x/ji+OQkxCsVJiFEoTkKMQnESYhSKkxCjUJyEGEW1UlJFu2U5wQtBqrki2G7wBfsUYajYG9rfC3AcvAgvmqYpjGWFco4l/sxAsWBCsMwrsD0gGbadNAugUM4/9arO43lQwWu0z8vx9fAKfI4esIKqyj0LPRzzQ8V2minX2MO+SAmucamYREHw4u9BvjkJMQrFSYhRKE5CjEJxEmIUipMQo1CchBhFtVJKpcJBSpzOL3P3Oi/Hqfdihi2MoKak5QVXFiALo1BS+XEUwVhW4lgxU36b8n1Z5o55Sm8nX7FtvABX6ZSB2y4REZnkbsvk8BTbDaMUn+NwiNcFJb4erar7OsYevs/teg3GahX8DBc+fuZ81RZxnyN+OkRmSiUUPgdCiEkoTkKMQnESYhSKkxCjUJyEGEXN1oY5zshKoGQTwabtSqBkf0Ols4yyu93XNhSDU8y0zJmPzyOKcVZw5aXbMHbeP4Gxk9Ox+7tCnHX1RdmMnuFbOinx+T/ccZ9jWZmHa2YBLmRImzgzPBz0YGzvqO883qzg35UfuteIiKwt4+t4qYWvYzXUeg+5n+NYeYRzJUON4JuTEKNQnIQYheIkxCgUJyFGoTgJMQrFSYhRVCtFa5zvhR0cA63zM639vY9tljTDG5RjpcdNnoNeL8pGdFHa/sdKH5vf/cM/grFPPvgQxvb7p87jI8USyXJsYezsHsPY1t4ejFU6q87jV5c34Jqy0oKxNMT3JWouwliWDJ3HT4/24Zp6B9s9u8PnMJaAXlciIsstvI29Hrk3vuczty0mIqJM0MBrXnwJIeRXAcVJiFEoTkKMQnESYhSKkxCjUJyEGEW1UqY+TpUPxnUYy8G4gG4T2yXtANsbodJPp1BsFg8s03ojaVUu4/EZjL3/rz+Csed9XN3zfOj+vp09/F07B89gLKg2YSwP2jDWaC84j0d1/HlhFVe5VJQRCVUfW0EnqXvMx+rVNbgmmYxgbGsLWym9gXuquIhI4OHf/dKiOxbl2JrxQF8tDb45CTEKxUmIUShOQoxCcRJiFIqTEKNQnIQYRbVSjid4xEBv1oGxn33wU+fxr93CKfTvvuJO5YuIdJVmYgWoPBER8UHbfN/HFQd5iccIKO6AbO1swVhvgis0ynrXeTxo4lS+372AsVpnDsbSBFsHKRh30O7ie9Zu4tjR4SGMnZ/hBl+t2P1IVmvYtnl6hhuoRa0lGDs+fApjzef4Gq+03edS85RKIm1SOYBvTkKMQnESYhSKkxCjUJyEGIXiJMQoFCchRtFnpczh5k7jU6zrWexu4NQbY2tmnOLZGu0YV54UYG7F/wedh4MAV9QkKU7ZHyujY04usKWjNaDqLrqrLUbFOVyzIPgcA6VSJI3wdUxGbusgGeLzWF++BGNjYImIiByByhMRES9y206DHm6eJUrDtskIV6wEMX4Ojs5xVdABqGZZX8DPt48LVvCaF19CCPlVQHESYhSKkxCjUJyEGIXiJMQoarb25d9+E8Z2f/4FjDXn3NnaN9/Cn1cPdmAsBZlEERE/xJvYvciduczLDlzTWroGY599vgljzQ7OXF5ZfwXGSt+dnYyUzGoxdY9wEBFJU2XkhXKtArBp+/69z+GadkUZWdDAm+IbSl+i/UN3zx9tGnkAMrwiIt0Wzl4PcrwZ/ayHY1uHA+fxy8srcE2oOA4IvjkJMQrFSYhRKE5CjEJxEmIUipMQo1CchBhFtVLqc9geWL9+G8YmIAu9tnETrlmY4VR5fwvbLDNl43ueuTc2v/nOn8E1a9e/BWMbv7UNY598eg/Guk2cYt8/cve/CcsYrqlE2MIQZYLyUNkEPgB9fboN/F3asOZcsT4WFvFk6+nMfT9Pztz2hYiIp4zQaCl9jsIAP/5pgjfaP3m26zy+2MG2za2reLQJgm9OQoxCcRJiFIqTEKNQnIQYheIkxCgUJyFGUa2UoKJUDzx/CGOvffMN5/HGHO7ZElzswVie4bR8qPSqefLMXc3ydhf3RpL6VRhqNXB6vRria1VTetVUY1BRofTFuXJ5FcYefPUVjMUx7tN0fuG+Vi9dvQXX3L7zdRjr9XAPnma7A2P7h0fO456P+/N0urhH00DpBRQoFkyt3oGxyYX7OdgEz5uISC1+8fcg35yEGIXiJMQoFCchRqE4CTEKxUmIUShOQoyiWilRtQ1jSYIbFk2n7rKUSLEU6g38XQ1lxEAlwFUpzdA9P+Ef/u7v4Zo//Yu/hrFohKc1xxX8P+f7+Bw3rl9xHj/q7cM1yRBXl6ws4QnhvXNsBU1T9/28fhNXEt24iSuTBp/ehbHRxRDGzkfuc8xy3LhsMsETuzvKpO+8xNZHu4OrcbLUfT8DH8/r2D1wW0QafHMSYhSKkxCjUJyEGIXiJMQoFCchRqE4CTGKaqV4AU4nj5V0fjJ2Ty6OlJkWF6e4CkMCbKVEghs/rXbclQxfPsQzT/Z3cUzG2N7Y2d2GsW+s4BkxV9bdzb8uHy3DNaNN3PBsvtKBsVYH2yxPnmw7j69edls9IiL9czz1eqZYH8+P8ayXovScxz2lGddYsVI8Hz9X7m/6JQ2lMZgU7iqY2MMTu9NTbMMh+OYkxCgUJyFGoTgJMQrFSYhRKE5CjEJxEmIU1UoRbdR3iVPlqwvuGSv1KrZS3v8cN6bqZvi7bs1ju6dacafR4xCn3o+PtmGsmOJmUWs3cNOwQPnd9XbXeXxhGTcaO+3hqo6BUnmSK27VIphfEir2VwKqM0REUjDzRERkkuDqjQycJDouIpJMcYVUluH3z6WFJRjzPPxcxZ77+al4ytyeEldkIfjmJMQoFCchRqE4CTEKxUmIUShOQoyi9xAKcQv8uSbejN5puWNegbNZ5yXeaHxyhrcoL7TwT2jE7oxb7oPR2yKyvb8NY8td3I9m/SYeTZDgr5OPPnGPtdg7wJnhVtOd4RURiSI8cuH+5lN8IuB/ulD+v6dKtnY4wpvAO/N4fEIGNr4fPMc9eBotfF/CADsO9TrOoMZoTIaIyMy9cT8f9eGS5SVOtibkNwaKkxCjUJyEGIXiJMQoFCchRqE4CTGKPtnawxbGypK7980vPxSk5ZUNz6tX8cbxjxV7o+9hC6YM3H2O5hbwJuq5Nt7wHFVxOvwlxUppzrkLAUREfvDuPzqPj5VrdT7pwdh4gns7RcrdXum6f3fSw/2KRqCwQERkro3vy6MvvoSx58+PncfPlREOnQ7+Ye0GnjgelNjjilJ8HQPQS2qxgT9vrqp1LHLDNychRqE4CTEKxUmIUShOQoxCcRJiFIqTEKOoVoq2M7/dxVZKlrs/thLiz7u9sQZjH3+CLYzzCE9eLjz35OLlK9guefDw5zD27d//Kxj78AO8bjRSxhakJ87jR4fP4BrtP3U4w7FQcKq/67urYK7U8LkPjrElkgW4cmZ5Ccfy3F3pok2vTia4b9JI6YGUFdiemSV7MLYUuStuLjdxlcs0w1U6CL45CTEKxUmIUShOQoxCcRJiFIqTEKNQnIQYRbVStOm+3QU8JTnz3B+b+DFcU222YazTwQ2cnj7DE4PffuMV93kM8XiHestdFSEicrC3C2Objx/DWJbjcQE+6KE2OscTu1uXVmFsMMC2wlwTN/96+farzuO/uPcIrrn7aBvG3v7OH8NYFGPL4cmme7L44AL/Lq0JWTLBdsn6Mrboag3cwG5+3r2uDHHDsyzFjcYQfHMSYhSKkxCjUJyEGIXiJMQoFCchRqE4CTGKaqUUmZKWn8eNk0YTd+Onca5Myg7w/8TaNTzl+fF9XBkxGLstk2YDV8BcuwFDsvMYN7va2z+AsbfeegPGxmN3qr91+QpcM38ZN0N72sPWx2SKLaS44Z5f0l68Btd8o4Xvy/Gxe56IiMj2zj0YG03ctlN/gC0RNJVbRGSuxPdlvYktrqU2nhMUee5KnXSGK08aSrM8BN+chBiF4iTEKBQnIUahOAkxCsVJiFHUbO3FKc501ZTeLNPEnQXzCvx1noczuQvzeJzBY/8JjB313C31TwOctZxr4t5Id17FG/Cf7OCePzM8tUD65+6M+K1bt+CaWxs4pbxzgDfM37//PzB2euLejB5XcFa+28Qbx3fv46zx4SnuS+SB4ohAGYWhjfJYV5Kkay1cCFD18Sb2aeJ+fooC96aaZfjzEHxzEmIUipMQo1CchBiF4iTEKBQnIUahOAkximqlPNnENsXara/BWNV3WylFijcGh1Ulra3EWi2c6m+23X2J7tx5Ga75j3//CYyNB7hfUX1+CcY2d49g7NpV9yb8jZdfh2sqMb5t19fwpv5+zz1yQUTkwUN3AUFRYh9or483jp+D4gcRkSTHNtx5320tLa3gTfZPT3GBxvw1bH+dVvB5SIF/Wz9z/7YyxM/pVPk8BN+chBiF4iTEKBQnIUahOAkxCsVJiFEoTkKMolopn21iC2Dt1TdhrBB3NYin7cwvcFXK+YV7QrWISL/vngwtInJp/jXn8e9/77twzWu/cwfG3vuXH8KY5+GeM3NzeJLzlctui6DZ7sA1Qea+viIi8yv4lq5u4MnWg5rbBvj0Hu73czDEJR9lhMdrzK3gKqOFG27rI1BsirzE5/FFiUeKbB5iuycO8GdOEveU7bHyeGcFfj4QfHMSYhSKkxCjUJyEGIXiJMQoFCchRqE4CTGKaqU8HuDpvic5brhURu5Us5/i5lOlkmr20fhnEbm8iqtBfu/b7sqOaoRT6BvreAzCn/z5X8LYP//wxzB2coh/98HA3SwqSdwTnkVEYsE5+94ExzZ3cFWNpG6bpVzAFTzdJTyhuhBsjXkeboRVVN2fWXh4KvpMGfMxyPF3VSNl0nqIrZSR566CmUX4u8oC21gIvjkJMQrFSYhRKE5CjEJxEmIUipMQo1CchBhFt1L6WLs/+i88d+O19QXn8ZUYVwjUI6WaYgXPL1ldwNUPN66DplAlbrZ0oExkfvefsF1y97MHMIZmx4iIwEKdEl/7Msefl1fw9ch9nOoPxW2bZUq1TeZjq62qPVlKFUmSun936eM1oVKxEhR4Lk6ZYNspE7wuKtznGHj4nqUzTrYm5DcGipMQo1CchBiF4iTEKBQnIUZRs7VDMGVYROQ/7z6GsS+/co9x+N43vw7X3LiM2+ZvPXGPChAReeeNV2GsCjYiX6Q4A/nev/0Cxj59sA9j40xp7a9kE/3I/f9YKD2VfA9nGbWsZl7gDf9TkIGc5XiN5+HN3FNRNoGX+LeFIciEBvg9Uq/j5zQWfP45TshK7mFp5GBhNsP3JW518JcB+OYkxCgUJyFGoTgJMQrFSYhRKE5CjEJxEmIU1Uq5tLAIY70znA4/OOs7j39w7xFck8/WlTPBqfJFZeKxF7jtjY8+/l+45sfvfwhj0wL3zJEQWym+/+L/gfkUb24vFZulUOwSzcJAIw2iED8iXqCMGAjwPQuVdUHg/j5tgnmgXF+/xHZPrhQXFIoVhDyYlRVsB7baOIbgm5MQo1CchBiF4iTEKBQnIUahOAkxCsVJiFFUK0VLeUcRtg6yxJ1G335+DtdMRw9h7J3Xb8NYrbMKY4PEnfL+6X9/DNckJa4smGU4LV+p4MqTQuljMx67W/trBErFhKe1qsFOilSAheH5yiOixLwKtp1qNdx7KATWzUyp+LgY4UnfuWI7TTN8X+a67j5YIiLLq+5YU2mcNFGmsyP45iTEKBQnIUahOAkxCsVJiFEoTkKMQnESYhTVSikyXOGgjQsoAretkAq2Zo6GUxi7+wVurPX9MU6VX5Tu9PXeGU5rV5q4+iEb4/NPpvj863XFOgBjKLTP85RJ374yPkGrMCmBLVIq/9+RYh8NZ/jZSTNsfSCbRauo0SyRkTIKo9nBdklnEY8ASTP3Z37xCFddRUq1EIJvTkKMQnESYhSKkxCjUJyEGIXiJMQoFCchRlGtFFF29EuJ09dB4G6OVJQ4za9NXd4+wtbHu+/9BMb+4Dvfch7f2j+Ga8a51vRJsRWquKFVEONYHcwAiWvYpphcYCtCq94oFcshAhUVQYjvmfZdgVLRpM2BmYyHL7xG+65Odx7GLi3jiqaT0x6M9U8O3cef4pk+Nzc2YAzBNychRqE4CTEKxUmIUShOQoxCcRJiFIqTEKOoVsp8pwNjSYLtjdHEvWs/DnB1Rqak+X2lmdjPPvocxrb23dUsgxFu1NUbTmAMFCOIiEijoVSzKA2+KhX3bwsV+6VawxUOgVKxEkb4M3PwP50pFoanxMpSGfc+w9c/nbkvcq2KraWFS5dgrLuA7ZJUqayaxkqzror7OhYhtgNHCX6uEHxzEmIUipMQo1CchBiF4iTEKBQnIUZRs7VTJcNUUWQ9zd3ZuEiZdpwpQ5JLbXJxDWdJd8AGd1/ZzJ3NcAZSyygnSQJjI2VcAJp6jbK4IiKNGGcFa8qGed/H5x9X3d9Xq+Prm6Z44/tJD28cLwSvCyP39ei2G3DN8nwHxlZW8Mb3/gj3abron8HYcNB3Hu/M4+86OT6BMQTfnIQYheIkxCgUJyFGoTgJMQrFSYhRKE5CjKJbKRNsD1QCPEK5Dj61mGFrRpkiIIVgC6BQehkVYPxDliobtnP8u7SRAFpMm2yNrJSzM5zK7ynXsd3ElsOc0k+nDXoZVQVbM3mBrYjQUzbnV/DNnibuz6yE+L5o35WNB0oMn/+wfwpjBdicX61giytR+hwh+OYkxCgUJyFGoTgJMQrFSYhRKE5CjEJxEmIUT7MACCG/PvjmJMQoFCchRqE4CTEKxUmIUShOQoxCcRJilP8DKnTF0srourIAAAAASUVORK5CYII=\n",
+      "text/plain": [
+       "<Figure size 432x288 with 1 Axes>"
+      ]
+     },
+     "metadata": {
+      "needs_background": "light"
+     },
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/plain": [
+       "array([[3.92254496e-09, 1.20455460e-11, 2.66010169e-09, 9.99992609e-01,\n",
+       "        2.52212834e-10, 5.40860242e-07, 6.75951833e-06, 4.75118165e-12,\n",
+       "        6.90873403e-09, 1.07275378e-11]])"
+      ]
+     },
+     "execution_count": 14,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
    "source": [
     "show_image(data.X_test[0:1])\n",
     "svc.remote(payload=data.X_test[0:1])"
@@ -498,12 +572,36 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 15,
    "id": "03ef662f",
    "metadata": {},
-   "outputs": [],
+   "outputs": [
+    {
+     "data": {
+      "image/png": "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\n",
+      "text/plain": [
+       "<Figure size 432x288 with 1 Axes>"
+      ]
+     },
+     "metadata": {
+      "needs_background": "light"
+     },
+     "output_type": "display_data"
+    },
+    {
+     "data": {
+      "text/plain": [
+       "array([], dtype=float64)"
+      ]
+     },
+     "execution_count": 15,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
    "source": [
     "from src.utils import create_cifar10_outlier\n",
+    "\n",
     "outlier_img = create_cifar10_outlier(data)\n",
     "show_image(outlier_img)\n",
     "svc.remote(payload=outlier_img)"
@@ -511,7 +609,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 16,
    "id": "6c6ea7c7",
    "metadata": {},
    "outputs": [],
@@ -556,6 +654,7 @@
    "source": [
     "from tempo.examples.minio import create_minio_rclone\n",
     "import os\n",
+    "\n",
     "create_minio_rclone(os.getcwd()+\"/rclone-minio.conf\")"
    ]
   },
@@ -566,10 +665,9 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "from tempo.serve.loader import upload\n",
-    "upload(cifar10_model)\n",
-    "upload(outlier)\n",
-    "upload(svc)"
+    "tempo.upload(cifar10_model)\n",
+    "tempo.upload(outlier)\n",
+    "tempo.upload(svc)"
    ]
   },
   {
@@ -580,6 +678,7 @@
    "outputs": [],
    "source": [
     "from tempo.serve.metadata import RuntimeOptions, KubernetesOptions\n",
+    "\n",
     "runtime_options = RuntimeOptions(\n",
     "        k8s_options=KubernetesOptions(\n",
     "            namespace=\"production\",\n",
@@ -595,7 +694,8 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "from tempo.seldon.k8s import SeldonKubernetesRuntime\n",
+    "from tempo.seldon import SeldonKubernetesRuntime\n",
+    "\n",
     "k8s_runtime = SeldonKubernetesRuntime(runtime_options)\n",
     "k8s_runtime.deploy(svc)\n",
     "k8s_runtime.wait_ready(svc)"
@@ -609,6 +709,7 @@
    "outputs": [],
    "source": [
     "from src.utils import show_image\n",
+    "\n",
     "show_image(data.X_test[0:1])\n",
     "svc.remote(payload=data.X_test[0:1])"
    ]
@@ -621,6 +722,7 @@
    "outputs": [],
    "source": [
     "from src.utils import create_cifar10_outlier\n",
+    "\n",
     "outlier_img = create_cifar10_outlier(data)\n",
     "show_image(outlier_img)\n",
     "svc.remote(payload=outlier_img)"
@@ -654,9 +756,11 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "from tempo.seldon.k8s import SeldonKubernetesRuntime\n",
+    "from tempo.seldon import SeldonKubernetesRuntime\n",
+    "\n",
     "k8s_runtime = SeldonKubernetesRuntime(runtime_options)\n",
     "yaml_str = k8s_runtime.to_k8s_yaml(svc)\n",
+    "\n",
     "with open(os.getcwd()+\"/k8s/tempo.yaml\",\"w\") as f:\n",
     "    f.write(yaml_str)"
    ]
diff --git a/docs/examples/outlier/README.md b/docs/examples/outlier/README.md
index 51f25185..fc09a669 100644
--- a/docs/examples/outlier/README.md
+++ b/docs/examples/outlier/README.md
@@ -36,12 +36,15 @@ conda env create --name tempo-examples --file conda/tempo-examples.yaml
 
 ```python
 import os
-from tempo.utils import logger
 import logging
 import numpy as np
+import tempo
+
+from tempo.utils import logger
+from src.constants import ARTIFACTS_FOLDER
+
 logger.setLevel(logging.ERROR)
 logging.basicConfig(level=logging.ERROR)
-ARTIFACTS_FOLDER = os.getcwd()+"/artifacts"
 ```
 
 
@@ -75,10 +78,11 @@ else:
 
 ```python
 from src.tempo import create_outlier_cls, create_model, create_svc_cls
-cifar10_model = create_model(ARTIFACTS_FOLDER)
-OutlierModel = create_outlier_cls(ARTIFACTS_FOLDER)
+
+cifar10_model = create_model()
+OutlierModel = create_outlier_cls()
 outlier = OutlierModel()
-Cifar10Svc = create_svc_cls(outlier, cifar10_model, ARTIFACTS_FOLDER)
+Cifar10Svc = create_svc_cls(outlier, cifar10_model)
 svc = Cifar10Svc()
 ```
 
@@ -89,48 +93,35 @@ import json
 import os
 
 import numpy as np
-from src.constants import MODEL_FOLDER, OUTLIER_FOLDER
+from alibi_detect.base import NumpyEncoder
+from src.constants import ARTIFACTS_FOLDER, MODEL_FOLDER, OUTLIER_FOLDER
 
 from tempo.kfserving.protocol import KFServingV1Protocol, KFServingV2Protocol
 from tempo.serve.metadata import ModelFramework
 from tempo.serve.model import Model
 from tempo.serve.pipeline import PipelineModels
 from tempo.serve.utils import model, pipeline, predictmethod
-from alibi_detect.base import NumpyEncoder
 
 
-def create_outlier_cls(artifacts_folder: str):
+def create_outlier_cls():
     @model(
         name="outlier",
-        platform=ModelFramework.TempoPipeline,
+        platform=ModelFramework.Custom,
         protocol=KFServingV2Protocol(),
         uri="s3://tempo/outlier/cifar10/outlier",
-        local_folder=f"{artifacts_folder}/{OUTLIER_FOLDER}",
+        local_folder=os.path.join(ARTIFACTS_FOLDER, OUTLIER_FOLDER),
     )
     class OutlierModel(object):
-
         def __init__(self):
-            self.loaded = False
-
-        def load(self):
             from alibi_detect.utils.saving import load_detector
 
-            if "MLSERVER_MODELS_DIR" in os.environ:
-                models_folder = "/mnt/models"
-            else:
-                models_folder = f"{artifacts_folder}/{OUTLIER_FOLDER}"
+            model = self.get_tempo()
+            models_folder = model.details.local_folder
             print(f"Loading from {models_folder}")
-            self.od = load_detector(f"{models_folder}/cifar10")
-            self.loaded = True
-
-        def unload(self):
-            self.od = None
-            self.loaded = False
+            self.od = load_detector(os.path.join(models_folder, "cifar10"))
 
         @predictmethod
         def outlier(self, payload: np.ndarray) -> dict:
-            if not self.loaded:
-                self.load()
             od_preds = self.od.predict(
                 payload,
                 outlier_type="instance",  # use 'feature' or 'instance' level
@@ -144,25 +135,25 @@ def create_outlier_cls(artifacts_folder: str):
     return OutlierModel
 
 
-def create_model(arifacts_folder: str):
+def create_model():
 
     cifar10_model = Model(
         name="resnet32",
         protocol=KFServingV1Protocol(),
         platform=ModelFramework.Tensorflow,
         uri="gs://seldon-models/tfserving/cifar10/resnet32",
-        local_folder=f"{arifacts_folder}/{MODEL_FOLDER}",
+        local_folder=os.path.join(ARTIFACTS_FOLDER, MODEL_FOLDER),
     )
 
     return cifar10_model
 
 
-def create_svc_cls(outlier, model, arifacts_folder: str):
+def create_svc_cls(outlier, model):
     @pipeline(
         name="cifar10-service",
         protocol=KFServingV2Protocol(),
         uri="s3://tempo/outlier/cifar10/svc",
-        local_folder=f"{arifacts_folder}/svc",
+        local_folder=os.path.join(ARTIFACTS_FOLDER, "svc"),
         models=PipelineModels(outlier=outlier, cifar10=model),
     )
     class Cifar10Svc(object):
@@ -185,32 +176,33 @@ def create_svc_cls(outlier, model, arifacts_folder: str):
 
 ```python
 # %load tests/test_tempo.py
-from src.tempo import create_outlier_cls, create_model, create_svc_cls
 import numpy as np
+from src.tempo import create_model, create_outlier_cls, create_svc_cls
 
 
 def test_svc_outlier():
-    model = create_model("")
-    OutlierModel = create_outlier_cls("")
+    model = create_model()
+    OutlierModel = create_outlier_cls()
     outlier = OutlierModel()
-    Cifar10Svc = create_svc_cls(outlier, model, "")
+    Cifar10Svc = create_svc_cls(outlier, model)
     svc = Cifar10Svc()
-    svc.models.outlier = lambda payload: {"data":{"is_outlier":[1]}}
+    svc.models.outlier = lambda payload: {"data": {"is_outlier": [1]}}
     svc.models.cifar10 = lambda input: np.array([[0.2]])
     res = svc(np.array([1]))
     assert res.shape[0] == 0
 
 
 def test_svc_inlier():
-    model = create_model("")
-    OutlierModel = create_outlier_cls("")
+    model = create_model()
+    OutlierModel = create_outlier_cls()
     outlier = OutlierModel()
-    Cifar10Svc = create_svc_cls(outlier, model, "")
+    Cifar10Svc = create_svc_cls(outlier, model)
     svc = Cifar10Svc()
-    svc.models.outlier = lambda payload: {"data":{"is_outlier":[0]}}
+    svc.models.outlier = lambda payload: {"data": {"is_outlier": [0]}}
     svc.models.cifar10 = lambda input: np.array([[0.2]])
     res = svc(np.array([1]))
     assert res.shape[0] == 1
+
 ```
 
 
@@ -233,9 +225,8 @@ def test_svc_inlier():
 
 
 ```python
-from tempo.serve.loader import save
-save(outlier)
-save(svc)
+tempo.save(OutlierModel)
+tempo.save(Cifar10Svc)
 ```
 
 ## Test Locally on Docker
@@ -245,6 +236,7 @@ Here we test our models using production images but running locally on Docker. T
 
 ```python
 from tempo.seldon.docker import SeldonDockerRuntime
+
 docker_runtime = SeldonDockerRuntime()
 docker_runtime.deploy(svc)
 docker_runtime.wait_ready(svc)
@@ -253,6 +245,7 @@ docker_runtime.wait_ready(svc)
 
 ```python
 from src.utils import show_image
+
 show_image(data.X_test[0:1])
 svc(payload=data.X_test[0:1])
 ```
@@ -266,6 +259,7 @@ svc.remote(payload=data.X_test[0:1])
 
 ```python
 from src.utils import create_cifar10_outlier
+
 outlier_img = create_cifar10_outlier(data)
 show_image(outlier_img)
 svc.remote(payload=outlier_img)
@@ -297,20 +291,21 @@ docker_runtime.undeploy(svc)
 ```python
 from tempo.examples.minio import create_minio_rclone
 import os
+
 create_minio_rclone(os.getcwd()+"/rclone-minio.conf")
 ```
 
 
 ```python
-from tempo.serve.loader import upload
-upload(cifar10_model)
-upload(outlier)
-upload(svc)
+tempo.upload(cifar10_model)
+tempo.upload(outlier)
+tempo.upload(svc)
 ```
 
 
 ```python
 from tempo.serve.metadata import RuntimeOptions, KubernetesOptions
+
 runtime_options = RuntimeOptions(
         k8s_options=KubernetesOptions(
             namespace="production",
@@ -321,7 +316,8 @@ runtime_options = RuntimeOptions(
 
 
 ```python
-from tempo.seldon.k8s import SeldonKubernetesRuntime
+from tempo.seldon import SeldonKubernetesRuntime
+
 k8s_runtime = SeldonKubernetesRuntime(runtime_options)
 k8s_runtime.deploy(svc)
 k8s_runtime.wait_ready(svc)
@@ -330,6 +326,7 @@ k8s_runtime.wait_ready(svc)
 
 ```python
 from src.utils import show_image
+
 show_image(data.X_test[0:1])
 svc.remote(payload=data.X_test[0:1])
 ```
@@ -337,6 +334,7 @@ svc.remote(payload=data.X_test[0:1])
 
 ```python
 from src.utils import create_cifar10_outlier
+
 outlier_img = create_cifar10_outlier(data)
 show_image(outlier_img)
 svc.remote(payload=outlier_img)
@@ -354,9 +352,11 @@ k8s_runtime.undeploy(svc)
 
 
 ```python
-from tempo.seldon.k8s import SeldonKubernetesRuntime
+from tempo.seldon import SeldonKubernetesRuntime
+
 k8s_runtime = SeldonKubernetesRuntime(runtime_options)
 yaml_str = k8s_runtime.to_k8s_yaml(svc)
+
 with open(os.getcwd()+"/k8s/tempo.yaml","w") as f:
     f.write(yaml_str)
 ```
diff --git a/docs/examples/outlier/src/constants.py b/docs/examples/outlier/src/constants.py
index 9b200654..ab42b074 100644
--- a/docs/examples/outlier/src/constants.py
+++ b/docs/examples/outlier/src/constants.py
@@ -1,2 +1,7 @@
+import os
+
 MODEL_FOLDER = "model"
 OUTLIER_FOLDER = "outlier"
+
+ROOT_FOLDER = os.path.dirname(os.path.dirname(__file__))
+ARTIFACTS_FOLDER = os.path.join(ROOT_FOLDER, "artifacts")
diff --git a/docs/examples/outlier/src/tempo.py b/docs/examples/outlier/src/tempo.py
index fb569323..336094f0 100644
--- a/docs/examples/outlier/src/tempo.py
+++ b/docs/examples/outlier/src/tempo.py
@@ -3,7 +3,7 @@
 
 import numpy as np
 from alibi_detect.base import NumpyEncoder
-from src.constants import MODEL_FOLDER, OUTLIER_FOLDER
+from src.constants import ARTIFACTS_FOLDER, MODEL_FOLDER, OUTLIER_FOLDER
 
 from tempo.kfserving.protocol import KFServingV1Protocol, KFServingV2Protocol
 from tempo.serve.metadata import ModelFramework
@@ -12,37 +12,25 @@
 from tempo.serve.utils import model, pipeline, predictmethod
 
 
-def create_outlier_cls(artifacts_folder: str):
+def create_outlier_cls():
     @model(
         name="outlier",
-        platform=ModelFramework.TempoPipeline,
+        platform=ModelFramework.Custom,
         protocol=KFServingV2Protocol(),
         uri="s3://tempo/outlier/cifar10/outlier",
-        local_folder=f"{artifacts_folder}/{OUTLIER_FOLDER}",
+        local_folder=os.path.join(ARTIFACTS_FOLDER, OUTLIER_FOLDER),
     )
     class OutlierModel(object):
         def __init__(self):
-            self.loaded = False
-
-        def load(self):
             from alibi_detect.utils.saving import load_detector
 
-            if "MLSERVER_MODELS_DIR" in os.environ:
-                models_folder = "/mnt/models"
-            else:
-                models_folder = f"{artifacts_folder}/{OUTLIER_FOLDER}"
+            model = self.get_tempo()
+            models_folder = model.details.local_folder
             print(f"Loading from {models_folder}")
-            self.od = load_detector(f"{models_folder}/cifar10")
-            self.loaded = True
-
-        def unload(self):
-            self.od = None
-            self.loaded = False
+            self.od = load_detector(os.path.join(models_folder, "cifar10"))
 
         @predictmethod
         def outlier(self, payload: np.ndarray) -> dict:
-            if not self.loaded:
-                self.load()
             od_preds = self.od.predict(
                 payload,
                 outlier_type="instance",  # use 'feature' or 'instance' level
@@ -56,25 +44,25 @@ def outlier(self, payload: np.ndarray) -> dict:
     return OutlierModel
 
 
-def create_model(arifacts_folder: str):
+def create_model():
 
     cifar10_model = Model(
         name="resnet32",
         protocol=KFServingV1Protocol(),
         platform=ModelFramework.Tensorflow,
         uri="gs://seldon-models/tfserving/cifar10/resnet32",
-        local_folder=f"{arifacts_folder}/{MODEL_FOLDER}",
+        local_folder=os.path.join(ARTIFACTS_FOLDER, MODEL_FOLDER),
     )
 
     return cifar10_model
 
 
-def create_svc_cls(outlier, model, arifacts_folder: str):
+def create_svc_cls(outlier, model):
     @pipeline(
         name="cifar10-service",
         protocol=KFServingV2Protocol(),
         uri="s3://tempo/outlier/cifar10/svc",
-        local_folder=f"{arifacts_folder}/svc",
+        local_folder=os.path.join(ARTIFACTS_FOLDER, "svc"),
         models=PipelineModels(outlier=outlier, cifar10=model),
     )
     class Cifar10Svc(object):
diff --git a/requirements-dev.txt b/requirements-dev.txt
index f8366ee5..fd68100f 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -7,6 +7,7 @@ mypy==0.800
 # Testing
 pytest==6.2.0
 pytest-asyncio==0.14.0
+pytest-cases==3.4.6
 tox==3.22.0
 
 # Pushing to PyPi
diff --git a/setup.py b/setup.py
index 9cfa03b6..39635fa3 100644
--- a/setup.py
+++ b/setup.py
@@ -48,7 +48,7 @@ def _load_description() -> str:
         "packaging",
         "requests",
         "pydantic",
-        "dill",
+        "cloudpickle",
         "python-rclone",
         "seldon-deploy-sdk",
         "conda-pack",
diff --git a/tempo/__init__.py b/tempo/__init__.py
index 1399de68..66f40053 100644
--- a/tempo/__init__.py
+++ b/tempo/__init__.py
@@ -1,3 +1,4 @@
+from .serve.loader import save, upload
 from .serve.metadata import ModelFramework
 from .serve.model import Model
 from .serve.pipeline import Pipeline
@@ -12,4 +13,6 @@
     "pipeline",
     "predictmethod",
     "model",
+    "save",
+    "upload",
 ]
diff --git a/tempo/mlserver.py b/tempo/mlserver.py
index 80dbbcd5..402a6f87 100644
--- a/tempo/mlserver.py
+++ b/tempo/mlserver.py
@@ -12,8 +12,12 @@
 from .serve.utils import PredictMethodAttr
 
 
-def _is_class(model: BaseModel) -> bool:
-    return hasattr(model._user_func, PredictMethodAttr)
+def _needs_init(model: BaseModel):
+    is_class = model._K is not None
+    has_annotation = hasattr(model._user_func, PredictMethodAttr)
+    is_bound = hasattr(model._user_func, "__self__")
+
+    return is_class and has_annotation and not is_bound
 
 
 class InferenceRuntime(MLModel):
@@ -34,9 +38,11 @@ async def _load_model(self) -> BaseModel:
             # If pipeline, call children models remotely
             model.set_remote(True)
 
-        if _is_class(model):
-            # TODO: Call __init__()
-            pass
+        if _needs_init(model):
+            instance = model._K()
+            # Make sure that the model is the instance's model (and not the
+            # class attribute)
+            model = instance.get_tempo()
 
         if model._load_func:
             model._load_func()
diff --git a/tempo/serve/base.py b/tempo/serve/base.py
index 052d93e9..cdfd1dd4 100644
--- a/tempo/serve/base.py
+++ b/tempo/serve/base.py
@@ -4,7 +4,7 @@
 import tempfile
 from pydoc import locate
 from types import SimpleNamespace
-from typing import Any, Dict, Optional, Tuple
+from typing import Any, Dict, Optional, Tuple, Type
 
 import numpy as np
 
@@ -69,6 +69,9 @@ def __init__(
         self.use_remote: bool = False
         self.runtime_options_override: Optional[RuntimeOptions] = None
 
+        # K holds the wrapped class (if any)
+        self._K: Optional[Type] = None
+
         # context represents internal context shared (optionally) between different
         # methods of the model (e.g. predict, loader, etc.)
         self.context = SimpleNamespace()
@@ -132,12 +135,6 @@ def save(self, save_env=True):
             return
 
         file_path_pkl = os.path.join(self.details.local_folder, DefaultModelFilename)
-        # TODO: Is it necessary to change the `deployed` flag here?
-        #  if not self.deployed:
-        #  self.deployed = True
-        #  save_custom(self, file_path_pkl)
-        #  self.deployed = False
-        #  else:
         logger.info("Saving tempo model to %s", file_path_pkl)
         save_custom(self, file_path_pkl)
 
diff --git a/tempo/serve/loader/artifact.py b/tempo/serve/loader/artifact.py
index 471cdf6a..5525f0a4 100644
--- a/tempo/serve/loader/artifact.py
+++ b/tempo/serve/loader/artifact.py
@@ -1,76 +1,19 @@
 import os
-from types import FunctionType
 from typing import Any
 
-import dill
+import cloudpickle
 
 from ..constants import DefaultModelFilename, DefaultRemoteFilename
 
 
-def _fixed_create_function(
-    fcode,
-    fglobals,
-    fname=None,
-    fdefaults=None,
-    fclosure=None,
-    fdict=None,
-    fkwdefaults=None,
-):
-    """
-    Patched version of `dill`'s `_create_function`, to make sure __builtins__
-    is serialised.
-
-    Patched version from:
-        https://github.com/uqfoundation/dill/issues/219#issuecomment-522068603
-    Original source code can be found at:
-        https://github.com/uqfoundation/dill/blob/846b888ef957dc1991464b562df425aa35142cb8/dill/_dill.py#L594-L603
-
-    """
-    # same as FunctionType, but enable passing __dict__ to new function,
-    # __dict__ is the storehouse for attributes added after function creation
-    if fdict is None:
-        fdict = dict()
-    func = FunctionType(fcode, fglobals or dict(), fname, fdefaults, fclosure)
-    func.__dict__.update(fdict)  # XXX: better copy? option to copy?
-    if fkwdefaults is not None:
-        func.__kwdefaults__ = fkwdefaults
-
-    # THE WORKAROUND:
-    # if the function was serialized without recurse, fglobals would actually contain
-    # __builtins__, but because of recurse only the referenced modules/objects
-    # end up in fglobals and we are missing the important __builtins__
-    if "__builtins__" not in func.__globals__:
-        func.__globals__["__builtins__"] = globals()["__builtins__"]
-
-    return func
-
-
-def _patch_dill(f):
-    def _patched(*args, **kwargs):
-        _dill = dill._dill
-        # Save original before patching
-        original_create_function = _dill._create_function
-        _dill._create_function = _fixed_create_function
-
-        ret = f(*args, **kwargs)
-
-        _dill._create_function = original_create_function
-
-        return ret
-
-    return _patched
-
-
-@_patch_dill
 def save(tempo_artifact: Any, save_env=True):
     model = tempo_artifact.get_tempo()
     model.save(save_env=save_env)
 
 
-@_patch_dill
 def save_custom(pipeline, file_path: str) -> str:
     with open(file_path, "wb") as file:
-        dill.dump(pipeline, file, recurse=True)
+        cloudpickle.dump(pipeline, file)
 
     return file_path
 
@@ -82,10 +25,10 @@ def load(folder: str):
 
 def load_custom(file_path: str):
     with open(file_path, "rb") as file:
-        return dill.load(file)
+        return cloudpickle.load(file)
 
 
 def load_remote(folder: str):
     file_path = os.path.join(folder, DefaultRemoteFilename)
     with open(file_path, "rb") as file:
-        return dill.load(file)
+        return cloudpickle.load(file)
diff --git a/tempo/serve/utils.py b/tempo/serve/utils.py
index be090100..98fbcdb4 100644
--- a/tempo/serve/utils.py
+++ b/tempo/serve/utils.py
@@ -1,18 +1,24 @@
-import inspect
-from inspect import getmembers, isfunction
+import copy
+from inspect import getmembers, isclass, isfunction
 from typing import Any, Callable, Optional, Type
 
-from tempo.kfserving.protocol import KFServingV2Protocol
-from tempo.serve.metadata import ModelFramework, RuntimeOptions
-from tempo.serve.model import Model
-from tempo.serve.pipeline import Pipeline, PipelineModels
-from tempo.serve.protocol import Protocol
-from tempo.serve.types import ModelDataType
+from ..kfserving.protocol import KFServingV2Protocol
+from .base import BaseModel
+from .metadata import ModelFramework, RuntimeOptions
+from .model import Model
+from .pipeline import Pipeline, PipelineModels
+from .protocol import Protocol
+from .types import ModelDataType
 
 PredictMethodAttr = "_tempo_predict"
 LoadMethodAttr = "_tempo_load"
 
 
+def predictmethod(f):
+    setattr(f, PredictMethodAttr, True)
+    return f
+
+
 def _bind(instance, func):
     """
     Bind the function *func* to *instance*, with either provided name *as_name*
@@ -32,6 +38,51 @@ def _get_predict_method(K: Type) -> Optional[Callable]:
     return None
 
 
+def _wrap_class(K: Type, model: BaseModel, field_name: str = "model") -> Type:
+    setattr(K, field_name, model)
+    model._K = K
+
+    _bind_tempo_interface(K, model)
+
+    orig_init = K.__init__
+
+    # Make copy of original __init__, so we can call it without recursion
+    def __init__(self, *args, **kws):
+        # On __init__, copy pipeline object and update _user_func.
+        # Class-level attributes mutate to instance-level attributes when
+        # overriden.
+        # Therefore, this won't modify the class-level model / pipeline object.
+        class_model = getattr(K, field_name)
+        instance_model = copy.copy(class_model)
+        setattr(self, field_name, instance_model)
+
+        # We bind _user_func so that `self` is passed implicitly
+        instance_model._user_func = _bind(self, instance_model._user_func)
+
+        # Bind back Tempo interface to make sure it points to instance referece
+        _bind_tempo_interface(self, instance_model)
+
+        orig_init(self, *args, **kws)  # Call the original __init__
+
+    K.__init__ = __init__  # Set the class' __init__ to the new one
+
+    def __call__(self, *args, **kwargs) -> Any:
+        model = getattr(self, field_name)
+        return model(*args, **kwargs)
+
+    K.__call__ = __call__  # type: ignore
+
+    return K
+
+
+def _bind_tempo_interface(artifact: Any, model: BaseModel) -> Any:
+    setattr(artifact, "request", model.request)
+    setattr(artifact, "remote", model.remote)
+    setattr(artifact, "get_tempo", model.get_tempo)
+
+    return artifact
+
+
 def pipeline(
     name: str,
     protocol: Protocol = KFServingV2Protocol(),
@@ -75,40 +126,25 @@ def pipeline(
     """
 
     def _pipeline(f):
-        if inspect.isclass(f):
-            K = f
-            predict_method = _get_predict_method(K)
-
-            K.pipeline = Pipeline(
-                name,
-                local_folder=local_folder,
-                uri=uri,
-                models=models,
-                inputs=inputs,
-                outputs=outputs,
-                pipeline_func=predict_method,
-                conda_env=conda_env,
-                protocol=protocol,
-                runtime_options=runtime_options,
-            )
-            setattr(K, "request", K.pipeline.request)
-            setattr(K, "remote", K.pipeline.remote)
-            setattr(K, "get_tempo", K.pipeline.get_tempo)
-
-            orig_init = K.__init__
-
-            # Make copy of original __init__, so we can call it without recursion
-            def __init__(self, *args, **kws):
-                # We bind _user_func so that `self` is passed implicitly
-                K.pipeline._user_func = _bind(self, K.pipeline._user_func)
-                orig_init(self, *args, **kws)  # Call the original __init__
-
-            K.__init__ = __init__  # Set the class' __init__ to the new one
-
-            def __call__(self, *args, **kwargs) -> Any:
-                return self.pipeline(*args, **kwargs)
-
-            K.__call__ = __call__
+        predict_method = f
+        if isclass(f):
+            predict_method = _get_predict_method(f)
+
+        pipeline = Pipeline(
+            name,
+            local_folder=local_folder,
+            uri=uri,
+            models=models,
+            inputs=inputs,
+            outputs=outputs,
+            pipeline_func=predict_method,
+            conda_env=conda_env,
+            protocol=protocol,
+            runtime_options=runtime_options,
+        )
+
+        if isclass(f):
+            K = _wrap_class(f, pipeline, field_name="pipeline")
 
             @property
             def models_property(self):
@@ -119,26 +155,10 @@ def models_property(self):
             K.models = models_property
 
             return K
-        else:
-            return Pipeline(
-                name,
-                local_folder=local_folder,
-                uri=uri,
-                models=models,
-                inputs=inputs,
-                outputs=outputs,
-                pipeline_func=f,
-                conda_env=conda_env,
-                protocol=protocol,
-                runtime_options=runtime_options,
-            )
-
-    return _pipeline
 
+        return pipeline
 
-def predictmethod(f):
-    setattr(f, PredictMethodAttr, True)
-    return f
+    return _pipeline
 
 
 def model(
@@ -183,55 +203,26 @@ def model(
     """
 
     def _model(f):
-        if inspect.isclass(f):
-            K = f
-            predict_method = _get_predict_method(K)
-
-            K.pipeline = Model(
-                name,
-                protocol=protocol,
-                local_folder=local_folder,
-                uri=uri,
-                platform=platform,
-                inputs=inputs,
-                outputs=outputs,
-                model_func=predict_method,
-                conda_env=conda_env,
-                runtime_options=runtime_options,
-            )
-
-            setattr(K, "request", K.pipeline.request)
-            setattr(K, "remote", K.pipeline.remote)
-            setattr(K, "get_tempo", K.pipeline.get_tempo)
-
-            orig_init = K.__init__
-
-            # Make copy of original __init__, so we can call it without recursion
-            def __init__(self, *args, **kws):
-                # We bind _user_func so that `self` is passed implicitly
-                K.pipeline._user_func = _bind(self, K.pipeline._user_func)
-                orig_init(self, *args, **kws)  # Call the original __init__
-
-            K.__init__ = __init__  # Set the class' __init__ to the new one
-
-            def __call__(self, *args, **kwargs) -> Any:
-                return self.pipeline(*args, **kwargs)
-
-            K.__call__ = __call__
-
-            return K
-        else:
-            return Model(
-                name,
-                protocol=protocol,
-                local_folder=local_folder,
-                uri=uri,
-                platform=platform,
-                inputs=inputs,
-                outputs=outputs,
-                model_func=f,
-                conda_env=conda_env,
-                runtime_options=runtime_options,
-            )
+        predict_method = f
+        if isclass(f):
+            predict_method = _get_predict_method(f)
+
+        model = Model(
+            name,
+            protocol=protocol,
+            local_folder=local_folder,
+            uri=uri,
+            platform=platform,
+            inputs=inputs,
+            outputs=outputs,
+            model_func=predict_method,
+            conda_env=conda_env,
+            runtime_options=runtime_options,
+        )
+
+        if isclass(f):
+            return _wrap_class(f, model)
+
+        return model
 
     return _model


Test patch:
diff --git a/docs/examples/outlier/tests/test_tempo.py b/docs/examples/outlier/tests/test_tempo.py
index 9cb80f7f..12d90e9b 100644
--- a/docs/examples/outlier/tests/test_tempo.py
+++ b/docs/examples/outlier/tests/test_tempo.py
@@ -3,10 +3,10 @@
 
 
 def test_svc_outlier():
-    model = create_model("")
-    OutlierModel = create_outlier_cls("")
+    model = create_model()
+    OutlierModel = create_outlier_cls()
     outlier = OutlierModel()
-    Cifar10Svc = create_svc_cls(outlier, model, "")
+    Cifar10Svc = create_svc_cls(outlier, model)
     svc = Cifar10Svc()
     svc.models.outlier = lambda payload: {"data": {"is_outlier": [1]}}
     svc.models.cifar10 = lambda input: np.array([[0.2]])
@@ -15,10 +15,10 @@ def test_svc_outlier():
 
 
 def test_svc_inlier():
-    model = create_model("")
-    OutlierModel = create_outlier_cls("")
+    model = create_model()
+    OutlierModel = create_outlier_cls()
     outlier = OutlierModel()
-    Cifar10Svc = create_svc_cls(outlier, model, "")
+    Cifar10Svc = create_svc_cls(outlier, model)
     svc = Cifar10Svc()
     svc.models.outlier = lambda payload: {"data": {"is_outlier": [0]}}
     svc.models.cifar10 = lambda input: np.array([[0.2]])
diff --git a/tests/serve/test_utils.py b/tests/serve/test_utils.py
index a9c75031..247ac895 100644
--- a/tests/serve/test_utils.py
+++ b/tests/serve/test_utils.py
@@ -1,3 +1,5 @@
+import numpy as np
+
 from tempo.serve.utils import _get_predict_method
 
 
@@ -12,3 +14,34 @@ def test_bind_init(inference_pipeline_class):
 
     assert user_func.__self__ == inference_pipeline_class
     assert user_func == inference_pipeline_class.p
+
+
+def test_K_reference(inference_pipeline_class):
+    MyClass = inference_pipeline_class.__class__
+
+    assert inference_pipeline_class.pipeline._K == MyClass
+
+
+def test_multiple_instances(inference_pipeline_class):
+    MyClass = inference_pipeline_class.__class__
+    inference_pipeline_class_2 = MyClass()
+
+    payload = np.array([0, 1, 2, 3])
+
+    self_1 = inference_pipeline_class.pipeline._user_func.__self__
+    self_2 = inference_pipeline_class_2.pipeline._user_func.__self__
+    assert self_1 != self_2
+    assert self_1 == inference_pipeline_class
+    assert self_2 == inference_pipeline_class_2
+
+    inference_pipeline_class(payload=payload)
+    assert inference_pipeline_class.counter == 1
+    assert inference_pipeline_class_2.counter == 0
+
+    inference_pipeline_class_2(payload=payload)
+    assert inference_pipeline_class.counter == 1
+    assert inference_pipeline_class_2.counter == 1
+
+    inference_pipeline_class(payload=payload)
+    assert inference_pipeline_class.counter == 2
+    assert inference_pipeline_class_2.counter == 1
diff --git a/tests/test_mlserver.py b/tests/test_mlserver.py
index aa39568e..7a3803e2 100644
--- a/tests/test_mlserver.py
+++ b/tests/test_mlserver.py
@@ -1,30 +1,24 @@
+import copy
+
 import pytest
-from mlserver.settings import ModelParameters, ModelSettings
+from mlserver.settings import ModelSettings
 from mlserver.types import InferenceRequest, RequestInput
 from mlserver.utils import to_ndarray
+from pytest_cases import fixture, parametrize_with_cases
 
-from tempo import Model
 from tempo.mlserver import InferenceRuntime
-from tempo.serve.loader import save
-
-
-@pytest.fixture
-def model_settings(custom_model: Model) -> ModelSettings:
-    save(custom_model, save_env=False)
-    model_uri = custom_model.details.local_folder
+from tempo.serve.base import BaseModel
 
-    return ModelSettings(
-        name="sum-model",
-        parameters=ModelParameters(uri=model_uri),
-    )
+from .test_mlserver_cases import case_wrapped_class
 
 
 @pytest.fixture
 def inference_request() -> InferenceRequest:
-    return InferenceRequest(inputs=[RequestInput(name="input-0", shape=[4], data=[1, 2, 3, 4], datatype="FP32")])
+    return InferenceRequest(inputs=[RequestInput(name="payload", shape=[4], data=[1, 2, 3, 4], datatype="FP32")])
 
 
-@pytest.fixture
+@fixture
+@parametrize_with_cases("model_settings")
 async def mlserver_runtime(model_settings: ModelSettings) -> InferenceRuntime:
     _runtime = InferenceRuntime(model_settings)
     await _runtime.load()
@@ -32,24 +26,42 @@ async def mlserver_runtime(model_settings: ModelSettings) -> InferenceRuntime:
     return _runtime
 
 
-def test_load(mlserver_runtime: InferenceRuntime):
+async def test_load(mlserver_runtime: InferenceRuntime):
+    # NOTE: pytest-cases doesn't wait for async fixtures
+    # TODO: Raise issue in pytest-cases repo
+    mlserver_runtime = await mlserver_runtime
     assert mlserver_runtime.ready
-    assert isinstance(mlserver_runtime._model, Model)
+    assert isinstance(mlserver_runtime._model, BaseModel)
 
 
-async def test_predict(
-    mlserver_runtime: InferenceRuntime,
-    inference_request: InferenceRequest,
-    custom_model: Model,
-):
+async def test_predict(mlserver_runtime: InferenceRuntime, inference_request: InferenceRequest):
+    # NOTE: pytest-cases doesn't wait for async fixtures
+    # TODO: Raise issue in pytest-cases repo
+    mlserver_runtime = await mlserver_runtime
     res = await mlserver_runtime.predict(inference_request)
 
     assert len(res.outputs) == 1
 
     pipeline_input = to_ndarray(inference_request.inputs[0])
-    custom_model.get_tempo().set_remote(False)  # ensure direct call to class does not try to do remote
-    expected_output = custom_model(pipeline_input)
+    custom_model = copy.copy(mlserver_runtime._model)
+    # Ensure direct call to class does not try to do remote
+    custom_model.set_remote(False)
+    expected_output = custom_model(payload=pipeline_input)
 
     pipeline_output = res.outputs[0].data
 
     assert expected_output.tolist() == pipeline_output
+
+
+async def test_load_wrapped_class(inference_pipeline_class, inference_request: InferenceRequest):
+    pipeline_input = to_ndarray(inference_request.inputs[0])
+
+    inference_pipeline_class(pipeline_input)
+    assert inference_pipeline_class.counter == 1
+
+    model_settings = case_wrapped_class(inference_pipeline_class)
+    runtime = InferenceRuntime(model_settings)
+    await runtime.load()
+
+    assert inference_pipeline_class.counter == 1
+    assert runtime._model._user_func.__self__.counter == 0  # type: ignore
diff --git a/tests/test_mlserver_cases.py b/tests/test_mlserver_cases.py
new file mode 100644
index 00000000..cdb3e2e2
--- /dev/null
+++ b/tests/test_mlserver_cases.py
@@ -0,0 +1,36 @@
+from mlserver.settings import ModelParameters, ModelSettings
+
+from tempo import Model
+from tempo.serve.loader import save
+
+
+def case_custom_model(custom_model: Model) -> ModelSettings:
+    save(custom_model, save_env=False)
+    model_uri = custom_model.details.local_folder
+
+    return ModelSettings(
+        name="custom-model",
+        parameters=ModelParameters(uri=model_uri),
+    )
+
+
+def case_wrapped_class_instance(inference_pipeline_class) -> ModelSettings:
+    save(inference_pipeline_class, save_env=False)
+    model_uri = inference_pipeline_class.pipeline.details.local_folder
+
+    return ModelSettings(
+        name="wrapped-class-instance",
+        parameters=ModelParameters(uri=model_uri),
+    )
+
+
+def case_wrapped_class(inference_pipeline_class) -> ModelSettings:
+    MyClass = inference_pipeline_class.__class__
+
+    save(MyClass, save_env=False)
+    model_uri = MyClass.pipeline.details.local_folder
+
+    return ModelSettings(
+        name="wrapped-class",
+        parameters=ModelParameters(uri=model_uri),
+    )