Repository: Xilinx/inference-server (language: python)

Issue:
Implement missing C++ methods in Python
Some methods available and used in C++ are not bound to the Python API


Discussion / hints:

Fix patch:
diff --git a/docs/example_resnet50_python.rst b/docs/example_resnet50_python.rst
index eadabb792..01c0cb134 100644
--- a/docs/example_resnet50_python.rst
+++ b/docs/example_resnet50_python.rst
@@ -51,7 +51,7 @@ Creating the ``proteus.servers.Server`` object starts the inference server backe
     :start-after: +initialize
     :end-before: -initialize
     :language: py
-    :dedent: 4
+    :dedent: 8
 
 Depending on what protocol you want to use to communicate with the server, you may need to start it explicitly using one of the object's methods.
 
@@ -61,7 +61,7 @@ For example, if you were using HTTP/REST:
     :start-after: +start protocol
     :end-before: -start protocol
     :language: py
-    :dedent: 4
+    :dedent: 8
 
 If the server is already running somewhere, you don't need to do this.
 
diff --git a/examples/bert/migraphx.py b/examples/bert/migraphx.py
index 70159845b..1287c65fa 100644
--- a/examples/bert/migraphx.py
+++ b/examples/bert/migraphx.py
@@ -75,13 +75,23 @@ def load(client, args):
     you should use for subsequent requests
 
     Args:
-        client (proteus.client.Client): the client object
+        client (proteus.Client): the client object
         args (argparse.Namespace): the command line arguments
 
     Returns:
         str: endpoint
     """
 
+    # Depending on how the server is compiled, it may or may not have support
+    # for a particular backend. This guard checks to make sure the server does
+    # support the requested backend. If you already know it's supported, you can
+    # skip this check.
+    if not proteus.serverHasExtension(client, "migraphx"):
+        print(
+            "MIGraphX is not enabled. Please recompile with it enabled to run this example"
+        )
+        sys.exit(0)
+
     # The only parameter the migraphx worker requires is the model file name.
     # batch and timeout are optional.
     # It will take the file name stem and search for either a *.onnx or *.mxr extension, and if
@@ -97,9 +107,7 @@ def load(client, args):
     endpoint = client.workerLoad("migraphx", parameters)
 
     # wait for the worker to load and compile model
-    ready = False
-    while not ready:
-        ready = client.modelReady(endpoint)
+    proteus.waitUntilModelReady(client, endpoint)
 
     return endpoint
 
@@ -110,14 +118,14 @@ def construct_requests(eval_examples, input_ids, input_mask, segment_ids, batch_
     requests = []
     for idx in range(0, n):
         # Create an InferenceRequest
-        request = proteus.predict_api.InferenceRequest()
+        request = proteus.InferenceRequest()
         item = eval_examples[idx]  # class SquadExample
 
         # Depending on the model, it will require one or more input tensors
         # The values for name, datatype, shape will also be model-dependent
         # For MIGraphX, the names for each input tensor are meaningful
 
-        input_n = proteus.predict_api.InferenceRequestInput()
+        input_n = proteus.InferenceRequestInput()
         input_n.name = f"input_ids:0"
         input_n.datatype = proteus.DataType.INT64
         input_n.shape = (
@@ -127,7 +135,7 @@ def construct_requests(eval_examples, input_ids, input_mask, segment_ids, batch_
         input_n.setInt64Data(input_ids[idx : idx + batch_size])
         request.addInputTensor(input_n)
 
-        input_n = proteus.predict_api.InferenceRequestInput()
+        input_n = proteus.InferenceRequestInput()
         input_n.name = f"input_mask:0"
         input_n.datatype = proteus.DataType.INT64
         input_n.shape = (
@@ -137,7 +145,7 @@ def construct_requests(eval_examples, input_ids, input_mask, segment_ids, batch_
         input_n.setInt64Data(input_mask[idx : idx + batch_size])
         request.addInputTensor(input_n)
 
-        input_n = proteus.predict_api.InferenceRequestInput()
+        input_n = proteus.InferenceRequestInput()
         input_n.name = f"segment_ids:0"
         input_n.datatype = proteus.DataType.INT64
         input_n.shape = (
@@ -150,7 +158,7 @@ def construct_requests(eval_examples, input_ids, input_mask, segment_ids, batch_
         # This comes from the first argument; I think it's supposed to be output names
         #       see https://onnxruntime.ai/docs/api/python/api_summary.html#load-and-run-a-model
         # item = eval_examples[idx]   # class SquadExample
-        input_n = proteus.predict_api.InferenceRequestInput()
+        input_n = proteus.InferenceRequestInput()
         input_n.name = f"unique_ids_raw_output___9:0"
         input_n.datatype = proteus.DataType.INT64
         input_n.shape = (1,)
@@ -191,14 +199,13 @@ def main(args):
     print("Running the MIGraphX example for Bert in Python")
 
     # connect to the server
-    client = proteus.clients.HttpClient(f"http://127.0.0.1:{args.http_port}")
+    client = proteus.HttpClient(f"http://127.0.0.1:{args.http_port}")
     print("Waiting for server...", end="")
     start_server = not client.serverLive()
     if start_server:
-        server = proteus.servers.Server()
+        server = proteus.Server()
         server.startHttp(args.http_port)
-        while not client.serverLive():
-            time.sleep(1)
+    proteus.waitUntilServerReady(client)
     print("OK. Connected.")
 
     print("Loading worker...")
@@ -225,7 +232,7 @@ def main(args):
     )
 
     print(f"Sending {len(requests)} request(s)...")
-    responses = proteus.client_operators.inferAsyncOrdered(client, endpoint, requests)
+    responses = proteus.inferAsyncOrdered(client, endpoint, requests)
     print("Client received inference reply")
 
     print("Postprocessing...")
diff --git a/examples/hello_world/echo.py b/examples/hello_world/echo.py
index 87ec68173..33489952a 100644
--- a/examples/hello_world/echo.py
+++ b/examples/hello_world/echo.py
@@ -40,9 +40,9 @@ def make_request(data):
     Returns:
         proteus.InferenceRequest: Request
     """
-    request = proteus.predict_api.InferenceRequest()
+    request = proteus.InferenceRequest()
     # each request has one or more input tensors, depending on the worker/model that's going to process it
-    input_0 = proteus.predict_api.InferenceRequestInput()
+    input_0 = proteus.InferenceRequestInput()
     input_0.name = f"input0"
     input_0.setUint32Data(np.array([data], np.uint32))
     input_0.datatype = proteus.DataType.UINT32
@@ -56,24 +56,20 @@ def make_request(data):
 
 def main():
     # +create objects
-    client = proteus.clients.HttpClient("http://127.0.0.1:8998")
+    client = proteus.HttpClient("http://127.0.0.1:8998")
     # -create objects
 
     # +start server: if it's not already started, start it from Python
     start_server = not client.serverLive()
     if start_server:
-        server = proteus.servers.Server()
+        server = proteus.Server()
         server.startHttp(8998)
-        while not client.serverLive():
-            sleep(1)
+    proteus.waitUntilServerReady(client)
     # -start server:
 
     # +load worker: load the Echo worker which accepts a number, adds 1, and returns the sum
     endpoint = client.workerLoad("echo")
-
-    ready = False
-    while not ready:
-        ready = client.modelReady(endpoint)
+    proteus.waitUntilModelReady(client, endpoint)
     # -load worker
 
     # +inference: construct the request and make the inference
diff --git a/examples/resnet50/migraphx.cpp b/examples/resnet50/migraphx.cpp
index e4ae97e31..5afa8a22e 100644
--- a/examples/resnet50/migraphx.cpp
+++ b/examples/resnet50/migraphx.cpp
@@ -30,15 +30,15 @@
 #include <memory>               // for allocator
 #include <opencv2/core.hpp>     // for CV_32FC3
 #include <opencv2/imgproc.hpp>  // for COLOR_BGR2RGB
+#include <optional>             // for optional
 #include <ratio>                // for milli
 #include <string>               // for string
 #include <vector>               // for vector
 
-#include "proteus/client_operators/infer_async.hpp"        // for inferAsync...
-#include "proteus/proteus.hpp"                             // for InferenceR...
-#include "proteus/util/pre_post/image_preprocess.hpp"      // for ImagePrepr...
-#include "proteus/util/pre_post/resnet50_postprocess.hpp"  // for resnet50Po...
-#include "resnet50.hpp"                                    // for Args, pars...
+#include "proteus/pre_post/image_preprocess.hpp"      // for ImagePrepr...
+#include "proteus/pre_post/resnet50_postprocess.hpp"  // for resnet50Po...
+#include "proteus/proteus.hpp"                        // for InferenceR...
+#include "resnet50.hpp"                               // for Args, pars...
 
 namespace fs = std::filesystem;
 
@@ -59,8 +59,8 @@ Images preprocess(const std::vector<std::string>& paths) {
   // this example uses a custom image preprocessing function. You may use any
   // preprocessing logic or skip it entirely if your input data is already
   // preprocessed.
-  proteus::util::ImagePreprocessOptions<float, 3> options;
-  options.order = proteus::util::ImageOrder::NCHW;
+  proteus::pre_post::ImagePreprocessOptions<float, 3> options;
+  options.order = proteus::pre_post::ImageOrder::NCHW;
   options.height = image_size;
   options.width = image_size;
   options.mean = mean;
@@ -71,7 +71,7 @@ Images preprocess(const std::vector<std::string>& paths) {
   options.convert_type = true;
   options.type = CV_32FC3;
   options.convert_scale = 1.0 / 255.0;
-  return proteus::util::imagePreprocess(paths, options);
+  return proteus::pre_post::imagePreprocess(paths, options);
 }
 
 /**
@@ -84,7 +84,7 @@ Images preprocess(const std::vector<std::string>& paths) {
  */
 std::vector<int> postprocess(const proteus::InferenceResponseOutput& output,
                              int k) {
-  return proteus::util::resnet50Postprocess(
+  return proteus::pre_post::resnet50Postprocess(
     static_cast<const float*>(output.getData()), output.getSize(), k);
 }
 
@@ -129,7 +129,7 @@ std::string load(const proteus::Client* client, const Args& args) {
   if (!serverHasExtension(client, "migraphx")) {
     std::cerr << "MIGraphX is not enabled. Please recompile with it enabled to "
               << "run this example\n";
-    exit(1);
+    exit(0);
   }
 
   // Load-time parameters are used to pass one-time information to the batcher
@@ -153,6 +153,7 @@ std::string load(const proteus::Client* client, const Args& args) {
   // before sending the batch on
   parameters.put("timeout", timeout_ms);
   std::string endpoint = client->workerLoad("migraphx", &parameters);
+  proteus::waitUntilModelReady(client, endpoint);
   // -load
   return endpoint;
 }
@@ -184,18 +185,16 @@ int main(int argc, char* argv[]) {
 
   Args args = getArgs(argc, argv);
 
-  proteus::Server server;
-#ifdef PROTEUS_ENABLE_REST
-  server.startHttp(args.http_port);
-#else
-  std::cerr << "HTTP/REST is not enabled. Please recompile the library with it "
-            << "enabled to run this example.\n";
-  exit(1);
-#endif
-
   const auto http_port_str = std::to_string(args.http_port);
   proteus::HttpClient client{"http://127.0.0.1:" + http_port_str};
 
+  std::optional<proteus::Server> server;
+  if (!client.serverLive()) {
+    std::cout << "No server detected. Starting locally...\n";
+    server.emplace();
+    server.value().startHttp(args.http_port);
+  }
+
   std::cout << "Waiting until the server is ready...\n";
   proteus::waitUntilServerReady(&client);
 
diff --git a/examples/resnet50/migraphx.py b/examples/resnet50/migraphx.py
index d64424ee0..cf77df04c 100644
--- a/examples/resnet50/migraphx.py
+++ b/examples/resnet50/migraphx.py
@@ -26,10 +26,7 @@
 import cv2
 
 import proteus
-import proteus.client_operators
-import proteus.clients
-import proteus.servers
-import proteus.util.pre_post as pre_post
+import proteus.pre_post as pre_post
 
 # isort: split
 
@@ -103,7 +100,7 @@ def load(client, args):
     you should use for subsequent requests
 
     Args:
-        client (proteus.client.Client): the client object
+        client (proteus.Client): the client object
         args (argparse.Namespace): the command line arguments
 
     Returns:
@@ -113,12 +110,11 @@ def load(client, args):
     # for a particular backend. This guard checks to make sure the server does
     # support the requested backend. If you already know it's supported, you can
     # skip this check.
-    metadata = client.serverMetadata()
-    if "migraphx" not in metadata.extensions:
+    if not proteus.serverHasExtension(client, "migraphx"):
         print(
             "MIGraphX is not enabled. Please recompile with it enabled to run this example"
         )
-        sys.exit(1)
+        sys.exit(0)
 
     # Load-time parameters are used to pass one-time information to the batcher
     # and worker as it starts up. Each worker can choose to define its own
@@ -140,6 +136,7 @@ def load(client, args):
     # sending the batch on
     parameters.put("timeout", timeout_ms)
     endpoint = client.workerLoad("migraphx", parameters)
+    proteus.waitUntilModelReady(client, endpoint)
     return endpoint
 
 
@@ -165,26 +162,16 @@ def get_args():
 def main(args):
     print("Running the MIGraphX example for ResNet50 in Python")
 
-    server = proteus.servers.Server()
+    client = proteus.HttpClient(f"http://127.0.0.1:{args.http_port}")
+    if not client.serverLive():
+        server = proteus.Server()
+        server.startHttp(args.http_port)
     print("Waiting until the server is ready...")
-    server.startHttp(args.http_port)
-
-    client = proteus.clients.HttpClient(f"http://127.0.0.1:{args.http_port}")
-    ready = False
-    while not ready:
-        try:
-            ready = client.serverReady()
-        except proteus.RuntimeError:
-            pass
-        sleep(1)
+    proteus.waitUntilServerReady(client)
 
     print("Loading worker...")
     endpoint = load(client, args)
 
-    ready = False
-    while not ready:
-        ready = client.modelReady(endpoint)
-
     paths = resolve_image_paths(pathlib.Path(args.image))
 
     images = preprocess(paths)
@@ -194,7 +181,7 @@ def main(args):
     assert len(paths) == len(requests)
     # +run inference
     # in migraphx.py
-    responses = proteus.client_operators.inferAsyncOrdered(client, endpoint, requests)
+    responses = proteus.inferAsyncOrdered(client, endpoint, requests)
     print("Making inferences...")
     for image_path, response in zip(paths, responses):
         assert not response.isError()
diff --git a/examples/resnet50/ptzendnn.cpp b/examples/resnet50/ptzendnn.cpp
index fbf456012..0b96f467b 100644
--- a/examples/resnet50/ptzendnn.cpp
+++ b/examples/resnet50/ptzendnn.cpp
@@ -34,10 +34,10 @@
 #include <string>               // for string
 #include <vector>               // for vector
 
-#include "proteus/proteus.hpp"                             // for InferenceR...
-#include "proteus/util/pre_post/image_preprocess.hpp"      // for ImagePrepr...
-#include "proteus/util/pre_post/resnet50_postprocess.hpp"  // for resnet50Po...
-#include "resnet50.hpp"                                    // for Args, pars...
+#include "proteus/pre_post/image_preprocess.hpp"      // for ImagePrepr...
+#include "proteus/pre_post/resnet50_postprocess.hpp"  // for resnet50Po...
+#include "proteus/proteus.hpp"                        // for InferenceR...
+#include "resnet50.hpp"                               // for Args, pars...
 
 namespace fs = std::filesystem;
 
@@ -57,8 +57,8 @@ Images preprocess(const std::vector<std::string>& paths) {
   // this example uses a custom image preprocessing function. You may use any
   // preprocessing logic or skip it entirely if your input data is already
   // preprocessed.
-  proteus::util::ImagePreprocessOptions<float, 3> options;
-  options.order = proteus::util::ImageOrder::NCHW;
+  proteus::pre_post::ImagePreprocessOptions<float, 3> options;
+  options.order = proteus::pre_post::ImageOrder::NCHW;
   options.mean = mean;
   options.std = std;
   options.normalize = true;
@@ -67,7 +67,7 @@ Images preprocess(const std::vector<std::string>& paths) {
   options.convert_type = true;
   options.type = CV_32FC3;
   options.convert_scale = 1.0 / 255.0;
-  return proteus::util::imagePreprocess(paths, options);
+  return proteus::pre_post::imagePreprocess(paths, options);
 }
 
 /**
@@ -80,7 +80,7 @@ Images preprocess(const std::vector<std::string>& paths) {
  */
 std::vector<int> postprocess(const proteus::InferenceResponseOutput& output,
                              int k) {
-  return proteus::util::resnet50Postprocess(
+  return proteus::pre_post::resnet50Postprocess(
     static_cast<const float*>(output.getData()), output.getSize(), k);
 }
 
@@ -126,7 +126,7 @@ std::string load(const proteus::Client* client, const Args& args) {
     std::cerr
       << "PT+ZenDNN is not enabled. Please recompile with it enabled to "
       << "run this example\n";
-    exit(1);
+    exit(0);
   }
 
   // Load-time parameters are used to pass one-time information to the batcher
@@ -169,12 +169,12 @@ int main(int argc, char* argv[]) {
 
   Args args = getArgs(argc, argv);
 
-  proteus::Server server;
-
   // +create client
   // ptzendnn.cpp
   proteus::NativeClient client;
 
+  proteus::Server server;
+
   std::cout << "Waiting until the server is ready...\n";
   proteus::waitUntilServerReady(&client);
   // -create client
diff --git a/examples/resnet50/ptzendnn.py b/examples/resnet50/ptzendnn.py
index 89de710de..b09c4c510 100644
--- a/examples/resnet50/ptzendnn.py
+++ b/examples/resnet50/ptzendnn.py
@@ -26,10 +26,7 @@
 import cv2
 
 import proteus
-import proteus.client_operators
-import proteus.clients
-import proteus.servers
-import proteus.util.pre_post as pre_post
+import proteus.pre_post as pre_post
 
 # isort: split
 
@@ -97,7 +94,7 @@ def load(client, args):
     you should use for subsequent requests
 
     Args:
-        client (proteus.client.Client): the client object
+        client (proteus.Client): the client object
         args (argparse.Namespace): the command line arguments
 
     Returns:
@@ -107,13 +104,11 @@ def load(client, args):
     # for a particular backend. This guard checks to make sure the server does
     # support the requested backend. If you already know it's supported, you can
     # skip this check.
-
-    metadata = client.serverMetadata()
-    if "ptzendnn" not in metadata.extensions:
+    if not proteus.serverHasExtension(client, "ptzendnn"):
         print(
             "PT+ZenDNN is not enabled. Please recompile with it enabled to run this example"
         )
-        sys.exit(1)
+        sys.exit(0)
 
     # Load-time parameters are used to pass one-time information to the batcher
     # and worker as it starts up. Each worker can choose to define its own
@@ -126,6 +121,7 @@ def load(client, args):
     parameters.put("input_size", args.input_size)
     parameters.put("output_classes", args.output_classes)
     endpoint = client.workerLoad("ptzendnn", parameters)
+    proteus.waitUntilModelReady(client, endpoint)
     return endpoint
 
 
@@ -151,26 +147,15 @@ def get_args():
 def main(args):
     print("Running the PT+ZenDNN example for ResNet50 in Python")
 
-    server = proteus.servers.Server()
+    server = proteus.Server()
     print("Waiting until the server is ready...")
-    server.startHttp(args.http_port)
 
-    client = proteus.clients.HttpClient(f"http://127.0.0.1:{args.http_port}")
-    ready = False
-    while not ready:
-        try:
-            ready = client.serverReady()
-        except proteus.RuntimeError:
-            pass
-        sleep(1)
+    client = proteus.NativeClient()
+    proteus.waitUntilServerReady(client)
 
     print("Loading worker...")
     endpoint = load(client, args)
 
-    ready = False
-    while not ready:
-        ready = client.modelReady(endpoint)
-
     paths = resolve_image_paths(pathlib.Path(args.image))
 
     images = preprocess(paths)
@@ -178,7 +163,7 @@ def main(args):
     requests = construct_requests(images)
 
     assert len(paths) == len(requests)
-    responses = proteus.client_operators.inferAsyncOrdered(client, endpoint, requests)
+    responses = proteus.inferAsyncOrdered(client, endpoint, requests)
     print("Making inferences...")
     for image_path, response in zip(paths, responses):
         assert not response.isError()
diff --git a/examples/resnet50/tfzendnn.cpp b/examples/resnet50/tfzendnn.cpp
index 6478fc3ca..27db42250 100644
--- a/examples/resnet50/tfzendnn.cpp
+++ b/examples/resnet50/tfzendnn.cpp
@@ -28,14 +28,15 @@
 #include <iostream>             // for operator<<
 #include <memory>               // for allocator
 #include <opencv2/imgproc.hpp>  // for COLOR_BGR2RGB
+#include <optional>             // for optional
 #include <ratio>                // for milli
 #include <string>               // for string
 #include <vector>               // for vector
 
-#include "proteus/proteus.hpp"                             // for InferenceR...
-#include "proteus/util/pre_post/image_preprocess.hpp"      // for ImagePrepr...
-#include "proteus/util/pre_post/resnet50_postprocess.hpp"  // for resnet50Po...
-#include "resnet50.hpp"                                    // for Args, pars...
+#include "proteus/pre_post/image_preprocess.hpp"      // for ImagePrepr...
+#include "proteus/pre_post/resnet50_postprocess.hpp"  // for resnet50Po...
+#include "proteus/proteus.hpp"                        // for InferenceR...
+#include "resnet50.hpp"                               // for Args, pars...
 
 namespace fs = std::filesystem;
 
@@ -52,11 +53,11 @@ Images preprocess(const std::vector<std::string>& paths) {
   // this example uses a custom image preprocessing function. You may use any
   // preprocessing logic or skip it entirely if your input data is already
   // preprocessed.
-  proteus::util::ImagePreprocessOptions<float, 3> options;
+  proteus::pre_post::ImagePreprocessOptions<float, 3> options;
   options.convert_color = true;
   options.color_code = cv::COLOR_BGR2RGB;
   options.assign = true;
-  return proteus::util::imagePreprocess(paths, options);
+  return proteus::pre_post::imagePreprocess(paths, options);
 }
 
 /**
@@ -69,7 +70,7 @@ Images preprocess(const std::vector<std::string>& paths) {
  */
 std::vector<int> postprocess(const proteus::InferenceResponseOutput& output,
                              int k) {
-  return proteus::util::resnet50Postprocess(
+  return proteus::pre_post::resnet50Postprocess(
     static_cast<const float*>(output.getData()), output.getSize(), k);
 }
 
@@ -115,7 +116,7 @@ std::string load(const proteus::Client* client, const Args& args) {
     std::cerr
       << "TF+ZenDNN is not enabled. Please recompile with it enabled to "
       << "run this example\n";
-    exit(1);
+    exit(0);
   }
 
   // Load-time parameters are used to pass one-time information to the batcher
@@ -132,6 +133,7 @@ std::string load(const proteus::Client* client, const Args& args) {
   parameters.put("input_node", args.input_node);
   parameters.put("output_node", args.output_node);
   std::string endpoint = client->workerLoad("tfzendnn", &parameters);
+  proteus::waitUntilModelReady(client, endpoint);
   // -load
   return endpoint;
 }
@@ -171,22 +173,18 @@ int main(int argc, char* argv[]) {
 
   Args args = getArgs(argc, argv);
 
-  proteus::Server server;
-#ifdef PROTEUS_ENABLE_GRPC
-  // +start protocol
-  server.startGrpc(args.grpc_port);
-  // -start protocol
-#else
-  std::cerr << "gRPC is not enabled. Please recompile the library with it "
-            << "enabled to run this example.\n";
-  exit(1);
-#endif
-
   // +create client
   // tfzendnn.cpp
   const auto grpc_port_str = std::to_string(args.grpc_port);
   proteus::GrpcClient client{"127.0.0.1:" + grpc_port_str};
 
+  std::optional<proteus::Server> server;
+  if (!client.serverLive()) {
+    std::cout << "No server detected. Starting locally...\n";
+    server.emplace();
+    server.value().startGrpc(args.grpc_port);
+  }
+
   std::cout << "Waiting until the server is ready...\n";
   proteus::waitUntilServerReady(&client);
   // -create client
diff --git a/examples/resnet50/tfzendnn.py b/examples/resnet50/tfzendnn.py
index df1a785e4..0a89d9dfb 100644
--- a/examples/resnet50/tfzendnn.py
+++ b/examples/resnet50/tfzendnn.py
@@ -26,10 +26,7 @@
 import cv2
 
 import proteus
-import proteus.client_operators
-import proteus.clients
-import proteus.servers
-import proteus.util.pre_post as pre_post
+import proteus.pre_post as pre_post
 
 # isort: split
 
@@ -91,7 +88,7 @@ def load(client, args):
     you should use for subsequent requests
 
     Args:
-        client (proteus.client.Client): the client object
+        client (proteus.Client): the client object
         args (argparse.Namespace): the command line arguments
 
     Returns:
@@ -102,12 +99,11 @@ def load(client, args):
     # for a particular backend. This guard checks to make sure the server does
     # support the requested backend. If you already know it's supported, you can
     # skip this check.
-    metadata = client.serverMetadata()
-    if "tfzendnn" not in metadata.extensions:
+    if not proteus.serverHasExtension(client, "tfzendnn"):
         print(
             "TF+ZenDNN is not enabled. Please recompile with it enabled to run this example"
         )
-        sys.exit(1)
+        sys.exit(0)
 
     # Load-time parameters are used to pass one-time information to the batcher
     # and worker as it starts up. Each worker can choose to define its own
@@ -122,6 +118,7 @@ def load(client, args):
     parameters.put("input_node", args.input_node)
     parameters.put("output_node", args.output_node)
     endpoint = client.workerLoad("tfzendnn", parameters)
+    proteus.waitUntilModelReady(client, endpoint)
     return endpoint
 
 
@@ -155,26 +152,16 @@ def get_args():
 def main(args):
     print("Running the TF+ZenDNN example for ResNet50 in Python")
 
-    server = proteus.servers.Server()
+    client = proteus.GrpcClient(f"127.0.0.1:{args.grpc_port}")
+    if not client.serverLive():
+        server = proteus.Server()
+        server.startGrpc(args.grpc_port)
     print("Waiting until the server is ready...")
-    server.startHttp(args.http_port)
-
-    client = proteus.clients.HttpClient(f"http://127.0.0.1:{args.http_port}")
-    ready = False
-    while not ready:
-        try:
-            ready = client.serverReady()
-        except proteus.RuntimeError:
-            pass
-        sleep(1)
+    proteus.waitUntilServerReady(client)
 
     print("Loading worker...")
     endpoint = load(client, args)
 
-    ready = False
-    while not ready:
-        ready = client.modelReady(endpoint)
-
     paths = resolve_image_paths(pathlib.Path(args.image))
 
     images = preprocess(paths)
diff --git a/examples/resnet50/vitis.cpp b/examples/resnet50/vitis.cpp
index 8ebfc6a7e..e670d4151 100644
--- a/examples/resnet50/vitis.cpp
+++ b/examples/resnet50/vitis.cpp
@@ -30,6 +30,7 @@
 #include <iostream>          // for operator<<
 #include <memory>            // for allocator
 #include <opencv2/core.hpp>  // for int8_t
+#include <optional>          // for optional
 #include <ratio>             // for milli
 #include <string>            // for string
 #include <vector>            // for vector
@@ -38,8 +39,8 @@
 #include "proteus/proteus.hpp"
 // -include:
 
-#include "proteus/util/pre_post/image_preprocess.hpp"
-#include "proteus/util/pre_post/resnet50_postprocess.hpp"
+#include "proteus/pre_post/image_preprocess.hpp"
+#include "proteus/pre_post/resnet50_postprocess.hpp"
 #include "resnet50.hpp"
 
 namespace fs = std::filesystem;
@@ -60,12 +61,12 @@ Images preprocess(const std::vector<std::string>& paths) {
   // this example uses a custom image preprocessing function. You may use any
   // preprocessing logic or skip it entirely if your input data is already
   // preprocessed.
-  proteus::util::ImagePreprocessOptions<int8_t, 3> options;
-  options.order = proteus::util::ImageOrder::NHWC;
+  proteus::pre_post::ImagePreprocessOptions<int8_t, 3> options;
+  options.order = proteus::pre_post::ImageOrder::NHWC;
   options.mean = mean;
   options.std = std;
   options.normalize = true;
-  return proteus::util::imagePreprocess(paths, options);
+  return proteus::pre_post::imagePreprocess(paths, options);
 }
 
 /**
@@ -78,7 +79,7 @@ Images preprocess(const std::vector<std::string>& paths) {
  */
 std::vector<int> postprocess(const proteus::InferenceResponseOutput& output,
                              int k) {
-  return proteus::util::resnet50Postprocess(
+  return proteus::pre_post::resnet50Postprocess(
     static_cast<const int8_t*>(output.getData()), output.getSize(), k);
 }
 
@@ -125,7 +126,7 @@ std::string load(const proteus::Client* client, const Args& args) {
   if (!serverHasExtension(client, "vitis")) {
     std::cerr << "Vitis AI is not enabled. Please recompile with it enabled to "
                  "run this example\n";
-    exit(1);
+    exit(0);
   }
 
   // Load-time parameters are used to pass one-time information to the batcher
@@ -170,27 +171,25 @@ int main(int argc, char* argv[]) {
 
   Args args = getArgs(argc, argv);
 
-  // +initialize:
-  proteus::Server server;
-  // -initialize:
-#ifdef PROTEUS_ENABLE_REST
-  // +start protocol:
-  server.startHttp(args.http_port);
-  // -start protocol:
-#else
-  std::cerr << "HTTP/REST is not enabled. Please recompile with it enabled to "
-            << "run this example.\n";
-  exit(1);
-#endif
-
   // +create client:
   // vitis.cpp
   const auto http_port_str = std::to_string(args.http_port);
   proteus::HttpClient client{"http://127.0.0.1:" + http_port_str};
+  // -create client:
+
+  // +initialize:
+  std::optional<proteus::Server> server;
+  // -initialize:
+  // +start protocol:
+  if (!client.serverLive()) {
+    std::cout << "No server detected. Starting locally...\n";
+    server.emplace();
+    server.value().startHttp(args.http_port);
+  }
+  // -start protocol:
 
   std::cout << "Waiting until the server is ready...\n";
   proteus::waitUntilServerReady(&client);
-  // -create client:
 
   std::cout << "Loading worker...\n";
   std::string endpoint = load(&client, args);
diff --git a/examples/resnet50/vitis.py b/examples/resnet50/vitis.py
index 693c5cbee..89de604c1 100644
--- a/examples/resnet50/vitis.py
+++ b/examples/resnet50/vitis.py
@@ -25,9 +25,7 @@
 
 # +import
 import proteus
-import proteus.clients
-import proteus.servers
-import proteus.util.pre_post as pre_post
+import proteus.pre_post as pre_post
 
 # -import
 
@@ -96,7 +94,7 @@ def load(client, args):
     you should use for subsequent requests
 
     Args:
-        client (proteus.client.Client): the client object
+        client (proteus.Client): the client object
         args (argparse.Namespace): the command line arguments
 
     Returns:
@@ -107,12 +105,11 @@ def load(client, args):
     # for a particular backend. This guard checks to make sure the server does
     # support the requested backend. If you already know it's supported, you can
     # skip this check.
-    metadata = client.serverMetadata()
-    if "vitis" not in metadata.extensions:
+    if not proteus.serverHasExtension(client, "vitis"):
         print(
             "Vitis AI is not enabled. Please recompile with it enabled to run this example"
         )
-        sys.exit(1)
+        sys.exit(0)
 
     # Load-time parameters are used to pass one-time information to the batcher
     # and worker as it starts up. Each worker can choose to define its own
@@ -153,32 +150,24 @@ def get_args():
 def main(args):
     print("Running the Vitis example for ResNet50 in Python")
 
-    # +initialize
-    server = proteus.servers.Server()
-    # -initialize
-    print("Waiting until the server is ready...")
-    # +start protocol
-    server.startHttp(args.http_port)
-    # -start protocol
-
     # + create client
-    client = proteus.clients.HttpClient(f"http://127.0.0.1:{args.http_port}")
+    client = proteus.HttpClient(f"http://127.0.0.1:{args.http_port}")
     # - create client
-    ready = False
-    while not ready:
-        try:
-            ready = client.serverReady()
-        except proteus.RuntimeError:
-            pass
-        sleep(1)
+    if not client.serverLive():
+        # +initialize
+        server = proteus.Server()
+        # -initialize
+        # +start protocol
+        server.startHttp(args.http_port)
+        # -start protocol
+    print("Waiting until the server is ready...")
+    proteus.waitUntilServerReady(client)
 
     print("Loading worker...")
     endpoint = load(client, args)
 
     # +wait model ready
-    ready = False
-    while not ready:
-        ready = client.modelReady(endpoint)
+    proteus.waitUntilModelReady(client, endpoint)
     # -wait model ready
 
     # +prepare images
diff --git a/examples/yolo/migraphx.py b/examples/yolo/migraphx.py
index ae38c2cba..7cf490ff1 100644
--- a/examples/yolo/migraphx.py
+++ b/examples/yolo/migraphx.py
@@ -42,7 +42,6 @@
 
 
 import proteus
-import proteus.clients
 
 # isort: split
 
@@ -92,12 +91,11 @@ def load(client, args):
     # for a particular backend. This guard checks to make sure the server does
     # support the requested backend. If you already know it's supported, you can
     # skip this check.
-    metadata = client.serverMetadata()
-    if "migraphx" not in metadata.extensions:
+    if not proteus.serverHasExtension(client, "migraphx"):
         print(
             "MIGraphX is not enabled. Please recompile with it enabled to run this example"
         )
-        sys.exit(1)
+        sys.exit(0)
 
     # Load-time parameters are used to pass one-time information to the batcher
     # and worker as it starts up. Each worker can choose to define its own
@@ -123,9 +121,7 @@ def load(client, args):
     endpoint = client.workerLoad("migraphx", parameters)
 
     # wait for the worker to load and compile model
-    ready = False
-    while not ready:
-        ready = client.modelReady(endpoint)
+    proteus.waitUntilModelReady(client, endpoint)
 
     return endpoint
 
@@ -152,18 +148,13 @@ def get_args():
 def main(args):
     print("Running the MIGraphX example for Yolo in Python")
 
-    server = proteus.servers.Server()
+    client = proteus.HttpClient(f"http://127.0.0.1:{args.http_port}")
+    if not client.serverLive():
+        print("No server detected. Starting locally...")
+        server = proteus.Server()
+        server.startHttp(args.http_port)
     print("Waiting until the server is ready...")
-    server.startHttp(args.http_port)
-
-    client = proteus.clients.HttpClient(f"http://127.0.0.1:{args.http_port}")
-    ready = False
-    while not ready:
-        try:
-            ready = client.serverReady()
-        except proteus.RuntimeError:
-            pass
-        time.sleep(1)
+    proteus.waitUntilServerReady(client)
 
     print("Loading worker...")
     endpoint = load(client, args)
@@ -174,7 +165,7 @@ def main(args):
 
     print("Creating inference requests...")
     requests = [proteus.ImageInferenceRequest(image) for image in images]
-    responses = proteus.client_operators.inferAsyncOrdered(client, endpoint, requests)
+    responses = proteus.inferAsyncOrdered(client, endpoint, requests)
     print("Client received inference reply")
 
     assert len(responses) == len(original_images)
diff --git a/include/proteus/client_operators/infer_async.hpp b/include/proteus/client_operators/infer_async.hpp
deleted file mode 100644
index 4f35f1ff7..000000000
--- a/include/proteus/client_operators/infer_async.hpp
+++ /dev/null
@@ -1,21 +0,0 @@
-#ifndef GUARD_PROTEUS_CLIENT_API_INFER_ASYNC
-#define GUARD_PROTEUS_CLIENT_API_INFER_ASYNC
-
-#include <string>
-#include <vector>
-
-#include "proteus/clients/client.hpp"
-#include "proteus/core/predict_api.hpp"
-
-namespace proteus {
-
-std::vector<InferenceResponse> inferAsyncOrdered(
-  Client* client, const std::string& model,
-  const std::vector<InferenceRequest>& requests);
-std::vector<InferenceResponse> inferAsyncOrderedBatched(
-  Client* client, const std::string& model,
-  const std::vector<InferenceRequest>& requests, size_t batch_size);
-
-}  // namespace proteus
-
-#endif  // GUARD_PROTEUS_CLIENT_API_INFER_ASYNC
diff --git a/include/proteus/clients/client.hpp b/include/proteus/clients/client.hpp
index 5c7f46ff2..678ea4919 100644
--- a/include/proteus/clients/client.hpp
+++ b/include/proteus/clients/client.hpp
@@ -62,6 +62,13 @@ bool serverHasExtension(const Client* client, const std::string& extension);
 void waitUntilServerReady(const Client* client);
 void waitUntilModelReady(const Client* client, const std::string& model);
 
+std::vector<InferenceResponse> inferAsyncOrdered(
+  Client* client, const std::string& model,
+  const std::vector<InferenceRequest>& requests);
+std::vector<InferenceResponse> inferAsyncOrderedBatched(
+  Client* client, const std::string& model,
+  const std::vector<InferenceRequest>& requests, size_t batch_size);
+
 }  // namespace proteus
 
 #endif  // GUARD_PROTEUS_CLIENTS_CLIENT
diff --git a/src/proteus/CMakeLists.txt b/src/proteus/CMakeLists.txt
index b6a6db3ca..a6a367423 100644
--- a/src/proteus/CMakeLists.txt
+++ b/src/proteus/CMakeLists.txt
@@ -26,7 +26,6 @@ target_link_libraries(
 add_subdirectory(batching)
 add_subdirectory(bindings)
 add_subdirectory(buffers)
-add_subdirectory(client_operators)
 add_subdirectory(clients)
 add_subdirectory(core)
 add_subdirectory(observation)
@@ -49,7 +48,6 @@ add_library(
   proteus
   ${TYPE}
   ${BATCHING_OBJS}
-  ${CLIENT_OPERATORS_OBJS}
   ${CLIENT_OBJS}
   ${CORE_OBJS}
   ${HELPER_OBJS}
@@ -69,8 +67,7 @@ target_link_libraries(
 )
 
 add_library(
-  proteus-client ${TYPE} ${CLIENT_OPERATORS_OBJS} ${CLIENT_OBJS} ${CORE_OBJS}
-                 ${OBSERVATION_OBJS}
+  proteus-client ${TYPE} ${CLIENT_OBJS} ${CORE_OBJS} ${OBSERVATION_OBJS}
 )
 target_link_libraries(proteus-client PRIVATE clients core observation util)
 
@@ -110,7 +107,7 @@ if(NOT SKBUILD)
 
   # cmake-format: off
   install(
-    TARGETS proteus proteus-client proteus-server util
+    TARGETS proteus proteus-client proteus-server
     EXPORT proteus_Targets
     RUNTIME COMPONENT proteus_Runtime
     LIBRARY COMPONENT proteus_Runtime
diff --git a/src/proteus/bindings/python/CMakeLists.txt b/src/proteus/bindings/python/CMakeLists.txt
index 61ddc52e4..514a7b0b8 100644
--- a/src/proteus/bindings/python/CMakeLists.txt
+++ b/src/proteus/bindings/python/CMakeLists.txt
@@ -44,13 +44,12 @@ add_custom_target(
 )
 
 add_subdirectory(core) # sets CORE_OBJS
-add_subdirectory(client_operators) # sets CLIENT_OPERATORS_OBJS
 add_subdirectory(clients) # sets CLIENT_OBJS
 add_subdirectory(servers) # sets SERVER_OBJS
 if(${PROTEUS_BUILD_TESTING})
   add_subdirectory(testing) # sets TESTING_OBJS
 endif()
-add_subdirectory(util) # sets UTIL_OBJS
+add_subdirectory(pre_post) # sets PRE_POST_OBJS
 
 # add this dummy file to add a dependency on the Pybind11 target so it runs if
 # the Proteus package isn't installed in pip
@@ -70,14 +69,8 @@ endif()
 pybind11_add_module(proteus_py MODULE proteus.cpp ${dummy_new_file})
 target_include_directories(proteus_py PRIVATE ${PROTEUS_INCLUDE_DIRS})
 target_link_libraries(
-  proteus_py
-  PRIVATE proteus
-          ${Python_LIBRARIES}
-          ${CORE_OBJS}
-          ${CLIENT_OPERATORS_OBJS}
-          ${CLIENT_OBJS}
-          ${SERVER_OBJS}
-          ${UTIL_OBJS}
+  proteus_py PRIVATE proteus ${Python_LIBRARIES} ${CORE_OBJS} ${CLIENT_OBJS}
+                     ${SERVER_OBJS} ${PRE_POST_OBJS}
 )
 if(${PROTEUS_BUILD_TESTING})
   target_link_libraries(proteus_py PRIVATE ${TESTING_OBJS})
diff --git a/src/proteus/bindings/python/client_operators/CMakeLists.txt b/src/proteus/bindings/python/client_operators/CMakeLists.txt
deleted file mode 100644
index df0ac5779..000000000
--- a/src/proteus/bindings/python/client_operators/CMakeLists.txt
+++ /dev/null
@@ -1,34 +0,0 @@
-# Copyright 2022 Xilinx Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-list(APPEND TARGETS infer_async)
-
-list(APPEND LINK_TARGETS) # create empty list
-
-foreach(target ${TARGETS})
-  add_library(${target}_py OBJECT ${target}.cpp)
-  target_include_directories(
-    ${target}_py PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/.. ${PROTEUS_INCLUDE_DIRS}
-                         ${Python_INCLUDE_DIRS}
-  )
-  target_link_libraries(
-    ${target}_py PRIVATE ${Python_LIBRARIES} ${target}
-                         $<TARGET_OBJECTS:${target}>
-  )
-  add_dependencies(${target}_py pybind11Mkdoc)
-
-  list(APPEND LINK_TARGETS ${target}_py)
-endforeach()
-
-set(CLIENT_OPERATORS_OBJS ${LINK_TARGETS} PARENT_SCOPE)
diff --git a/src/proteus/bindings/python/client_operators/client_operators.hpp b/src/proteus/bindings/python/client_operators/client_operators.hpp
deleted file mode 100644
index abfeb0c74..000000000
--- a/src/proteus/bindings/python/client_operators/client_operators.hpp
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright 2022 Xilinx Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-/**
- * @file
- * @brief Defines the objects for Python bindings
- */
-
-#ifndef GUARD_PROTEUS_BINDINGS_PYTHON_CLIENT_OPERATORS_CLIENT_OPERATORS
-#define GUARD_PROTEUS_BINDINGS_PYTHON_CLIENT_OPERATORS_CLIENT_OPERATORS
-
-namespace pybind11 {
-class module_;
-}
-
-namespace proteus {
-
-void wrapInferAsync(pybind11::module_ &);
-
-}  // namespace proteus
-
-#endif  // GUARD_PROTEUS_BINDINGS_PYTHON_CLIENT_OPERATORS_CLIENT_OPERATORS
diff --git a/src/proteus/bindings/python/client_operators/infer_async.cpp b/src/proteus/bindings/python/client_operators/infer_async.cpp
deleted file mode 100644
index e0f058296..000000000
--- a/src/proteus/bindings/python/client_operators/infer_async.cpp
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright 2022 Xilinx Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-/**
- * @file
- * @brief Implements the Python bindings for the http.hpp header
- */
-
-#include "proteus/client_operators/infer_async.hpp"
-
-#include <pybind11/cast.h>      // for arg
-#include <pybind11/pybind11.h>  // for class_, init
-#include <pybind11/stl.h>       // IWYU pragma: keep
-
-#include <unordered_map>  // for unordered_map
-
-#include "proteus/bindings/python/helpers/docstrings.hpp"  // for DOCS
-
-namespace py = pybind11;
-
-namespace proteus {
-
-void wrapInferAsync(py::module_ &m) {
-  m.def("inferAsyncOrdered", &inferAsyncOrdered, py::arg("client"),
-        py::arg("model"), py::arg("requests"));
-  m.def("inferAsyncOrderedBatched", &inferAsyncOrderedBatched,
-        py::arg("client"), py::arg("model"), py::arg("requests"),
-        py::arg("batch_sizes"));
-}
-
-}  // namespace proteus
diff --git a/src/proteus/bindings/python/clients/CMakeLists.txt b/src/proteus/bindings/python/clients/CMakeLists.txt
index ed73463c2..b934a4706 100644
--- a/src/proteus/bindings/python/clients/CMakeLists.txt
+++ b/src/proteus/bindings/python/clients/CMakeLists.txt
@@ -12,19 +12,17 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-list(APPEND TARGETS http websocket)
+list(APPEND targets client native)
 
-list(APPEND LINK_TARGETS client_py)
+if(${PROTEUS_ENABLE_HTTP})
+  list(APPEND targets http websocket)
+endif()
 
-add_library(client_py OBJECT client.cpp)
-target_include_directories(
-  client_py PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/.. ${PROTEUS_INCLUDE_DIRS}
-                    ${Python_INCLUDE_DIRS}
-)
-target_link_libraries(client_py PRIVATE ${Python_LIBRARIES})
-add_dependencies(client_py pybind11Mkdoc)
+if(${PROTEUS_ENABLE_GRPC})
+  list(APPEND targets grpc)
+endif()
 
-foreach(target ${TARGETS})
+foreach(target ${targets})
   add_library(${target}_py OBJECT ${target}.cpp)
   target_include_directories(
     ${target}_py PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/.. ${PROTEUS_INCLUDE_DIRS}
@@ -39,7 +37,4 @@ foreach(target ${TARGETS})
   set(LINK_TARGETS ${LINK_TARGETS} ${target}_py)
 endforeach()
 
-# target_link_libraries(data_types_py PRIVATE
-# /usr/lib/python3/dist-packages/xir.cpython-36m-x86_64-linux-gnu.so)
-
 set(CLIENT_OBJS ${LINK_TARGETS} PARENT_SCOPE)
diff --git a/src/proteus/bindings/python/clients/client.cpp b/src/proteus/bindings/python/clients/client.cpp
index 2bfc288ff..6602c646d 100644
--- a/src/proteus/bindings/python/clients/client.cpp
+++ b/src/proteus/bindings/python/clients/client.cpp
@@ -20,11 +20,26 @@
 #include "proteus/clients/client.hpp"
 
 #include <pybind11/pybind11.h>
+#include <pybind11/stl.h>  // IWYU pragma: keep
 
 namespace py = pybind11;
 
+namespace proteus {
+
 void wrapClient(py::module_ &m) {
-  using proteus::Client;
+  py::class_<Client> client{m, "Client"};
+
+  m.def("serverHasExtension", &serverHasExtension, py::arg("client"),
+        py::arg("extension"));
+  m.def("waitUntilServerReady", &waitUntilServerReady, py::arg("client"));
+  m.def("waitUntilModelReady", &waitUntilModelReady, py::arg("client"),
+        py::arg("model"));
 
-  py::class_<Client>(m, "Client");
+  m.def("inferAsyncOrdered", &inferAsyncOrdered, py::arg("client"),
+        py::arg("model"), py::arg("requests"));
+  m.def("inferAsyncOrderedBatched", &inferAsyncOrderedBatched,
+        py::arg("client"), py::arg("model"), py::arg("requests"),
+        py::arg("batch_sizes"));
 }
+
+}  // namespace proteus
diff --git a/src/proteus/bindings/python/clients/client.hpp b/src/proteus/bindings/python/clients/clients.hpp
similarity index 56%
rename from src/proteus/bindings/python/clients/client.hpp
rename to src/proteus/bindings/python/clients/clients.hpp
index ed495d6f8..938e11aa8 100644
--- a/src/proteus/bindings/python/clients/client.hpp
+++ b/src/proteus/bindings/python/clients/clients.hpp
@@ -17,15 +17,39 @@
  * @brief Defines the objects for Python bindings
  */
 
-#ifndef GUARD_PROTEUS_BINDINGS_PYTHON_CLIENTS_CLIENT
-#define GUARD_PROTEUS_BINDINGS_PYTHON_CLIENTS_CLIENT
+#ifndef GUARD_PROTEUS_BINDINGS_PYTHON_CLIENTS_CLIENTS
+#define GUARD_PROTEUS_BINDINGS_PYTHON_CLIENTS_CLIENTS
+
+#include "proteus/build_options.hpp"
 
 namespace pybind11 {
 class module_;
-}
+}  // namespace pybind11
+
+namespace proteus {
 
 void wrapClient(pybind11::module_ &);
+void wrapNativeClient(pybind11::module_ &);
+#ifdef PROTEUS_ENABLE_HTTP
 void wrapHttpClient(pybind11::module_ &);
 void wrapWebSocketClient(pybind11::module_ &);
+#endif
+#ifdef PROTEUS_ENABLE_GRPC
+void wrapGrpcClient(pybind11::module_ &);
+#endif
+
+void wrapClients(pybind11::module_ &m) {
+  wrapClient(m);
+  wrapNativeClient(m);
+#ifdef PROTEUS_ENABLE_HTTP
+  wrapHttpClient(m);
+  wrapWebSocketClient(m);
+#endif
+#ifdef PROTEUS_ENABLE_GRPC
+  wrapGrpcClient(m);
+#endif
+}
+
+}  // namespace proteus
 
-#endif  // GUARD_PROTEUS_BINDINGS_PYTHON_CLIENTS_CLIENT
+#endif  // GUARD_PROTEUS_BINDINGS_PYTHON_CLIENTS_CLIENTS
diff --git a/src/proteus/bindings/python/clients/grpc.cpp b/src/proteus/bindings/python/clients/grpc.cpp
new file mode 100644
index 000000000..44dc25b4a
--- /dev/null
+++ b/src/proteus/bindings/python/clients/grpc.cpp
@@ -0,0 +1,67 @@
+// Copyright 2022 Advanced Micro Devices, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @file
+ * @brief Implements the Python bindings for the http.hpp header
+ */
+
+#include "proteus/clients/grpc.hpp"
+
+#include <pybind11/cast.h>      // for arg
+#include <pybind11/pybind11.h>  // for class_, init
+#include <pybind11/stl.h>       // IWYU pragma: keep
+
+#include <unordered_map>  // for unordered_map
+
+#include "proteus/bindings/python/helpers/docstrings.hpp"  // for DOCS
+#include "proteus/declarations.hpp"
+
+namespace py = pybind11;
+
+namespace proteus {
+
+void wrapGrpcClient(py::module_ &m) {
+  py::class_<GrpcClient, proteus::Client>(m, "GrpcClient")
+    .def(py::init<const std::string &>(), py::arg("address"),
+         DOCS(GrpcClient, GrpcClient))
+    .def("serverMetadata", &GrpcClient::serverMetadata,
+         DOCS(GrpcClient, serverMetadata))
+    .def("serverLive", &GrpcClient::serverLive, DOCS(GrpcClient, serverLive))
+    .def("serverReady", &GrpcClient::serverReady, DOCS(GrpcClient, serverReady))
+    .def("modelReady", &GrpcClient::modelReady, py::arg("model"),
+         DOCS(GrpcClient, modelReady))
+    .def("modelMetadata", &GrpcClient::modelMetadata, py::arg("model"),
+         DOCS(GrpcClient, modelMetadata))
+    .def("modelLoad", &GrpcClient::modelLoad, py::arg("model"),
+         py::arg("parameters") = proteus::RequestParameters(),
+         DOCS(GrpcClient, modelLoad))
+    .def("modelUnload", &GrpcClient::modelUnload, py::arg("model"),
+         DOCS(GrpcClient, modelUnload))
+    .def("workerLoad", &GrpcClient::workerLoad, py::arg("model"),
+         py::arg("parameters") = proteus::RequestParameters(),
+         DOCS(GrpcClient, workerLoad))
+    .def("workerUnload", &GrpcClient::workerUnload, py::arg("model"),
+         DOCS(GrpcClient, workerUnload))
+    .def("modelInfer", &GrpcClient::modelInfer, py::arg("model"),
+         py::arg("request"), DOCS(GrpcClient, modelInfer))
+    // cannot wrap future directly in Python
+    // .def("modelInferAsync", &GrpcClient::modelInferAsync, py::arg("model"),
+    //      py::arg("request"), DOCS(GrpcClient, modelInferAsync))
+    .def("modelList", &GrpcClient::modelList, DOCS(GrpcClient, modelList))
+    .def("hasHardware", &GrpcClient::hasHardware, py::arg("name"),
+         py::arg("num"), DOCS(GrpcClient, hasHardware));
+}
+
+}  // namespace proteus
diff --git a/src/proteus/bindings/python/clients/http.cpp b/src/proteus/bindings/python/clients/http.cpp
index fd82e1e0d..8387abef7 100644
--- a/src/proteus/bindings/python/clients/http.cpp
+++ b/src/proteus/bindings/python/clients/http.cpp
@@ -31,15 +31,17 @@
 
 namespace py = pybind11;
 
+namespace proteus {
+
 void wrapHttpClient(py::module_ &m) {
   using proteus::HttpClient;
 
   py::class_<HttpClient, proteus::Client>(m, "HttpClient")
     .def(py::init<const std::string &,
-                  const std::unordered_map<std::string, std::string>>(),
+                  const std::unordered_map<std::string, std::string>, int>(),
          py::arg("address"),
          py::arg("headers") = std::unordered_map<std::string, std::string>(),
-         DOCS(HttpClient, HttpClient))
+         py::arg("parallelism") = 32, DOCS(HttpClient, HttpClient))
     .def("serverMetadata", &HttpClient::serverMetadata,
          DOCS(HttpClient, serverMetadata))
     .def("serverLive", &HttpClient::serverLive, DOCS(HttpClient, serverLive))
@@ -65,7 +67,7 @@ void wrapHttpClient(py::module_ &m) {
     //      py::arg("request"), DOCS(HttpClient, modelInferAsync))
     .def("modelList", &HttpClient::modelList, DOCS(HttpClient, modelList))
     .def("hasHardware", &HttpClient::hasHardware, py::arg("name"),
-         py::arg("num"), DOCS(HttpClient, modelList))
+         py::arg("num"), DOCS(HttpClient, hasHardware))
     .def(py::pickle(
       [](const HttpClient &p) {  // __getstate__
         return py::make_tuple(p.getAddress(), p.getHeaders());
@@ -77,3 +79,5 @@ void wrapHttpClient(py::module_ &m) {
         return {t[0].cast<std::string>(), t[1].cast<proteus::StringMap>()};
       }));
 }
+
+}  // namespace proteus
diff --git a/src/proteus/bindings/python/clients/native.cpp b/src/proteus/bindings/python/clients/native.cpp
new file mode 100644
index 000000000..56254058a
--- /dev/null
+++ b/src/proteus/bindings/python/clients/native.cpp
@@ -0,0 +1,68 @@
+// Copyright 2022 Advanced Micro Devices, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @file
+ * @brief Implements the Python bindings for the http.hpp header
+ */
+
+#include "proteus/clients/native.hpp"
+
+#include <pybind11/cast.h>      // for arg
+#include <pybind11/pybind11.h>  // for class_, init
+#include <pybind11/stl.h>       // IWYU pragma: keep
+
+#include <unordered_map>  // for unordered_map
+
+#include "proteus/bindings/python/helpers/docstrings.hpp"  // for DOCS
+#include "proteus/declarations.hpp"
+
+namespace py = pybind11;
+
+namespace proteus {
+
+void wrapNativeClient(py::module_ &m) {
+  py::class_<NativeClient, proteus::Client>(m, "NativeClient")
+    .def(py::init<>())
+    .def("serverMetadata", &NativeClient::serverMetadata,
+         DOCS(NativeClient, serverMetadata))
+    .def("serverLive", &NativeClient::serverLive,
+         DOCS(NativeClient, serverLive))
+    .def("serverReady", &NativeClient::serverReady,
+         DOCS(NativeClient, serverReady))
+    .def("modelReady", &NativeClient::modelReady, py::arg("model"),
+         DOCS(NativeClient, modelReady))
+    .def("modelMetadata", &NativeClient::modelMetadata, py::arg("model"),
+         DOCS(NativeClient, modelMetadata))
+    .def("modelLoad", &NativeClient::modelLoad, py::arg("model"),
+         py::arg("parameters") = proteus::RequestParameters(),
+         DOCS(NativeClient, modelLoad))
+    .def("modelUnload", &NativeClient::modelUnload, py::arg("model"),
+         DOCS(NativeClient, modelUnload))
+    .def("workerLoad", &NativeClient::workerLoad, py::arg("model"),
+         py::arg("parameters") = proteus::RequestParameters(),
+         DOCS(NativeClient, workerLoad))
+    .def("workerUnload", &NativeClient::workerUnload, py::arg("model"),
+         DOCS(NativeClient, workerUnload))
+    .def("modelInfer", &NativeClient::modelInfer, py::arg("model"),
+         py::arg("request"), DOCS(NativeClient, modelInfer))
+    // cannot wrap future directly in Python
+    // .def("modelInferAsync", &NativeClient::modelInferAsync, py::arg("model"),
+    //      py::arg("request"), DOCS(NativeClient, modelInferAsync))
+    .def("modelList", &NativeClient::modelList, DOCS(NativeClient, modelList))
+    .def("hasHardware", &NativeClient::hasHardware, py::arg("name"),
+         py::arg("num"), DOCS(NativeClient, hasHardware));
+}
+
+}  // namespace proteus
diff --git a/src/proteus/bindings/python/clients/websocket.cpp b/src/proteus/bindings/python/clients/websocket.cpp
index 68b515b73..b8537b648 100644
--- a/src/proteus/bindings/python/clients/websocket.cpp
+++ b/src/proteus/bindings/python/clients/websocket.cpp
@@ -32,7 +32,7 @@ class DataType;
 
 namespace py = pybind11;
 
-using proteus::DataType;
+namespace proteus {
 
 void wrapWebSocketClient(py::module_ &m) {
   using proteus::WebSocketClient;
@@ -58,11 +58,19 @@ void wrapWebSocketClient(py::module_ &m) {
          DOCS(WebSocketClient, modelUnload))
     .def("modelInfer", &WebSocketClient::modelInfer, py::arg("model"),
          py::arg("request"), DOCS(WebSocketClient, modelInfer))
+    // cannot wrap future directly in Python
+    // .def("modelInferAsync", &WebSocketClient::modelInferAsync,
+    // py::arg("model"),
+    //      py::arg("request"), DOCS(WebSocketClient, modelInferAsync))
     .def("modelInferWs", &WebSocketClient::modelInferWs, py::arg("model"),
          py::arg("request"), DOCS(WebSocketClient, modelInferWs))
     .def("modelRecv", &WebSocketClient::modelRecv,
          DOCS(WebSocketClient, modelRecv))
     .def("modelList", &WebSocketClient::modelList,
          DOCS(WebSocketClient, modelList))
+    .def("hasHardware", &WebSocketClient::hasHardware, py::arg("name"),
+         py::arg("num"), DOCS(WebSocketClient, hasHardware))
     .def("close", &WebSocketClient::close, DOCS(WebSocketClient, close));
 }
+
+}  // namespace proteus
diff --git a/src/proteus/bindings/python/core/CMakeLists.txt b/src/proteus/bindings/python/core/CMakeLists.txt
index 970512719..99aa3c043 100644
--- a/src/proteus/bindings/python/core/CMakeLists.txt
+++ b/src/proteus/bindings/python/core/CMakeLists.txt
@@ -16,6 +16,16 @@ list(APPEND TARGETS predict_api data_types)
 
 list(APPEND LINK_TARGETS)
 
+add_library(exceptions_py OBJECT exceptions.cpp)
+target_include_directories(
+  exceptions_py PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/.. ${PROTEUS_INCLUDE_DIRS}
+                        ${Python_INCLUDE_DIRS}
+)
+target_link_libraries(exceptions_py PRIVATE ${Python_LIBRARIES})
+add_dependencies(exceptions_py pybind11Mkdoc)
+
+set(LINK_TARGETS ${LINK_TARGETS} exceptions_py)
+
 foreach(target ${TARGETS})
   add_library(${target}_py OBJECT ${target}.cpp)
   target_include_directories(
@@ -31,7 +41,4 @@ foreach(target ${TARGETS})
   set(LINK_TARGETS ${LINK_TARGETS} ${target}_py)
 endforeach()
 
-# target_link_libraries(data_types_py PRIVATE
-# /usr/lib/python3/dist-packages/xir.cpython-36m-x86_64-linux-gnu.so)
-
 set(CORE_OBJS ${LINK_TARGETS} PARENT_SCOPE)
diff --git a/src/proteus/bindings/python/core/predict_api.hpp b/src/proteus/bindings/python/core/core.hpp
similarity index 54%
rename from src/proteus/bindings/python/core/predict_api.hpp
rename to src/proteus/bindings/python/core/core.hpp
index 2639a5a7e..0a447b2e3 100644
--- a/src/proteus/bindings/python/core/predict_api.hpp
+++ b/src/proteus/bindings/python/core/core.hpp
@@ -1,4 +1,4 @@
-// Copyright 2022 Xilinx Inc.
+// Copyright 2022 Advanced Micro Devices, Inc.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -14,17 +14,32 @@
 
 /**
  * @file
- * @brief Defines the objects for Python bindings
+ * @brief Defines the objects for Python bindings for the core
  */
 
-#ifndef GUARD_PROTEUS_BINDINGS_PYTHON_PREDICT_API
-#define GUARD_PROTEUS_BINDINGS_PYTHON_PREDICT_API
+#ifndef GUARD_PROTEUS_BINDINGS_PYTHON_CORE_CORE
+#define GUARD_PROTEUS_BINDINGS_PYTHON_CORE_CORE
 
 namespace pybind11 {
 class module_;
-}
+}  // namespace pybind11
+
+namespace proteus {
 
+void wrapDataType(pybind11::module_ &);
+void wrapTypeMaps(pybind11::module_ &);
 void wrapRequestParameters(pybind11::module_ &);
 void wrapPredictApi(pybind11::module_ &);
+void wrapExceptions(pybind11::module_ &);
+
+void wrapCore(pybind11::module_ &m) {
+  wrapExceptions(m);
+  wrapDataType(m);
+  wrapTypeMaps(m);
+  wrapRequestParameters(m);
+  wrapPredictApi(m);
+}
+
+}  // namespace proteus
 
-#endif  // GUARD_PROTEUS_BINDINGS_PYTHON_PREDICT_API
+#endif  // GUARD_PROTEUS_BINDINGS_PYTHON_CORE_CORE
diff --git a/src/proteus/bindings/python/core/data_types.cpp b/src/proteus/bindings/python/core/data_types.cpp
index aa81b4019..189d009b7 100644
--- a/src/proteus/bindings/python/core/data_types.cpp
+++ b/src/proteus/bindings/python/core/data_types.cpp
@@ -35,13 +35,14 @@
 
 namespace py = pybind11;
 
-using proteus::DataType;
+namespace proteus {
 
 void wrapDataType(py::module_& m) {
   auto datatype = py::class_<DataType>(m, "DataType");
   auto value = py::enum_<DataType::Value>(datatype, "Value");
 
   datatype.def(py::init<>())
+    .def(py::init<>())
     .def(py::init<const char*>())
     .def(py::init<DataType::Value>())
     .def_property_readonly_static("BOOL",
@@ -114,3 +115,5 @@ void wrapTypeMaps([[maybe_unused]] py::module_& m) {
   m.def("mapTypeToXir", proteus::mapTypeToXir, py::arg("type"));
 #endif
 }
+
+}  // namespace proteus
diff --git a/src/proteus/bindings/python/core/data_types.hpp b/src/proteus/bindings/python/core/data_types.hpp
deleted file mode 100644
index 22642ef0a..000000000
--- a/src/proteus/bindings/python/core/data_types.hpp
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright 2022 Xilinx Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-/**
- * @file
- * @brief Defines the objects for Python bindings
- */
-
-#ifndef GUARD_PROTEUS_BINDINGS_PYTHON_DATA_TYPES
-#define GUARD_PROTEUS_BINDINGS_PYTHON_DATA_TYPES
-
-namespace pybind11 {
-class module_;
-}
-
-void wrapDataType(pybind11::module_ &);
-void wrapTypeMaps(pybind11::module_ &);
-
-#endif  // GUARD_PROTEUS_BINDINGS_PYTHON_DATA_TYPES
diff --git a/src/proteus/bindings/python/core/exceptions.cpp b/src/proteus/bindings/python/core/exceptions.cpp
new file mode 100644
index 000000000..aab594bc2
--- /dev/null
+++ b/src/proteus/bindings/python/core/exceptions.cpp
@@ -0,0 +1,40 @@
+// Copyright 2022 Advanced Micro Devices, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @file
+ * @brief Implements the Python bindings for the exceptions
+ */
+
+#include "proteus/core/exceptions.hpp"
+
+#include <pybind11/pybind11.h>
+
+namespace py = pybind11;
+
+namespace proteus {
+
+void wrapExceptions(py::module_& m) {
+  py::register_exception<runtime_error>(m, "RuntimeError");
+  py::register_exception<bad_status>(m, "BadStatus");
+  py::register_exception<connection_error>(m, "ConnectionError");
+  py::register_exception<file_not_found_error>(m, "FileNotFoundError");
+  py::register_exception<file_read_error>(m, "FileReadError");
+  py::register_exception<external_error>(m, "ExternalError");
+  py::register_exception<invalid_argument>(m, "InvalidArgumentError");
+  py::register_exception<environment_not_set_error>(m,
+                                                    "EnvironmentNotSetError");
+}
+
+}  // namespace proteus
diff --git a/src/proteus/bindings/python/core/predict_api.cpp b/src/proteus/bindings/python/core/predict_api.cpp
index 8176f77dd..c6389afad 100644
--- a/src/proteus/bindings/python/core/predict_api.cpp
+++ b/src/proteus/bindings/python/core/predict_api.cpp
@@ -34,6 +34,8 @@
 
 namespace py = pybind11;
 
+namespace proteus {
+
 void wrapRequestParameters(py::module_ &m) {
   using proteus::RequestParameters;
 
@@ -169,6 +171,16 @@ void wrapRequestParameters(py::module_ &m) {
 //     };
 // }} // namespace pybind11::detail
 
+void wrapServerMetadata(py::module_ &m) {
+  py::class_<ServerMetadata>(m, "ServerMetadata")
+    .def(py::init<>(), DOCS(ServerMetadata))
+    .def_readwrite("name", &ServerMetadata::name, DOCS(ServerMetadata, name))
+    .def_readwrite("version", &ServerMetadata::version,
+                   DOCS(ServerMetadata, version))
+    .def_readwrite("extensions", &ServerMetadata::extensions,
+                   DOCS(ServerMetadata, extensions));
+}
+
 template <typename T>
 py::array_t<T> getData(const proteus::InferenceRequestInput &self) {
   auto *data = static_cast<T *>(self.getData());
@@ -182,21 +194,7 @@ void setData(proteus::InferenceRequestInput &self, py::array_t<T> &b) {
   self.setData(static_cast<void *>(const_cast<T *>(b.data())));
 }
 
-void wrapPredictApi(py::module_ &m) {
-  using proteus::InferenceRequest;
-  using proteus::InferenceRequestInput;
-  using proteus::InferenceRequestOutput;
-  using proteus::InferenceResponse;
-  using proteus::ServerMetadata;
-
-  py::class_<ServerMetadata>(m, "ServerMetadata")
-    .def(py::init<>(), DOCS(ServerMetadata))
-    .def_readwrite("name", &ServerMetadata::name, DOCS(ServerMetadata, name))
-    .def_readwrite("version", &ServerMetadata::version,
-                   DOCS(ServerMetadata, version))
-    .def_readwrite("extensions", &ServerMetadata::extensions,
-                   DOCS(ServerMetadata, extensions));
-
+void wrapInferenceRequestInput(py::module_ &m) {
   auto setShape =
     static_cast<void (InferenceRequestInput::*)(const std::vector<uint64_t> &)>(
       &InferenceRequestInput::setShape);
@@ -254,7 +252,9 @@ void wrapPredictApi(py::module_ &m) {
                   ")";
          })
     .def("__str__", &proteus::to_string<InferenceRequestInput>);
+}
 
+void wrapInferenceRequestOutput(py::module_ &m) {
   py::class_<InferenceRequestOutput>(m, "InferenceRequestOutput")
     .def(py::init<>(), DOCS(InferenceRequestOutput))
     .def_property("name", &InferenceRequestOutput::getName,
@@ -267,12 +267,9 @@ void wrapPredictApi(py::module_ &m) {
       (void)self;
       return "InferenceRequestOutput\n";
     });
-  // .def("__str__", [](const InferenceRequestOutput& self) {
-  //   std::ostringstream os;
-  //   os << self;
-  //   return os.str();
-  // });
+}
 
+void wrapInferenceResponse(py::module_ &m) {
   py::class_<InferenceResponse>(m, "InferenceResponse")
     .def(py::init<>(), DOCS(InferenceResponse, InferenceResponse))
     .def(py::init<const std::string &>(),
@@ -304,7 +301,9 @@ void wrapPredictApi(py::module_ &m) {
            return "InferenceResponse\n";
          })
     .def("__str__", &proteus::to_string<InferenceResponse>);
+}
 
+void wrapInferenceRequest(py::module_ &m) {
   auto addInputTensor =
     static_cast<void (InferenceRequest::*)(InferenceRequestInput)>(
       &InferenceRequest::addInputTensor);
@@ -339,13 +338,9 @@ void wrapPredictApi(py::module_ &m) {
       (void)self;
       return "InferenceRequest";
     });
-  // .def("__str__", [](const InferenceRequest& self) {
-  //   std::ostringstream os;
-  //   os << self;
-  //   return os.str();
-  // });
+}
 
-  using proteus::ModelMetadataTensor;
+void wrapModelMetadata(py::module_ &m) {
   py::class_<ModelMetadataTensor>(m, "ModelMetadataTensor")
     .def(
       py::init<const std::string &, proteus::DataType, std::vector<uint64_t>>(),
@@ -357,19 +352,18 @@ void wrapPredictApi(py::module_ &m) {
     .def("getShape", &ModelMetadataTensor::getShape,
          DOCS(ModelMetadataTensor, getShape));
 
-  using proteus::ModelMetadata;
-  auto addInputTensor2 = static_cast<void (ModelMetadata::*)(
+  auto addInputTensor = static_cast<void (ModelMetadata::*)(
     const std::string &, proteus::DataType, std::vector<int>)>(
     &ModelMetadata::addInputTensor);
-  auto addOutputTensor2 = static_cast<void (ModelMetadata::*)(
+  auto addOutputTensor = static_cast<void (ModelMetadata::*)(
     const std::string &, proteus::DataType, std::vector<int>)>(
     &ModelMetadata::addOutputTensor);
   py::class_<ModelMetadata>(m, "ModelMetadata")
     .def(py::init<const std::string &, const std::string &>(),
          DOCS(ModelMetadata, ModelMetadata))
-    .def("addInputTensor", addInputTensor2, py::keep_alive<1, 2>(),
+    .def("addInputTensor", addInputTensor, py::keep_alive<1, 2>(),
          DOCS(ModelMetadata, addInputTensor))
-    .def("addOutputTensor", addOutputTensor2, py::arg("name"),
+    .def("addOutputTensor", addOutputTensor, py::arg("name"),
          py::arg("datatype"), py::arg("shape"), py::keep_alive<1, 2>(),
          DOCS(ModelMetadata, addOutputTensor))
     .def_property("name", &ModelMetadata::getName, &ModelMetadata::setName)
@@ -378,3 +372,14 @@ void wrapPredictApi(py::module_ &m) {
     .def("setReady", &ModelMetadata::setReady, DOCS(ModelMetadata, setReady))
     .def("isReady", &ModelMetadata::isReady, DOCS(ModelMetadata, isReady));
 }
+
+void wrapPredictApi(py::module_ &m) {
+  wrapServerMetadata(m);
+  wrapInferenceRequestInput(m);
+  wrapInferenceRequestOutput(m);
+  wrapInferenceResponse(m);
+  wrapInferenceRequest(m);
+  wrapModelMetadata(m);
+}
+
+}  // namespace proteus
diff --git a/src/proteus/bindings/python/util/CMakeLists.txt b/src/proteus/bindings/python/pre_post/CMakeLists.txt
similarity index 64%
rename from src/proteus/bindings/python/util/CMakeLists.txt
rename to src/proteus/bindings/python/pre_post/CMakeLists.txt
index b12c9387d..a2a6b08f6 100644
--- a/src/proteus/bindings/python/util/CMakeLists.txt
+++ b/src/proteus/bindings/python/pre_post/CMakeLists.txt
@@ -12,15 +12,15 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-add_library(util_py OBJECT util.cpp)
+add_library(pre_post_py OBJECT pre_post.cpp)
 target_include_directories(
-  util_py PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/.. ${PROTEUS_INCLUDE_DIRS}
-                  ${Python_INCLUDE_DIRS}
+  pre_post_py PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/.. ${PROTEUS_INCLUDE_DIRS}
+                      ${Python_INCLUDE_DIRS}
 )
 target_link_libraries(
-  util_py PRIVATE ${Python_LIBRARIES} util opencv_core opencv_imgproc
-                  opencv_imgcodecs
+  pre_post_py PRIVATE ${Python_LIBRARIES} opencv_core opencv_imgproc
+                      opencv_imgcodecs
 )
-add_dependencies(util_py pybind11Mkdoc)
+add_dependencies(pre_post_py pybind11Mkdoc)
 
-set(UTIL_OBJS util_py PARENT_SCOPE)
+set(PRE_POST_OBJS pre_post_py PARENT_SCOPE)
diff --git a/src/proteus/bindings/python/util/util.cpp b/src/proteus/bindings/python/pre_post/pre_post.cpp
similarity index 66%
rename from src/proteus/bindings/python/util/util.cpp
rename to src/proteus/bindings/python/pre_post/pre_post.cpp
index 05da96ac6..eecac03c7 100644
--- a/src/proteus/bindings/python/util/util.cpp
+++ b/src/proteus/bindings/python/pre_post/pre_post.cpp
@@ -14,7 +14,7 @@
 
 /**
  * @file
- * @brief Implements the Python bindings for the server.hpp header
+ * @brief Implements the Python bindings for pre_post
  */
 
 #include <pybind11/cast.h>  // for arg
@@ -24,13 +24,15 @@
 
 #include "proteus/bindings/python/helpers/docstrings.hpp"
 #include "proteus/core/predict_api.hpp"
-#include "proteus/util/pre_post/image_preprocess.hpp"
-#include "proteus/util/pre_post/resnet50_postprocess.hpp"
+#include "proteus/pre_post/image_preprocess.hpp"
+#include "proteus/pre_post/resnet50_postprocess.hpp"
 
 namespace py = pybind11;
 
+namespace proteus {
+
 template <typename T>
-using ImagePreprocessOptions = proteus::util::ImagePreprocessOptions<T, 3>;
+using ImagePreprocessOptions = pre_post::ImagePreprocessOptions<T, 3>;
 
 template <typename T>
 void addPreprocessOptions(const py::module& m, const char* name) {
@@ -54,39 +56,40 @@ void addPreprocessOptions(const py::module& m, const char* name) {
 template <typename T>
 auto imagePreprocess(const std::vector<std::string>& paths,
                      const ImagePreprocessOptions<T>& options) {
-  auto images = proteus::util::imagePreprocess(paths, options);
+  auto images = pre_post::imagePreprocess(paths, options);
   py::array_t<T> ret = py::cast(images);
   return ret;
 }
 
-void wrapUtil(py::module_& m) {
-  py::module pre_post = m.def_submodule("pre_post", "pre_post documentation");
-  pre_post.def(
+void wrapPrePost(py::module_& m) {
+  m.def(
     "resnet50PostprocessInt8",
-    [](const proteus::InferenceResponseOutput& output, int k) {
-      return proteus::util::resnet50Postprocess<int8_t>(
+    [](const InferenceResponseOutput& output, int k) {
+      return pre_post::resnet50Postprocess<int8_t>(
         static_cast<int8_t*>(output.getData()), output.getSize(), k);
     }
 
     ,
     py::arg("output"), py::arg("k"));
-  pre_post.def(
+  m.def(
     "resnet50PostprocessFloat",
-    [](const proteus::InferenceResponseOutput& output, int k) {
-      return proteus::util::resnet50Postprocess<float>(
+    [](const InferenceResponseOutput& output, int k) {
+      return pre_post::resnet50Postprocess<float>(
         static_cast<float*>(output.getData()), output.getSize(), k);
     },
     py::arg("output"), py::arg("k"));
 
-  py::enum_<proteus::util::ImageOrder>(pre_post, "ImageOrder")
-    .value("NHWC", proteus::util::ImageOrder::NHWC)
-    .value("NCHW", proteus::util::ImageOrder::NCHW);
+  py::enum_<pre_post::ImageOrder>(m, "ImageOrder")
+    .value("NHWC", pre_post::ImageOrder::NHWC)
+    .value("NCHW", pre_post::ImageOrder::NCHW);
 
-  addPreprocessOptions<int8_t>(pre_post, "ImagePreprocessOptionsInt8");
-  addPreprocessOptions<float>(pre_post, "ImagePreprocessOptionsFloat");
+  addPreprocessOptions<int8_t>(m, "ImagePreprocessOptionsInt8");
+  addPreprocessOptions<float>(m, "ImagePreprocessOptionsFloat");
 
-  pre_post.def("imagePreprocessInt8", &imagePreprocess<int8_t>,
-               py::arg("paths"), py::arg("options"));
-  pre_post.def("imagePreprocessFloat", &imagePreprocess<float>,
-               py::arg("paths"), py::arg("options"));
+  m.def("imagePreprocessInt8", &imagePreprocess<int8_t>, py::arg("paths"),
+        py::arg("options"));
+  m.def("imagePreprocessFloat", &imagePreprocess<float>, py::arg("paths"),
+        py::arg("options"));
 }
+
+}  // namespace proteus
diff --git a/src/proteus/bindings/python/util/util.hpp b/src/proteus/bindings/python/pre_post/pre_post.hpp
similarity index 68%
rename from src/proteus/bindings/python/util/util.hpp
rename to src/proteus/bindings/python/pre_post/pre_post.hpp
index 22e125a96..2999915cb 100644
--- a/src/proteus/bindings/python/util/util.hpp
+++ b/src/proteus/bindings/python/pre_post/pre_post.hpp
@@ -14,16 +14,20 @@
 
 /**
  * @file
- * @brief Defines the Python bindings for the testing headers
+ * @brief Defines the Python bindings for pre_post
  */
 
-#ifndef GUARD_PROTEUS_BINDINGS_PYTHON_UTIL_UTIL
-#define GUARD_PROTEUS_BINDINGS_PYTHON_UTIL_UTIL
+#ifndef GUARD_PROTEUS_BINDINGS_PYTHON_PRE_POST_PRE_POST
+#define GUARD_PROTEUS_BINDINGS_PYTHON_PRE_POST_PRE_POST
 
 namespace pybind11 {
 class module_;
 }
 
-void wrapUtil(pybind11::module_ &);
+namespace proteus {
 
-#endif  // GUARD_PROTEUS_BINDINGS_PYTHON_UTIL_UTIL
+void wrapPrePost(pybind11::module_ &);
+
+}  // namespace proteus
+
+#endif  // GUARD_PROTEUS_BINDINGS_PYTHON_PRE_POST_PRE_POST
diff --git a/src/proteus/bindings/python/proteus.cpp b/src/proteus/bindings/python/proteus.cpp
index 7396b4f9c..eea2c7187 100644
--- a/src/proteus/bindings/python/proteus.cpp
+++ b/src/proteus/bindings/python/proteus.cpp
@@ -20,45 +20,29 @@
 
 #include <pybind11/pybind11.h>
 
-#include "proteus/bindings/python/client_operators/client_operators.hpp"
-#include "proteus/bindings/python/clients/client.hpp"
-#include "proteus/bindings/python/core/data_types.hpp"
-#include "proteus/bindings/python/core/predict_api.hpp"
-#include "proteus/bindings/python/servers/server.hpp"
+#include "proteus/bindings/python/clients/clients.hpp"
+#include "proteus/bindings/python/core/core.hpp"
+#include "proteus/bindings/python/pre_post/pre_post.hpp"
+#include "proteus/bindings/python/servers/servers.hpp"
 #include "proteus/bindings/python/testing/testing.hpp"
-#include "proteus/bindings/python/util/util.hpp"
 #include "proteus/build_options.hpp"
-#include "proteus/core/exceptions.hpp"
 
 namespace py = pybind11;
 
 namespace proteus {
 
 PYBIND11_MODULE(_proteus, m) {
-  py::module n = m.def_submodule("predict_api", "predict_api documentation");
-  py::module f =
-    m.def_submodule("client_operators", "client operators documentation");
-  py::module c = m.def_submodule("clients", "client documentation");
-  py::module s = m.def_submodule("servers", "server documentation");
   py::module t = m.def_submodule("testing", "testing documentation");
-  py::module u = m.def_submodule("util", "util documentation");
+  py::module p = m.def_submodule("pre_post", "pre/post documentation");
   m.doc() = "proteus inference library";
 
-  py::register_exception<runtime_error>(m, "RuntimeError");
-
-  wrapRequestParameters(m);
-  wrapDataType(m);
-  wrapTypeMaps(m);
-  wrapPredictApi(n);
-  wrapClient(c);
-  wrapHttpClient(c);
-  wrapWebSocketClient(c);
-  wrapServer(s);
-  wrapInferAsync(f);
+  wrapCore(m);
+  wrapClients(m);
+  wrapServers(m);
 #ifdef PROTEUS_BUILD_TESTING
   wrapTesting(t);
 #endif
-  wrapUtil(u);
+  wrapPrePost(p);
 }
 
 }  // namespace proteus
diff --git a/src/proteus/bindings/python/servers/server.cpp b/src/proteus/bindings/python/servers/server.cpp
index dc2c6b3d0..42de64149 100644
--- a/src/proteus/bindings/python/servers/server.cpp
+++ b/src/proteus/bindings/python/servers/server.cpp
@@ -27,13 +27,17 @@
 
 namespace py = pybind11;
 
-void wrapServer(py::module_ &m) {
-  using proteus::Server;
+namespace proteus {
 
+void wrapServer(py::module_ &m) {
   py::class_<Server>(m, "Server")
     .def(py::init<>(), DOCS(Server, Server))
     .def("startHttp", &Server::startHttp, py::arg("port"),
          DOCS(Server, startHttp))
+    .def("stopHttp", &Server::stopHttp, DOCS(Server, stopHttp))
     .def("startGrpc", &Server::startGrpc, py::arg("port"),
-         DOCS(Server, startGrpc));
+         DOCS(Server, startGrpc))
+    .def("stopGrpc", &Server::stopGrpc, DOCS(Server, stopGrpc));
 }
+
+}  // namespace proteus
diff --git a/src/proteus/bindings/python/servers/server.hpp b/src/proteus/bindings/python/servers/servers.hpp
similarity index 65%
rename from src/proteus/bindings/python/servers/server.hpp
rename to src/proteus/bindings/python/servers/servers.hpp
index 0f2b201b3..23653dfaa 100644
--- a/src/proteus/bindings/python/servers/server.hpp
+++ b/src/proteus/bindings/python/servers/servers.hpp
@@ -1,4 +1,4 @@
-// Copyright 2022 Xilinx Inc.
+// Copyright 2022 Advanced Micro Devices, Inc.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -17,13 +17,19 @@
  * @brief Defines the Python bindings for the server.hpp header
  */
 
-#ifndef GUARD_PROTEUS_BINDINGS_PYTHON_SERVERS_SERVER
-#define GUARD_PROTEUS_BINDINGS_PYTHON_SERVERS_SERVER
+#ifndef GUARD_PROTEUS_BINDINGS_PYTHON_SERVERS_SERVERS
+#define GUARD_PROTEUS_BINDINGS_PYTHON_SERVERS_SERVERS
 
 namespace pybind11 {
 class module_;
 }
 
-void wrapServer(pybind11::module_ &);
+namespace proteus {
 
-#endif  // GUARD_PROTEUS_BINDINGS_PYTHON_SERVERS_SERVER
+void wrapServer(pybind11::module_&);
+
+void wrapServers(pybind11::module_& m) { wrapServer(m); }
+
+}  // namespace proteus
+
+#endif  // GUARD_PROTEUS_BINDINGS_PYTHON_SERVERS_SERVERS
diff --git a/src/proteus/bindings/python/src/proteus/__init__.py b/src/proteus/bindings/python/src/proteus/__init__.py
index 1b3eac8aa..18a020ea7 100644
--- a/src/proteus/bindings/python/src/proteus/__init__.py
+++ b/src/proteus/bindings/python/src/proteus/__init__.py
@@ -70,9 +70,9 @@ def ImageInferenceRequest(images, asTensor=True):
 
     if not isinstance(images, list):
         images = [images]
-    request = predict_api.InferenceRequest()
+    request = InferenceRequest()
     for index, image in enumerate(images):
-        input_n = predict_api.InferenceRequestInput()
+        input_n = InferenceRequestInput()
         input_n.name = f"input{index}"
         if isinstance(image, str):
             if asTensor:
@@ -133,13 +133,13 @@ def parallel_infer(client, model, data, processes):
 
 
 def start_http_client_server(address: str, extension=None):
-    client = clients.HttpClient(address)
+    client = HttpClient(address)
     port = address.split(":")[-1]
 
     # if it's not already started, start it here
     start_server = not client.serverLive()
     if start_server:
-        server = servers.Server()
+        server = Server()
         server.startHttp(int(port))
         while not client.serverLive():
             time.sleep(1)
@@ -153,3 +153,71 @@ def start_http_client_server(address: str, extension=None):
             sys.exit(0)
 
     return client, server
+
+
+def _get_data(request_input: InferenceRequestInput):
+    datatype = request_input.datatype
+    if datatype == DataType.BOOL:
+        raise NotImplementedError("Bool datatype not supported")
+    if datatype == DataType.UINT8:
+        return request_input.getUint8Data()
+    if datatype == DataType.UINT16:
+        return request_input.getUint16Data()
+    if datatype == DataType.UINT32:
+        return request_input.getUint32Data()
+    if datatype == DataType.UINT64:
+        return request_input.getUint64Data()
+    if datatype == DataType.INT8:
+        return request_input.getInt8Data()
+    if datatype == DataType.INT16:
+        return request_input.getInt16Data()
+    if datatype == DataType.INT32:
+        return request_input.getInt32Data()
+    if datatype == DataType.INT64:
+        return request_input.getInt64Data()
+    if datatype == DataType.FP16:
+        return request_input.getFp16Data()
+    if datatype == DataType.FP32:
+        return request_input.getFp32Data()
+    if datatype == DataType.FP64:
+        return request_input.getFp64Data()
+    if datatype == DataType.STRING:
+        return request_input.getStringData()
+    raise NotImplementedError(f"{datatype.str()} not supported")
+
+
+def inference_request_to_dict(request: InferenceRequest):
+    import numpy as np
+
+    req = {}
+    if request.id:
+        req["id"] = request.id
+    if request.parameters:
+        for key, value in request.parameters:
+            req[key] = value
+    req["inputs"] = []
+    inputs = request.getInputs()
+    for inp in inputs:
+        new_input = {}
+        new_input["name"] = inp.name
+        new_input["shape"] = inp.shape
+        new_input["datatype"] = inp.datatype.str()
+        if inp.parameters:
+            for key, value in inp.parameters:
+                new_input[key] = value
+        data = _get_data(inp)
+        if isinstance(data, np.ndarray):
+            data = data.tolist()
+        new_input["data"] = data
+        req["inputs"].append(new_input)
+    outputs = request.getOutputs()
+    if outputs:
+        req["outputs"] = []
+        for output in outputs:
+            new_output = {}
+            new_output["name"] = output.name
+            if output.parameters:
+                for key, value in output.parameters:
+                    new_output[key] = value
+            req["outputs"].append(new_output)
+    return req
diff --git a/src/proteus/bindings/python/src/proteus/client_operators/__init__.py b/src/proteus/bindings/python/src/proteus/client_operators/__init__.py
deleted file mode 100644
index df330b7ab..000000000
--- a/src/proteus/bindings/python/src/proteus/client_operators/__init__.py
+++ /dev/null
@@ -1,16 +0,0 @@
-# Copyright 2022 Xilinx, Inc.
-# Copyright 2022 Advanced Micro Devices, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-from proteus._proteus.client_operators import *
diff --git a/src/proteus/bindings/python/src/proteus/clients/__init__.py b/src/proteus/bindings/python/src/proteus/clients/__init__.py
deleted file mode 100644
index 30596036e..000000000
--- a/src/proteus/bindings/python/src/proteus/clients/__init__.py
+++ /dev/null
@@ -1,16 +0,0 @@
-# Copyright 2022 Xilinx, Inc.
-# Copyright 2022 Advanced Micro Devices, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-from proteus._proteus.clients import *
diff --git a/src/proteus/bindings/python/src/proteus/util/__init__.py b/src/proteus/bindings/python/src/proteus/pre_post/__init__.py
similarity index 93%
rename from src/proteus/bindings/python/src/proteus/util/__init__.py
rename to src/proteus/bindings/python/src/proteus/pre_post/__init__.py
index 0bf386a3f..bb63aa62e 100644
--- a/src/proteus/bindings/python/src/proteus/util/__init__.py
+++ b/src/proteus/bindings/python/src/proteus/pre_post/__init__.py
@@ -13,4 +13,4 @@
 # limitations under the License.
 
 import proteus
-from proteus._proteus.util import *
+from proteus._proteus.pre_post import *
diff --git a/src/proteus/bindings/python/src/proteus/predict_api/__init__.py b/src/proteus/bindings/python/src/proteus/predict_api/__init__.py
deleted file mode 100644
index 53b9639fe..000000000
--- a/src/proteus/bindings/python/src/proteus/predict_api/__init__.py
+++ /dev/null
@@ -1,86 +0,0 @@
-# Copyright 2022 Xilinx, Inc.
-# Copyright 2022 Advanced Micro Devices, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-from proteus._proteus.predict_api import *
-
-
-def get_data(request_input: InferenceRequestInput):
-    from _proteus import DataType
-
-    datatype = request_input.datatype
-    if datatype == DataType.BOOL:
-        raise NotImplementedError("Bool datatype not supported")
-    if datatype == DataType.UINT8:
-        return request_input.getUint8Data()
-    if datatype == DataType.UINT16:
-        return request_input.getUint16Data()
-    if datatype == DataType.UINT32:
-        return request_input.getUint32Data()
-    if datatype == DataType.UINT64:
-        return request_input.getUint64Data()
-    if datatype == DataType.INT8:
-        return request_input.getInt8Data()
-    if datatype == DataType.INT16:
-        return request_input.getInt16Data()
-    if datatype == DataType.INT32:
-        return request_input.getInt32Data()
-    if datatype == DataType.INT64:
-        return request_input.getInt64Data()
-    if datatype == DataType.FP16:
-        return request_input.getFp16Data()
-    if datatype == DataType.FP32:
-        return request_input.getFp32Data()
-    if datatype == DataType.FP64:
-        return request_input.getFp64Data()
-    if datatype == DataType.STRING:
-        return request_input.getStringData()
-    raise NotImplementedError(f"{datatype.str()} not supported")
-
-
-def to_dict(request: InferenceRequest):
-    import numpy as np
-
-    req = {}
-    if request.id:
-        req["id"] = request.id
-    if request.parameters:
-        for key, value in request.parameters:
-            req[key] = value
-    req["inputs"] = []
-    inputs = request.getInputs()
-    for inp in inputs:
-        new_input = {}
-        new_input["name"] = inp.name
-        new_input["shape"] = inp.shape
-        new_input["datatype"] = inp.datatype.str()
-        if inp.parameters:
-            for key, value in inp.parameters:
-                new_input[key] = value
-        data = get_data(inp)
-        if isinstance(data, np.ndarray):
-            data = data.tolist()
-        new_input["data"] = data
-        req["inputs"].append(new_input)
-    outputs = request.getOutputs()
-    if outputs:
-        req["outputs"] = []
-        for output in outputs:
-            new_output = {}
-            new_output["name"] = output.name
-            if output.parameters:
-                for key, value in output.parameters:
-                    new_output[key] = value
-            req["outputs"].append(new_output)
-    return req
diff --git a/src/proteus/bindings/python/src/proteus/servers/__init__.py b/src/proteus/bindings/python/src/proteus/servers/__init__.py
deleted file mode 100644
index c7d0fe603..000000000
--- a/src/proteus/bindings/python/src/proteus/servers/__init__.py
+++ /dev/null
@@ -1,16 +0,0 @@
-# Copyright 2022 Xilinx, Inc.
-# Copyright 2022 Advanced Micro Devices, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-from proteus._proteus.servers import *
diff --git a/src/proteus/bindings/python/src/proteus/util/pre_post/__init__.py b/src/proteus/bindings/python/src/proteus/util/pre_post/__init__.py
deleted file mode 100644
index 1cdbd1073..000000000
--- a/src/proteus/bindings/python/src/proteus/util/pre_post/__init__.py
+++ /dev/null
@@ -1,16 +0,0 @@
-# Copyright 2022 Advanced Micro Devices, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import proteus
-from proteus._proteus.util.pre_post import *
diff --git a/src/proteus/client_operators/CMakeLists.txt b/src/proteus/client_operators/CMakeLists.txt
deleted file mode 100644
index c874f9431..000000000
--- a/src/proteus/client_operators/CMakeLists.txt
+++ /dev/null
@@ -1,30 +0,0 @@
-# Copyright 2022 Xilinx Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-list(APPEND files infer_async)
-
-foreach(file ${files})
-  set(target ${file})
-  add_library(${target} OBJECT ${file}.cpp)
-  target_include_directories(${target} PRIVATE ${PROTEUS_INCLUDE_DIRS})
-  enable_ipo_on_target(${target})
-
-  list(APPEND CLIENT_OPERATORS_OBJS $<TARGET_OBJECTS:${target}>)
-  list(APPEND targets ${target})
-endforeach()
-
-add_library(client_operators STATIC ${CLIENT_OPERATORS_OBJS})
-target_link_libraries(client_operators INTERFACE ${targets})
-
-set(CLIENT_OPERATORS_OBJS ${CLIENT_OPERATORS_OBJS} PARENT_SCOPE)
diff --git a/src/proteus/client_operators/infer_async.cpp b/src/proteus/client_operators/infer_async.cpp
deleted file mode 100644
index d0fc8edb0..000000000
--- a/src/proteus/client_operators/infer_async.cpp
+++ /dev/null
@@ -1,77 +0,0 @@
-// Copyright 2022 Xilinx Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include "proteus/client_operators/infer_async.hpp"
-
-#include <queue>
-#include <thread>
-
-namespace proteus {
-
-std::vector<InferenceResponse> inferAsyncOrdered(
-  Client* client, const std::string& model,
-  const std::vector<InferenceRequest>& requests) {
-  std::queue<InferenceResponseFuture> q;
-  for (const auto& request : requests) {
-    q.push(client->modelInferAsync(model, request));
-  }
-
-  const auto num_requests = requests.size();
-  std::vector<InferenceResponse> responses;
-  responses.reserve(num_requests);
-  for (auto i = 0U; i < num_requests; ++i) {
-    auto& future = q.front();
-    responses.push_back(future.get());
-    q.pop();
-  }
-  return responses;
-}
-
-std::vector<InferenceResponse> inferAsyncOrderedBatched(
-  Client* client, const std::string& model,
-  const std::vector<InferenceRequest>& requests, size_t batch_size) {
-  auto num_requests = requests.size();
-  std::vector<InferenceResponse> responses;
-  responses.reserve(num_requests);
-  auto start_index = 0U;
-  std::queue<InferenceResponseFuture> q;
-
-  while (start_index + batch_size < num_requests) {
-    for (auto i = start_index; i < batch_size; ++i) {
-      q.push(client->modelInferAsync(model, requests[i]));
-    }
-
-    for (auto i = 0U; i < batch_size; ++i) {
-      auto& future = q.front();
-      responses.push_back(future.get());
-      q.pop();
-    }
-    start_index += batch_size;
-  }
-
-  if (start_index != num_requests) {
-    for (auto i = start_index; i < num_requests; ++i) {
-      q.push(client->modelInferAsync(model, requests[i]));
-    }
-
-    for (auto i = 0U; i < num_requests - start_index; ++i) {
-      auto& future = q.front();
-      responses.push_back(future.get());
-      q.pop();
-    }
-  }
-  return responses;
-}
-
-}  // namespace proteus
diff --git a/src/proteus/clients/client.cpp b/src/proteus/clients/client.cpp
index 09cf27d2f..411143f0f 100644
--- a/src/proteus/clients/client.cpp
+++ b/src/proteus/clients/client.cpp
@@ -15,6 +15,8 @@
 
 #include "proteus/clients/client.hpp"
 
+#include <queue>
+
 #include "proteus/build_options.hpp"        // for PROTEUS_ENABLE_LOGGING
 #include "proteus/observation/logging.hpp"  // for getLogDirectory, initLogger
 
@@ -60,4 +62,59 @@ void waitUntilModelReady(const Client* client, const std::string& model) {
   }
 }
 
+std::vector<InferenceResponse> inferAsyncOrdered(
+  Client* client, const std::string& model,
+  const std::vector<InferenceRequest>& requests) {
+  std::queue<InferenceResponseFuture> q;
+  for (const auto& request : requests) {
+    q.push(client->modelInferAsync(model, request));
+  }
+
+  const auto num_requests = requests.size();
+  std::vector<InferenceResponse> responses;
+  responses.reserve(num_requests);
+  for (auto i = 0U; i < num_requests; ++i) {
+    auto& future = q.front();
+    responses.push_back(future.get());
+    q.pop();
+  }
+  return responses;
+}
+
+std::vector<InferenceResponse> inferAsyncOrderedBatched(
+  Client* client, const std::string& model,
+  const std::vector<InferenceRequest>& requests, size_t batch_size) {
+  auto num_requests = requests.size();
+  std::vector<InferenceResponse> responses;
+  responses.reserve(num_requests);
+  auto start_index = 0U;
+  std::queue<InferenceResponseFuture> q;
+
+  while (start_index + batch_size < num_requests) {
+    for (auto i = start_index; i < batch_size; ++i) {
+      q.push(client->modelInferAsync(model, requests[i]));
+    }
+
+    for (auto i = 0U; i < batch_size; ++i) {
+      auto& future = q.front();
+      responses.push_back(future.get());
+      q.pop();
+    }
+    start_index += batch_size;
+  }
+
+  if (start_index != num_requests) {
+    for (auto i = start_index; i < num_requests; ++i) {
+      q.push(client->modelInferAsync(model, requests[i]));
+    }
+
+    for (auto i = 0U; i < num_requests - start_index; ++i) {
+      auto& future = q.front();
+      responses.push_back(future.get());
+      q.pop();
+    }
+  }
+  return responses;
+}
+
 }  // namespace proteus
diff --git a/src/proteus/clients/grpc.cpp b/src/proteus/clients/grpc.cpp
index 6ad1df49e..634169f55 100644
--- a/src/proteus/clients/grpc.cpp
+++ b/src/proteus/clients/grpc.cpp
@@ -118,6 +118,9 @@ bool GrpcClient::serverReady() const {
   if (status.ok()) {
     return reply.ready();
   }
+  if (status.error_code() == ::grpc::StatusCode::UNAVAILABLE) {
+    throw connection_error(status.error_message());
+  }
   throw bad_status(status.error_message());
 }
 
diff --git a/src/proteus/clients/http.cpp b/src/proteus/clients/http.cpp
index 5a6b089b4..b7166a132 100644
--- a/src/proteus/clients/http.cpp
+++ b/src/proteus/clients/http.cpp
@@ -112,8 +112,18 @@ void check_error(drogon::ReqResult result) {
   switch (result) {
     case ReqResult::Ok:
       break;
+    case ReqResult::BadResponse:
+      throw bad_status{"Bad response"};
+    case ReqResult::NetworkFailure:
+      throw bad_status{"Network failure"};
     case ReqResult::BadServerAddress:
       throw connection_error{"Cannot connect to the server"};
+    case ReqResult::Timeout:
+      throw bad_status{"Timeout"};
+    case ReqResult::HandshakeError:
+      throw bad_status{"Handshake error"};
+    case ReqResult::InvalidCertificate:
+      throw bad_status{"Invalid certificate"};
     default:
       throw bad_status{"Request error code: " +
                        std::to_string(static_cast<int>(result))};
diff --git a/src/proteus/core/manager.cpp b/src/proteus/core/manager.cpp
index 1e479ffa6..21e1570d2 100644
--- a/src/proteus/core/manager.cpp
+++ b/src/proteus/core/manager.cpp
@@ -36,6 +36,8 @@ Manager::Manager() {
   init();
 }
 
+Manager::~Manager() { shutdown(); }
+
 void Manager::init() {
   // default constructed threads are not joinable
   if (!update_thread_.joinable()) {
@@ -125,9 +127,9 @@ std::vector<std::string> Manager::getWorkerEndpoints() {
 // TODO(varunsh): if multiple commands sent post-shutdown, they will linger
 // in the queue and may cause problems
 void Manager::shutdown() {
-  auto request = std::make_shared<UpdateCommand>(UpdateCommandType::Shutdown);
-  this->update_queue_->enqueue(request);
   if (this->update_thread_.joinable()) {
+    auto request = std::make_shared<UpdateCommand>(UpdateCommandType::Shutdown);
+    this->update_queue_->enqueue(request);
     this->update_thread_.join();
   }
 }
diff --git a/src/proteus/core/manager.hpp b/src/proteus/core/manager.hpp
index 49846c2a3..0351b2136 100644
--- a/src/proteus/core/manager.hpp
+++ b/src/proteus/core/manager.hpp
@@ -145,7 +145,7 @@ class Manager {
   /// Construct a new Manager object
   Manager();
   /// Destroy the Manager object
-  ~Manager() = default;
+  ~Manager();
 
   /**
    * @brief The Endpoints class is a helper class to bundle up all the worker
diff --git a/src/proteus/util/pre_post/center_crop.hpp b/src/proteus/pre_post/center_crop.hpp
similarity index 85%
rename from src/proteus/util/pre_post/center_crop.hpp
rename to src/proteus/pre_post/center_crop.hpp
index a8357d1e3..6720f4970 100644
--- a/src/proteus/util/pre_post/center_crop.hpp
+++ b/src/proteus/pre_post/center_crop.hpp
@@ -12,12 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef GUARD_PROTEUS_UTIL_PRE_POST_CENTER_CROP
-#define GUARD_PROTEUS_UTIL_PRE_POST_CENTER_CROP
+#ifndef GUARD_PROTEUS_PRE_POST_CENTER_CROP
+#define GUARD_PROTEUS_PRE_POST_CENTER_CROP
 
 #include <opencv2/core.hpp>  // for cv::Mat
 
-namespace proteus::util {
+namespace proteus::pre_post {
 
 /**
  * @brief Image preprocessing helper.  Crops the input `img` from center to a
@@ -36,6 +36,6 @@ inline cv::Mat centerCrop(cv::Mat img, int height, int width) {
   return img;
 }
 
-}  // namespace proteus::util
+}  // namespace proteus::pre_post
 
-#endif  // GUARD_PROTEUS_UTIL_PRE_POST_CENTER_CROP
+#endif  // GUARD_PROTEUS_PRE_POST_CENTER_CROP
diff --git a/src/proteus/util/pre_post/get_top_k.hpp b/src/proteus/pre_post/get_top_k.hpp
similarity index 86%
rename from src/proteus/util/pre_post/get_top_k.hpp
rename to src/proteus/pre_post/get_top_k.hpp
index bc1e086b8..eacd1a2c8 100644
--- a/src/proteus/util/pre_post/get_top_k.hpp
+++ b/src/proteus/pre_post/get_top_k.hpp
@@ -12,13 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef GUARD_PROTEUS_UTIL_PRE_POST_GET_TOP_K
-#define GUARD_PROTEUS_UTIL_PRE_POST_GET_TOP_K
+#ifndef GUARD_PROTEUS_PRE_POST_GET_TOP_K
+#define GUARD_PROTEUS_PRE_POST_GET_TOP_K
 
 #include <queue>
 #include <vector>
 
-namespace proteus::util {
+namespace proteus::pre_post {
 
 /**
  * @brief After running softmax, get the labels associated with the top k values
@@ -45,6 +45,6 @@ std::vector<int> get_top_k(const double* d, size_t size, int k) {
   return topKIndex;
 }
 
-}  // namespace proteus::util
+}  // namespace proteus::pre_post
 
-#endif  // GUARD_PROTEUS_UTIL_PRE_POST_GET_TOP_K
+#endif  // GUARD_PROTEUS_PRE_POST_GET_TOP_K
diff --git a/src/proteus/util/pre_post/image_preprocess.hpp b/src/proteus/pre_post/image_preprocess.hpp
similarity index 94%
rename from src/proteus/util/pre_post/image_preprocess.hpp
rename to src/proteus/pre_post/image_preprocess.hpp
index d356ca24c..b30dea291 100644
--- a/src/proteus/util/pre_post/image_preprocess.hpp
+++ b/src/proteus/pre_post/image_preprocess.hpp
@@ -12,8 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef GUARD_PROTEUS_UTIL_PRE_POST_IMAGE_PREPROCESS
-#define GUARD_PROTEUS_UTIL_PRE_POST_IMAGE_PREPROCESS
+#ifndef GUARD_PROTEUS_PRE_POST_IMAGE_PREPROCESS
+#define GUARD_PROTEUS_PRE_POST_IMAGE_PREPROCESS
 
 #include <array>
 #include <cassert>
@@ -25,9 +25,9 @@
 #include <string>
 #include <vector>
 
-#include "proteus/util/pre_post/center_crop.hpp"
+#include "proteus/pre_post/center_crop.hpp"
 
-namespace proteus::util {
+namespace proteus::pre_post {
 
 enum class ImageOrder {
   NHWC,
@@ -165,6 +165,6 @@ std::vector<std::vector<T>> imagePreprocess(
   return outputs;
 }
 
-}  // namespace proteus::util
+}  // namespace proteus::pre_post
 
-#endif  // GUARD_PROTEUS_UTIL_PRE_POST_IMAGE_PREPROCESS
+#endif  // GUARD_PROTEUS_PRE_POST_IMAGE_PREPROCESS
diff --git a/src/proteus/util/pre_post/resnet50_postprocess.hpp b/src/proteus/pre_post/resnet50_postprocess.hpp
similarity index 76%
rename from src/proteus/util/pre_post/resnet50_postprocess.hpp
rename to src/proteus/pre_post/resnet50_postprocess.hpp
index f535863ca..ab972f313 100644
--- a/src/proteus/util/pre_post/resnet50_postprocess.hpp
+++ b/src/proteus/pre_post/resnet50_postprocess.hpp
@@ -12,15 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef GUARD_PROTEUS_UTIL_PRE_POST_RESNET50_POSTPROCESS
-#define GUARD_PROTEUS_UTIL_PRE_POST_RESNET50_POSTPROCESS
+#ifndef GUARD_PROTEUS_PRE_POST_RESNET50_POSTPROCESS
+#define GUARD_PROTEUS_PRE_POST_RESNET50_POSTPROCESS
 
 #include <vector>
 
-#include "proteus/util/pre_post/get_top_k.hpp"
-#include "proteus/util/pre_post/softmax.hpp"
+#include "proteus/pre_post/get_top_k.hpp"
+#include "proteus/pre_post/softmax.hpp"
 
-namespace proteus::util {
+namespace proteus::pre_post {
 
 /**
  * @brief Perform postprocessing of the data
@@ -39,6 +39,6 @@ std::vector<int> resnet50Postprocess(const T* data, size_t size, int k) {
   return get_top_k(softmax.data(), size, k);
 }
 
-}  // namespace proteus::util
+}  // namespace proteus::pre_post
 
-#endif  // GUARD_PROTEUS_UTIL_PRE_POST_RESNET50_POSTPROCESS
+#endif  // GUARD_PROTEUS_PRE_POST_RESNET50_POSTPROCESS
diff --git a/src/proteus/util/pre_post/softmax.hpp b/src/proteus/pre_post/softmax.hpp
similarity index 86%
rename from src/proteus/util/pre_post/softmax.hpp
rename to src/proteus/pre_post/softmax.hpp
index edce9c8f3..ac6ca94dc 100644
--- a/src/proteus/util/pre_post/softmax.hpp
+++ b/src/proteus/pre_post/softmax.hpp
@@ -12,12 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef GUARD_PROTEUS_UTIL_PRE_POST_SOFTMAX
-#define GUARD_PROTEUS_UTIL_PRE_POST_SOFTMAX
+#ifndef GUARD_PROTEUS_PRE_POST_SOFTMAX
+#define GUARD_PROTEUS_PRE_POST_SOFTMAX
 
 #include <cstddef>
 
-namespace proteus::util {
+namespace proteus::pre_post {
 
 /**
  * @brief Calculate softmax of the data
@@ -48,6 +48,6 @@ void calc_softmax(const T* data, size_t size, double* result) {
   }
 }
 
-}  // namespace proteus::util
+}  // namespace proteus::pre_post
 
-#endif  // GUARD_PROTEUS_UTIL_PRE_POST_SOFTMAX
+#endif  // GUARD_PROTEUS_PRE_POST_SOFTMAX
diff --git a/src/proteus/util/CMakeLists.txt b/src/proteus/util/CMakeLists.txt
index b56585df5..ff92ca07b 100644
--- a/src/proteus/util/CMakeLists.txt
+++ b/src/proteus/util/CMakeLists.txt
@@ -34,7 +34,7 @@ endforeach()
 target_link_libraries(base64 INTERFACE b64)
 target_link_libraries(compression INTERFACE z)
 
-add_library(util SHARED ${UTIL_OBJS})
+add_library(util STATIC ${UTIL_OBJS})
 target_link_libraries(util PRIVATE ${targets})
 set_target_properties(util PROPERTIES OUTPUT_NAME proteus-util)
 set_target_properties(
diff --git a/tools/benchmark.py b/tools/benchmark.py
index bcb6793f0..0c2f00096 100644
--- a/tools/benchmark.py
+++ b/tools/benchmark.py
@@ -41,7 +41,6 @@
 from rich.table import Table
 
 import proteus
-import proteus.clients
 
 
 class Highlight(Enum):
@@ -637,7 +636,7 @@ def combine_wrk_stats(samples):
 
 
 def wrk_benchmarks(config: Config, benchmarks: Benchmarks):
-    client = proteus.clients.HttpClient(config.http_address)
+    client = proteus.HttpClient(config.http_address)
     parsed_url = urllib.parse.urlsplit(config.http_address)
     hostname = parsed_url.hostname
     addr = socket.gethostbyname(hostname)
@@ -646,7 +645,7 @@ def wrk_benchmarks(config: Config, benchmarks: Benchmarks):
         server = None
     else:
         if not client.serverLive():
-            server = proteus.servers.Server()
+            server = proteus.Server()
             server.startHttp(parsed_url.port)
             while not client.serverLive():
                 time.sleep(1)
@@ -949,12 +948,12 @@ def pytest_benchmarks(config: Config, quiet=False):
     parsed_url = urllib.parse.urlsplit(config.http_address)
     hostname = parsed_url.hostname
     port = parsed_url.port
-    client = proteus.clients.HttpClient(config.http_address)
+    client = proteus.HttpClient(config.http_address)
     addr = socket.gethostbyname(hostname)
     if not ipaddress.ip_address(addr).is_loopback:
         try:
             assert client.serverLive()
-        except proteus.exceptions.ConnectionError:
+        except proteus.ConnectionError:
             print(
                 f"Cannot connect to HTTP server at {config.http_address}. Check the address or set it to start automatically"
             )


Test patch:
diff --git a/tests/api/CMakeLists.txt b/tests/api/CMakeLists.txt
index 0c480f52d..538c9bca1 100644
--- a/tests/api/CMakeLists.txt
+++ b/tests/api/CMakeLists.txt
@@ -15,6 +15,7 @@
 
 list(
   APPEND tests
+         infer_async
          model_infer
          model_infer_async
          model_list
diff --git a/tests/unit/client_operators/test_infer_async.cpp b/tests/api/test_infer_async.cpp
similarity index 96%
rename from tests/unit/client_operators/test_infer_async.cpp
rename to tests/api/test_infer_async.cpp
index 82e138661..267b17f14 100644
--- a/tests/unit/client_operators/test_infer_async.cpp
+++ b/tests/api/test_infer_async.cpp
@@ -16,7 +16,7 @@
 #include <iostream>
 
 #include "gtest/gtest.h"  // for Test, SuiteApiResolver, TEST
-#include "proteus/client_operators/infer_async.hpp"
+#include "proteus/clients/client.hpp"
 #include "proteus/clients/native.hpp"
 #include "proteus/core/predict_api.hpp"
 #include "proteus/testing/gtest_fixtures.hpp"  // for BaseFixture
diff --git a/tests/api/test_model_infer.py b/tests/api/test_model_infer.py
index fa5ab6298..ba8a8ad43 100644
--- a/tests/api/test_model_infer.py
+++ b/tests/api/test_model_infer.py
@@ -18,7 +18,6 @@
 import pytest
 
 import proteus
-import proteus.predict_api
 
 
 @pytest.mark.usefixtures("server", "assign_client")
@@ -35,10 +34,10 @@ def test_model_infer(self):
         endpoint = self.rest_client.workerLoad("echo")
         assert endpoint == "echo"
 
-        input_data = proteus.predict_api.InferenceRequestInput()
+        input_data = proteus.InferenceRequestInput()
         input_data.shape = [1]
         input_data.setUint32Data(np.array([1], np.uint32))
-        request = proteus.predict_api.InferenceRequest()
+        request = proteus.InferenceRequest()
         request.addInputTensor(input_data)
 
         response = self.rest_client.modelInfer(endpoint, request)
diff --git a/tests/conftest.py b/tests/conftest.py
index 0d2deb3a1..28aaa9a10 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -25,8 +25,6 @@
 from xprocess import ProcessStarter
 
 import proteus
-import proteus.clients
-import proteus.servers
 
 from helper import build_path, kDefaultHttpPort, root_path, run_path
 
@@ -48,7 +46,7 @@ def pytest_sessionstart(session):
     addr = socket.gethostbyname(session.config.getoption("hostname"))
     ip_addr = ipaddress.ip_address(addr)
     if not ip_addr.is_loopback:
-        client = proteus.clients.HttpClient(http_server_addr)
+        client = proteus.HttpClient(http_server_addr)
         if not client.serverLive():
             pytest.exit(f"No HTTP server found at {http_server_addr}", returncode=1)
 
@@ -158,11 +156,11 @@ def pytest_collection_modifyitems(config, items):
 
     add_cpp_markers(items, config.getoption("--cpp"))
 
-    client = proteus.clients.HttpClient(http_server_addr)
+    client = proteus.HttpClient(http_server_addr)
     if not client.serverLive():
-        server = proteus.servers.Server()
+        server = proteus.Server()
         server.startHttp(8998)
-        client = proteus.clients.HttpClient("http://127.0.0.1:8998")
+        client = proteus.HttpClient("http://127.0.0.1:8998")
         while not client.serverLive():
             time.sleep(1)
 
@@ -183,7 +181,7 @@ def pytest_collection_modifyitems(config, items):
     else:
         fpgas_avail = {}
 
-    client = proteus.clients.HttpClient(http_server_addr)
+    client = proteus.HttpClient(http_server_addr)
     for item in items:
         for mark in item.iter_markers(name="fpgas"):
             fpga_i = mark.args[0]
@@ -221,7 +219,7 @@ def server(xprocess):
     global http_server_addr
     global proteus_command
 
-    client = proteus.clients.HttpClient(http_server_addr)
+    client = proteus.HttpClient(http_server_addr)
     if not client.serverLive():
 
         class Starter(ProcessStarter):
@@ -282,12 +280,12 @@ def load(request, server):
 
 def rest_client(request):
     address = get_http_addr(request.config)
-    return proteus.clients.HttpClient("http://" + address)
+    return proteus.HttpClient("http://" + address)
 
 
 def ws_client(request):
     address = get_http_addr(request.config)
-    return proteus.clients.WebSocketClient("ws://" + address, "http://" + address)
+    return proteus.WebSocketClient("ws://" + address, "http://" + address)
 
 
 @pytest.fixture(scope="class")
diff --git a/tests/helper.py b/tests/helper.py
index 246caafac..e454ca715 100644
--- a/tests/helper.py
+++ b/tests/helper.py
@@ -17,7 +17,7 @@
 import pathlib
 import re
 
-from proteus.predict_api import to_dict
+from proteus import inference_request_to_dict
 
 root = os.getenv("PROTEUS_ROOT")
 if root is None:
@@ -68,7 +68,7 @@ def run_benchmark_func(benchmark, func, **kwargs):
 
 
 def run_benchmark(benchmark, benchmark_name, func, request, **kwargs):
-    write_lua(benchmark_name, json.dumps(to_dict(request)))
+    write_lua(benchmark_name, json.dumps(inference_request_to_dict(request)))
     benchmark.extra_info["lua"] = benchmark_name
     benchmark.extra_info["model"] = kwargs["model"]
     for key, value in kwargs.items():
diff --git a/tests/performance/models/test_resnet50.cpp b/tests/performance/models/test_resnet50.cpp
index bddf95cb3..b66a4407d 100644
--- a/tests/performance/models/test_resnet50.cpp
+++ b/tests/performance/models/test_resnet50.cpp
@@ -23,12 +23,11 @@
 #include <string>
 #include <vector>
 
-#include "proteus/client_operators/infer_async.hpp"
+#include "proteus/pre_post/image_preprocess.hpp"
 #include "proteus/proteus.hpp"  // for InferenceResponse, Grp...
 #include "proteus/testing/get_path_to_asset.hpp"  // for getPathToAsset
 #include "proteus/testing/gtest_fixtures.hpp"     // for GrpcFixture
 #include "proteus/util/ctpl.h"                    // for thread_pool
-#include "proteus/util/pre_post/image_preprocess.hpp"
 
 namespace fs = std::filesystem;
 
@@ -44,7 +43,7 @@ struct Config {
 };
 
 template <typename T, int C>
-using ImagePreprocessOptions = proteus::util::ImagePreprocessOptions<T, C>;
+using ImagePreprocessOptions = proteus::pre_post::ImagePreprocessOptions<T, C>;
 
 struct Workers {
   const fs::path kRoot{std::getenv("PROTEUS_ROOT")};
@@ -64,9 +63,9 @@ struct PtzendnnWorker : public Workers {
 
     parameters.put("model", graph.string());
 
-    proteus::util::ImagePreprocessOptions<float, 3> options;
+    proteus::pre_post::ImagePreprocessOptions<float, 3> options;
     options.normalize = true;
-    options.order = proteus::util::ImageOrder::NCHW;
+    options.order = proteus::pre_post::ImageOrder::NCHW;
     options.mean = {0.485, 0.456, 0.406};
     options.std = {4.367, 4.464, 4.444};
     options.convert_color = true;
@@ -91,7 +90,7 @@ struct TfzendnnWorker : public Workers {
     parameters.put("inter_op", 64);
     parameters.put("intra_op", 1);
 
-    proteus::util::ImagePreprocessOptions<float, 3> options;
+    proteus::pre_post::ImagePreprocessOptions<float, 3> options;
     options.convert_color = true;
     options.color_code = cv::COLOR_BGR2RGB;
     options.assign = true;
@@ -144,7 +143,7 @@ void test(proteus::Client* client, const Config& config, Workers* worker) {
   std::visit(
     Overload{
       [&](const ImagePreprocessOptions<float, 3>& options_) {
-        auto images = proteus::util::imagePreprocess(paths, options_);
+        auto images = proteus::pre_post::imagePreprocess(paths, options_);
         request.addInputTensor(images[0].data(), {3, kInputSize, kInputSize},
                                proteus::DataType::FLOAT32);
       },
diff --git a/tests/test.sh b/tests/test.sh
index 7983c7b70..13aeb2d6f 100755
--- a/tests/test.sh
+++ b/tests/test.sh
@@ -146,6 +146,7 @@ if [[ $MODE == "examples" || $MODE == "all" ]]; then
   for file in $(find $examples_path -type f -print); do
     if [[ $file == *.py ]]; then
       python3 $(realpath $file)
+      echo ""
     fi
   done
 
@@ -153,6 +154,7 @@ if [[ $MODE == "examples" || $MODE == "all" ]]; then
     real_file=$(realpath $file)
     if [ -x $real_file ]; then
       $real_file
+      echo ""
     fi
   done
 fi
diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt
index 41715f34f..99d098e53 100644
--- a/tests/unit/CMakeLists.txt
+++ b/tests/unit/CMakeLists.txt
@@ -16,5 +16,4 @@
 add_subdirectory(batching)
 add_subdirectory(buffers)
 add_subdirectory(clients)
-add_subdirectory(client_operators)
 add_subdirectory(core)
diff --git a/tests/unit/client_operators/CMakeLists.txt b/tests/unit/client_operators/CMakeLists.txt
deleted file mode 100644
index 97a9b674d..000000000
--- a/tests/unit/client_operators/CMakeLists.txt
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright 2022 Xilinx, Inc.
-# Copyright 2022 Advanced Micro Devices, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-list(APPEND tests infer_async)
-
-proteus_add_system_tests("${tests}")
diff --git a/tests/validation/resnet50.py b/tests/validation/resnet50.py
index ba35eb7cc..628df10ad 100644
--- a/tests/validation/resnet50.py
+++ b/tests/validation/resnet50.py
@@ -39,9 +39,7 @@
 import numpy as np
 
 import proteus
-import proteus.clients
-import proteus.testing
-import proteus.util.pre_post as pre_post
+import proteus.pre_post as pre_post
 
 
 def preprocess(paths):
@@ -69,11 +67,11 @@ def main(args):
     labels_file = args.labels
     worker = args.worker
 
-    client = proteus.clients.HttpClient("http://127.0.0.1:8998")
+    client = proteus.HttpClient("http://127.0.0.1:8998")
     print("waiting for server...", end="")
     start_server = not client.serverLive()
     if start_server:
-        server = proteus.servers.Server()
+        server = proteus.Server()
         server.startHttp(8998)
         while not client.serverLive():
             time.sleep(1)
@@ -149,9 +147,7 @@ def main(args):
             imageReqs = [proteus.ImageInferenceRequest(image) for image in images]
             print("Request is ready.  Sending ", len(images), " requests")
             j = 0
-            responses = proteus.client_operators.inferAsyncOrdered(
-                client, endpoint, imageReqs
-            )
+            responses = proteus.inferAsyncOrdered(client, endpoint, imageReqs)
             for response in responses:
                 assert not response.isError(), response.getError()
 
diff --git a/tests/workers/test_aks.py b/tests/workers/test_aks.py
index c41f28b48..23c3b891c 100644
--- a/tests/workers/test_aks.py
+++ b/tests/workers/test_aks.py
@@ -17,11 +17,6 @@
 import pytest
 
 import proteus
-from proteus.predict_api import (
-    InferenceRequest,
-    InferenceRequestInput,
-    InferenceResponse,
-)
 
 
 @pytest.mark.usefixtures("load")
@@ -32,18 +27,18 @@ class TestAks:
 
     def test_aks_0(self):
         numbers = [3.0]
-        request = InferenceRequest()
+        request = proteus.InferenceRequest()
         request.id = "hello_world"
 
         for num in numbers:
-            input_0 = InferenceRequestInput()
+            input_0 = proteus.InferenceRequestInput()
             input_0.name = "aks"
             input_0.setFp32Data(np.asarray([num], np.float))
             input_0.shape = [1]
             input_0.datatype = proteus.DataType.FP32
             request.addInputTensor(input_0)
         try:
-            response: InferenceResponse = self.rest_client.modelInfer(
+            response: proteus.InferenceResponse = self.rest_client.modelInfer(
                 self.endpoint, request
             )
         except ConnectionError:
diff --git a/tests/workers/test_echo.py b/tests/workers/test_echo.py
index 763cf59cc..89f5c51bf 100644
--- a/tests/workers/test_echo.py
+++ b/tests/workers/test_echo.py
@@ -17,11 +17,6 @@
 import pytest
 
 import proteus
-from proteus.predict_api import (
-    InferenceRequest,
-    InferenceRequestInput,
-    InferenceRequestOutput,
-)
 
 from helper import run_benchmark
 
@@ -54,7 +49,7 @@ def construct_request(
             dict: The constructed request
         """
 
-        input_0 = InferenceRequestInput()
+        input_0 = proteus.InferenceRequestInput()
         input_0.name = "echo"
         input_0.datatype = proteus.DataType.UINT32
         input_0.shape = [1]
@@ -64,7 +59,7 @@ def construct_request(
             parameters.put("key", "value")
             input_0.parameters = parameters
 
-        request = InferenceRequest()
+        request = proteus.InferenceRequest()
         for _ in range(multiplier):
             request.addInputTensor(input_0)
 
@@ -80,7 +75,7 @@ def construct_request(
         return request
 
     @staticmethod
-    def add_outputs(request: InferenceRequest, add_parameters):
+    def add_outputs(request: proteus.InferenceRequest, add_parameters):
         """
         Add output tensors to the request. This is optional.
 
@@ -91,7 +86,7 @@ def add_outputs(request: InferenceRequest, add_parameters):
         Returns:
             dict: Updated request
         """
-        output_0 = InferenceRequestOutput()
+        output_0 = proteus.InferenceRequestOutput()
         output_0.name = "echo"
         if add_parameters:
             parameters = proteus.RequestParameters()
diff --git a/tests/workers/test_echo_multi.py b/tests/workers/test_echo_multi.py
index 6474ab1e8..c4d06a117 100644
--- a/tests/workers/test_echo_multi.py
+++ b/tests/workers/test_echo_multi.py
@@ -16,8 +16,6 @@
 import pytest
 
 import proteus
-import proteus.client_operators
-from proteus.predict_api import InferenceRequest, InferenceRequestInput
 
 
 @pytest.mark.usefixtures("load")
@@ -41,9 +39,9 @@ def construct_request(cls):
             dict: The constructed request
         """
 
-        request = InferenceRequest()
+        request = proteus.InferenceRequest()
         for i in cls.inputs:
-            input_0 = InferenceRequestInput()
+            input_0 = proteus.InferenceRequestInput()
             input_0.name = "echoMulti0"
             input_0.datatype = proteus.DataType.UINT32
             input_0.shape = [len(i)]
@@ -66,7 +64,7 @@ def send_request(self, request):
 
         requests = [request] * 2
         try:
-            responses = proteus.client_operators.inferAsyncOrdered(
+            responses = proteus.inferAsyncOrdered(
                 self.rest_client, self.endpoint, requests
             )
         except ConnectionError:
diff --git a/tests/workers/test_facedetect_stream.py b/tests/workers/test_facedetect_stream.py
index f975df5e0..cbfbdd00c 100644
--- a/tests/workers/test_facedetect_stream.py
+++ b/tests/workers/test_facedetect_stream.py
@@ -19,9 +19,6 @@
 
 import proteus
 import proteus.testing
-from proteus.predict_api import InferenceRequest, InferenceRequestInput
-
-from helper import root_path
 
 
 @pytest.mark.extensions(["aks", "vitis"])
@@ -42,7 +39,7 @@ def construct_request(self, requested_frames_count):
         asset_key = "asset_Physicsworks.ogv"
         video_path = proteus.testing.getPathToAsset(asset_key)
 
-        input_0 = InferenceRequestInput()
+        input_0 = proteus.InferenceRequestInput()
         input_0.name = "input0"
         input_0.datatype = proteus.DataType.STRING
         input_0.setStringData(video_path)
@@ -51,7 +48,7 @@ def construct_request(self, requested_frames_count):
         parameters.put("count", requested_frames_count)
         input_0.parameters = parameters
 
-        request = InferenceRequest()
+        request = proteus.InferenceRequest()
         request.addInputTensor(input_0)
         parameters_2 = proteus.RequestParameters()
         parameters_2.put("key", "0")
diff --git a/tests/workers/test_invert_video.py b/tests/workers/test_invert_video.py
index 0b780628b..be629a924 100644
--- a/tests/workers/test_invert_video.py
+++ b/tests/workers/test_invert_video.py
@@ -21,7 +21,6 @@
 
 import proteus
 import proteus.testing
-from proteus.predict_api import InferenceRequest, InferenceRequestInput
 
 from helper import root_path
 
@@ -36,7 +35,7 @@ class TestInvertVideo:
     parameters = None
 
     def construct_request(self, video_path, requested_frames_count):
-        input_0 = InferenceRequestInput()
+        input_0 = proteus.InferenceRequestInput()
         input_0.name = "input0"
         input_0.datatype = proteus.DataType.STRING
         input_0.setStringData(video_path)
@@ -45,8 +44,7 @@ def construct_request(self, video_path, requested_frames_count):
         parameters.put("count", requested_frames_count)
         input_0.parameters = parameters
 
-        # request = WebsocketInferenceRequest(self.endpoint, input_0)
-        request = InferenceRequest()
+        request = proteus.InferenceRequest()
         request.addInputTensor(input_0)
         parameters_2 = proteus.RequestParameters()
         parameters_2.put("key", "0")
diff --git a/tests/workers/test_migraphx.py b/tests/workers/test_migraphx.py
index 33ca711b9..e19c0b57b 100644
--- a/tests/workers/test_migraphx.py
+++ b/tests/workers/test_migraphx.py
@@ -20,8 +20,8 @@
 import pytest
 
 import proteus
+import proteus.pre_post as pre_post
 import proteus.testing
-import proteus.util.pre_post as pre_post
 
 from helper import root_path, run_benchmark
 
diff --git a/tests/workers/test_ptzendnn.py b/tests/workers/test_ptzendnn.py
index a72078024..3bc9361da 100644
--- a/tests/workers/test_ptzendnn.py
+++ b/tests/workers/test_ptzendnn.py
@@ -20,8 +20,8 @@
 import pytest
 
 import proteus
+import proteus.pre_post as pre_post
 import proteus.testing
-import proteus.util.pre_post as pre_post
 
 from helper import root_path, run_benchmark
 
diff --git a/tests/workers/test_resnet50_stream.py b/tests/workers/test_resnet50_stream.py
index 3452401eb..88608c63a 100644
--- a/tests/workers/test_resnet50_stream.py
+++ b/tests/workers/test_resnet50_stream.py
@@ -19,7 +19,6 @@
 
 import proteus
 import proteus.testing
-from proteus.predict_api import InferenceRequest, InferenceRequestInput
 
 from helper import root_path
 
@@ -38,7 +37,7 @@ class TestResnet50Stream:
     def construct_request(self, requested_frames_count):
         video_path = proteus.testing.getPathToAsset("asset_Physicsworks.ogv")
 
-        input_0 = InferenceRequestInput()
+        input_0 = proteus.InferenceRequestInput()
         input_0.name = "input0"
         input_0.datatype = proteus.DataType.STRING
         input_0.setStringData(video_path)
@@ -47,7 +46,7 @@ def construct_request(self, requested_frames_count):
         parameters.put("count", requested_frames_count)
         input_0.parameters = parameters
 
-        request = InferenceRequest()
+        request = proteus.InferenceRequest()
         request.addInputTensor(input_0)
         parameters_2 = proteus.RequestParameters()
         parameters_2.put("key", "0")
diff --git a/tests/workers/test_tfzendnn.py b/tests/workers/test_tfzendnn.py
index f52446bb8..ebc330aeb 100644
--- a/tests/workers/test_tfzendnn.py
+++ b/tests/workers/test_tfzendnn.py
@@ -20,8 +20,8 @@
 import pytest
 
 import proteus
+import proteus.pre_post as pre_post
 import proteus.testing
-import proteus.util.pre_post as pre_post
 
 from helper import root_path, run_benchmark
 
diff --git a/tests/workers/xmodel/test_resnet50.cpp b/tests/workers/xmodel/test_resnet50.cpp
index ce1e9473e..21d866b9a 100644
--- a/tests/workers/xmodel/test_resnet50.cpp
+++ b/tests/workers/xmodel/test_resnet50.cpp
@@ -15,29 +15,28 @@
 #include <string>
 #include <vector>
 
-#include "proteus/client_operators/infer_async.hpp"
 #include "proteus/clients/client.hpp"
+#include "proteus/pre_post/image_preprocess.hpp"
+#include "proteus/pre_post/resnet50_postprocess.hpp"
 #include "proteus/testing/get_path_to_asset.hpp"
 #include "proteus/testing/gtest_fixtures.hpp"
-#include "proteus/util/pre_post/image_preprocess.hpp"
-#include "proteus/util/pre_post/resnet50_postprocess.hpp"
 
 namespace proteus {
 
 using Images = std::vector<std::vector<int8_t>>;
 
 Images preprocess(const std::vector<std::string>& paths) {
-  proteus::util::ImagePreprocessOptions<int8_t, 3> options;
-  options.order = proteus::util::ImageOrder::NHWC;
+  proteus::pre_post::ImagePreprocessOptions<int8_t, 3> options;
+  options.order = proteus::pre_post::ImageOrder::NHWC;
   options.mean = {123, 107, 104};
   options.std = {1, 1, 1};
   options.normalize = true;
-  return util::imagePreprocess(paths, options);
+  return pre_post::imagePreprocess(paths, options);
 }
 
 std::vector<int> postprocess(const proteus::InferenceResponseOutput& output,
                              int k) {
-  return proteus::util::resnet50Postprocess(
+  return proteus::pre_post::resnet50Postprocess(
     static_cast<int8_t*>(output.getData()), output.getSize(), k);
 }
 
diff --git a/tests/workers/xmodel/test_resnet50.py b/tests/workers/xmodel/test_resnet50.py
index bf25bb271..65057dfad 100644
--- a/tests/workers/xmodel/test_resnet50.py
+++ b/tests/workers/xmodel/test_resnet50.py
@@ -15,8 +15,8 @@
 import pytest
 
 import proteus
+import proteus.pre_post as pre_post
 import proteus.testing
-import proteus.util.pre_post as pre_post
 
 from helper import run_benchmark
 
diff --git a/tests/workers/xmodel/test_yolov3.cpp b/tests/workers/xmodel/test_yolov3.cpp
index f899f0eb9..d2d4b4a2c 100644
--- a/tests/workers/xmodel/test_yolov3.cpp
+++ b/tests/workers/xmodel/test_yolov3.cpp
@@ -15,23 +15,22 @@
 #include <string>
 #include <vector>
 
-#include "proteus/client_operators/infer_async.hpp"
 #include "proteus/clients/client.hpp"
+#include "proteus/pre_post/image_preprocess.hpp"
+#include "proteus/pre_post/resnet50_postprocess.hpp"
 #include "proteus/testing/get_path_to_asset.hpp"
 #include "proteus/testing/gtest_fixtures.hpp"
-#include "proteus/util/pre_post/image_preprocess.hpp"
-#include "proteus/util/pre_post/resnet50_postprocess.hpp"
 
 namespace proteus {
 
 using Images = std::vector<std::vector<float>>;
 
 Images preprocess(const std::vector<std::string>& paths) {
-  proteus::util::ImagePreprocessOptions<float, 3> options;
+  proteus::pre_post::ImagePreprocessOptions<float, 3> options;
   options.height = 416;
   options.width = 416;
 
-  options.resize_algorithm = util::ResizeAlgorithm::CenterCrop;
+  options.resize_algorithm = pre_post::ResizeAlgorithm::CenterCrop;
 
   options.convert_color = true;
   options.color_code = cv::COLOR_BGR2RGB;
@@ -41,10 +40,10 @@ Images preprocess(const std::vector<std::string>& paths) {
   options.convert_scale = 1 / 255.0;
 
   options.normalize = true;
-  options.order = proteus::util::ImageOrder::NHWC;
+  options.order = proteus::pre_post::ImageOrder::NHWC;
   options.mean = {0, 0, 0};
   options.std = {1, 1, 1};
-  return util::imagePreprocess(paths, options);
+  return pre_post::imagePreprocess(paths, options);
 }
 
 void postprocess(const proteus::InferenceResponseOutput& output) {