diff --git a/clients/python/Readme.md b/clients/python/Readme.md
new file mode 100644
index 00000000..3e4d19a7
--- /dev/null
+++ b/clients/python/Readme.md
@@ -0,0 +1,33 @@
+Set directory path that contains superposition object files in SUPERPOSITION_LIB_PATH env variable;
+
+## [ CAC Client ](./cacclient)
+
+1. This exports a class that exposes functions that internally call rust functions.
+2. For Different platform it read different superposition object files.
+ * For Mac -> libcac_client.dylib
+ * For Windows -> libcac_client.so
+ * For Linux -> libcac_client.dll
+3. This run CAC CLient in two thread one is main thread another is worker thread.
+4. Polling updates for config are done on different thread. ([ref](./cacclient/client.py#L74)).
+
+
+## [ Experimentation Client ](./expclient)
+
+1. This exports a class that exposes functions that internally call rust functions.
+2. For Different platform it read different superposition object files.
+ * For Mac -> libexperimentation_client.dylib
+ * For Windows -> libexperimentation_client.so
+ * For Linux -> libexperimentation_client.dll
+3. This run Experimentation CLient in two thread one is main thread another is worker thread.
+4. Polling updates for experiments are done on different thread. ([ref](./expclient/client.py#L79)).
+
+
+## [ Test ](./main.py)
+
+1. To test this sample project follow below steps.
+ * Run superposition client.
+ * Run **python3 main.py** this will start a server that runs on port 8002.
+2. By Default this sample code uses [dev](./main.py#L7) tenant.
+3. By Default this sample code assumes superposition is running on [8080](./main.py#L9) port.
+3. By Default this sample code polls superposition every [1 second](./main.py#L8) port.
+4. This sample code creates both [CAC CLient](./main.py#L11) and [Experimentation Client](./main.py#L10) with above default values.
\ No newline at end of file
diff --git a/clients/python/cacclient/__init__.py b/clients/python/cacclient/__init__.py
new file mode 100644
index 00000000..4cb0e497
--- /dev/null
+++ b/clients/python/cacclient/__init__.py
@@ -0,0 +1 @@
+from .client import CacClient
\ No newline at end of file
diff --git a/clients/python/cacclient/client.py b/clients/python/cacclient/client.py
new file mode 100644
index 00000000..9b3e3b84
--- /dev/null
+++ b/clients/python/cacclient/client.py
@@ -0,0 +1,105 @@
+import ctypes
+import os
+import threading
+
+platform = os.uname().sysname.lower()
+lib_path = os.environ.get("SUPERPOSITION_LIB_PATH")
+if lib_path == None:
+ raise Exception("SUPERPOSITION_LIB_PATH not set on env")
+
+file_name = (
+ "libcac_client.dylib" if platform == "darwin"
+ else "libcac_client.dll" if platform == "linux"
+ else "libcac_client.so"
+)
+
+lib_path = os.path.join(lib_path, file_name)
+
+class CacClient:
+ rust_lib = ctypes.CDLL(lib_path)
+
+ rust_lib.cac_new_client.argtypes = [ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p]
+ rust_lib.cac_new_client.restype = ctypes.c_int
+
+ rust_lib.cac_get_client.argtypes = [ctypes.c_char_p]
+ rust_lib.cac_get_client.restype = ctypes.c_char_p
+
+ rust_lib.cac_start_polling_update.argtypes = [ctypes.c_char_p]
+ rust_lib.cac_start_polling_update.restype = None
+
+ rust_lib.cac_free_client.argtypes = [ctypes.c_char_p]
+ rust_lib.cac_free_client.restype = None
+
+ rust_lib.cac_last_error_message.restype = ctypes.c_char_p
+
+ rust_lib.cac_get_config.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p]
+ rust_lib.cac_get_config.restype = ctypes.c_char_p
+
+ rust_lib.cac_last_error_length.restype = ctypes.c_int
+
+ rust_lib.cac_free_string.argtypes = [ctypes.c_char_p]
+ rust_lib.cac_free_string.restype = None
+
+ rust_lib.cac_get_last_modified.argtypes = [ctypes.c_char_p]
+ rust_lib.cac_get_last_modified.restype = ctypes.c_char_p
+
+ rust_lib.cac_get_resolved_config.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p]
+ rust_lib.cac_get_resolved_config.restype = ctypes.c_char_p
+
+ rust_lib.cac_get_default_config.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
+ rust_lib.cac_get_default_config.restype = ctypes.c_char_p
+
+ def __init__(self, tenant_name: str, polling_frequency: int, cac_host_name: str):
+ if not tenant_name or not cac_host_name:
+ raise ValueError("tenantName cannot be null or empty")
+
+ self.tenant = tenant_name
+ self.polling_frequency = polling_frequency
+ self.cac_host_name = cac_host_name
+
+ def get_cac_last_error_message(self) -> str:
+ return self.rust_lib.cac_last_error_message().decode()
+
+ def get_cac_last_error_length(self) -> int:
+ return self.rust_lib.cac_last_error_length()
+
+ def get_cac_client(self) -> str:
+ return self.rust_lib.cac_get_client(self.tenant.encode())
+
+ def create_new_cac_client(self) -> int:
+ resp = self.rust_lib.cac_new_client(
+ self.tenant.encode(), self.polling_frequency, self.cac_host_name.encode())
+ if resp == 1:
+ error_message = self.get_cac_last_error_message()
+ print("Some Error Occur while creating new client ", error_message)
+ return resp
+
+ def start_cac_polling_update(self):
+ threading.Thread(target=self._polling_update_worker).start()
+
+ def _polling_update_worker(self):
+ self.rust_lib.cac_start_polling_update(self.tenant.encode())
+
+ def get_cac_config(self, filter_query: str | None = None, filter_prefix: str | None = None) -> str:
+ client_ptr = self.get_cac_client()
+ filter_prefix_ptr = None if filter_prefix is None else filter_prefix.encode()
+ filter_query_ptr = None if filter_query is None else filter_query.encode()
+ return self.rust_lib.cac_get_config(client_ptr, filter_query_ptr, filter_prefix_ptr).decode()
+
+ def free_cac_client(self, client_ptr: str):
+ self.rust_lib.cac_free_client(client_ptr.encode())
+
+ def free_cac_string(self, string: str):
+ self.rust_lib.cac_free_string(string.encode())
+
+ def get_last_modified(self) -> str:
+ return self.rust_lib.cac_get_last_modified(self.get_cac_client()).decode()
+
+ def get_resolved_config(self, query: str, merge_strategy: str, filter_keys: str | None = None) -> str:
+ filter_keys_ptr = None if filter_keys is None else filter_keys.encode()
+ return self.rust_lib.cac_get_resolved_config(
+ self.get_cac_client(), query.encode(), filter_keys_ptr, merge_strategy.encode()).decode()
+
+ def get_default_config(self, filter_keys: str | None = None) -> str:
+ filter_keys_ptr = None if filter_keys is None else filter_keys.encode()
+ return self.rust_lib.cac_get_default_config(self.get_cac_client(), filter_keys_ptr).decode()
diff --git a/clients/python/expclient/__init__.py b/clients/python/expclient/__init__.py
new file mode 100644
index 00000000..5e7a5b81
--- /dev/null
+++ b/clients/python/expclient/__init__.py
@@ -0,0 +1 @@
+from .client import ExperimentationClient
\ No newline at end of file
diff --git a/clients/python/expclient/client.py b/clients/python/expclient/client.py
new file mode 100644
index 00000000..643ab933
--- /dev/null
+++ b/clients/python/expclient/client.py
@@ -0,0 +1,109 @@
+import ctypes
+import os
+import threading
+
+platform = os.uname().sysname.lower()
+lib_path = os.environ.get("SUPERPOSITION_LIB_PATH")
+if lib_path == None:
+ raise Exception("SUPERPOSITION_LIB_PATH not set on env")
+
+file_name = (
+ "libexperimentation_client.dylib" if platform == "darwin"
+ else "libexperimentation_client.dll" if platform == "linux"
+ else "libexperimentation_client.so"
+)
+
+lib_path = os.path.join(lib_path, file_name)
+
+class ExperimentationClient:
+ rust_lib = ctypes.CDLL(lib_path)
+
+ rust_lib.expt_new_client.argtypes = [ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p]
+ rust_lib.expt_new_client.restype = ctypes.c_int
+
+ rust_lib.expt_start_polling_update.argtypes = [ctypes.c_char_p]
+ rust_lib.expt_start_polling_update.restype = ctypes.c_void_p
+
+ rust_lib.expt_get_client.argtypes = [ctypes.c_char_p]
+ rust_lib.expt_get_client.restype = ctypes.c_char_p
+
+ rust_lib.expt_get_applicable_variant.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_int]
+ rust_lib.expt_get_applicable_variant.restype = ctypes.c_char_p
+
+ rust_lib.expt_get_satisfied_experiments.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p]
+ rust_lib.expt_get_satisfied_experiments.restype = ctypes.c_char_p
+
+ rust_lib.expt_get_filtered_satisfied_experiments.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p]
+ rust_lib.expt_get_filtered_satisfied_experiments.restype = ctypes.c_char_p
+
+ rust_lib.expt_get_running_experiments.argtypes = [ctypes.c_char_p]
+ rust_lib.expt_get_running_experiments.restype = ctypes.c_char_p
+
+ rust_lib.expt_free_string.argtypes = [ctypes.c_char_p]
+ rust_lib.expt_free_string.restype = ctypes.c_void_p
+
+ rust_lib.expt_last_error_message.argtypes = []
+ rust_lib.expt_last_error_message.restype = ctypes.c_char_p
+
+ rust_lib.expt_last_error_length.argtypes = []
+ rust_lib.expt_last_error_length.restype = ctypes.c_int
+
+ rust_lib.expt_free_client.argtypes = [ctypes.c_char_p]
+ rust_lib.expt_free_client.restype = ctypes.c_void_p
+
+ def __init__(self, tenant_name: str, polling_frequency: int, cac_host_name: str):
+ if not tenant_name or not cac_host_name:
+ raise ValueError("tenantName cannot be null or empty")
+
+ self.tenant = tenant_name
+ self.polling_frequency = polling_frequency
+ self.cac_host_name = cac_host_name
+
+ def get_experimentation_last_error_message(self) -> str:
+ return self.rust_lib.expt_last_error_message().decode()
+
+ def create_new_experimentation_client(self) -> int:
+ resp_code = self.rust_lib.expt_new_client(self.tenant.encode(), self.polling_frequency, self.cac_host_name.encode())
+ if resp_code == 1:
+ error_message = self.get_experimentation_last_error_message()
+ print("Some error occurred while creating new experimentation client:", error_message)
+ raise Exception("Client Creation Error")
+ return resp_code
+
+ def get_experimentation_client(self) -> str:
+ return self.rust_lib.expt_get_client(self.tenant.encode())
+
+ def get_running_experiments(self) -> str:
+ return self.rust_lib.expt_get_running_experiments(self.get_experimentation_client()).decode()
+
+ def free_string(self, string: str):
+ self.rust_lib.expt_free_string(string.encode())
+
+ def start_experimentation_polling_update(self):
+ threading.Thread(target=self._polling_update_worker).start()
+
+ def _polling_update_worker(self):
+ self.rust_lib.expt_start_polling_update(self.tenant.encode())
+
+ def get_experimentation_last_error_length(self) -> int:
+ return self.rust_lib.expt_last_error_length()
+
+ def free_experimentation_client(self):
+ self.rust_lib.expt_free_client(self.get_experimentation_client())
+
+ def get_filtered_satisfied_experiments(self, context: str, filter_prefix: str | None = None) -> str:
+ filter_prefix_ptr = None if filter_prefix is None else filter_prefix.encode()
+ return self.rust_lib.expt_get_filtered_satisfied_experiments(
+ self.get_experimentation_client(), context.encode(), filter_prefix_ptr
+ ).decode()
+
+ def get_applicable_variant(self, context: str, toss: int) -> str:
+ return self.rust_lib.expt_get_applicable_variant(
+ self.get_experimentation_client(), context.encode(), toss
+ ).decode()
+
+ def get_satisfied_experiments(self, context: str, filter_prefix: str | None = None) -> str:
+ filter_prefix_ptr = None if filter_prefix is None else filter_prefix.encode()
+ return self.rust_lib.expt_get_satisfied_experiments(
+ self.get_experimentation_client(), context.encode(), filter_prefix_ptr
+ ).decode()
\ No newline at end of file
diff --git a/clients/python/main.py b/clients/python/main.py
new file mode 100644
index 00000000..52ed7638
--- /dev/null
+++ b/clients/python/main.py
@@ -0,0 +1,44 @@
+from expclient import ExperimentationClient
+from cacclient import CacClient
+import http.server
+import socketserver
+
+try:
+ tenant_name = "dev"
+ polling_frequency = 1
+ cac_host_name = "http://localhost:8080"
+ exp_client = ExperimentationClient(tenant_name, polling_frequency, cac_host_name)
+ cac_client = CacClient(tenant_name, polling_frequency, cac_host_name)
+
+ exp_client.create_new_experimentation_client()
+ cac_client.create_new_cac_client()
+
+ cac_client.start_cac_polling_update()
+ exp_client.start_experimentation_polling_update()
+
+ PORT = 8002
+
+ Handler = http.server.SimpleHTTPRequestHandler
+
+ class MyHandler(http.server.BaseHTTPRequestHandler):
+ def do_GET(self):
+ # Respond with a 200 OK status
+ if self.path == '/testconfig':
+ cacClientResp = cac_client.get_default_config()
+ self.send_response(200)
+ self.send_header("Content-type", "text/html")
+ self.end_headers()
+ self.wfile.write(cacClientResp.encode())
+ elif self.path == '/testexp':
+ expClientResp = exp_client.get_running_experiments()
+ self.send_response(200)
+ self.send_header("Content-type", "text/html")
+ self.end_headers()
+ self.wfile.write(expClientResp.encode())
+
+ with socketserver.TCPServer(("", PORT), MyHandler) as httpd:
+ print(f"Serving at port http://localhost:{PORT}")
+ httpd.serve_forever()
+
+except Exception as e:
+ print("Error:", e)
\ No newline at end of file