Skip to content

Commit

Permalink
feat: adding python cac and experimentation client wrapper
Browse files Browse the repository at this point in the history
  • Loading branch information
namitgoel committed Aug 8, 2024
1 parent f78ddbc commit f394d79
Show file tree
Hide file tree
Showing 6 changed files with 293 additions and 0 deletions.
33 changes: 33 additions & 0 deletions clients/python/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
Set directory path that contains superposition object files in <span style="color: red" > SUPERPOSITION_LIB_PATH </span> env variable;

## [<u> CAC Client </u>](./cacclient)

1. This exports a class that exposes functions that internally call rust functions.
2. For Different platform it read different superposition object files.
* <span style="color: #808080" >For Mac </span> -> libcac_client.dylib
* <span style="color: #357EC7" >For Windows </span> -> libcac_client.so
* <span style="color: orange" >For Linux </span> -> 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)).


## [<u> Experimentation Client </u>](./expclient)

1. This exports a class that exposes functions that internally call rust functions.
2. For Different platform it read different superposition object files.
* <span style="color: #808080" >For Mac </span> -> libexperimentation_client.dylib
* <span style="color: #357EC7" >For Windows </span> -> libexperimentation_client.so
* <span style="color: orange" >For Linux </span> -> 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)).


## [<u> Test </u>](./main.py)

1. To test this sample project follow below steps.
* Run superposition client.
* Run <u> **python3 main.py** </u> 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.
1 change: 1 addition & 0 deletions clients/python/cacclient/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .client import CacClient
105 changes: 105 additions & 0 deletions clients/python/cacclient/client.py
Original file line number Diff line number Diff line change
@@ -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()
1 change: 1 addition & 0 deletions clients/python/expclient/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .client import ExperimentationClient
109 changes: 109 additions & 0 deletions clients/python/expclient/client.py
Original file line number Diff line number Diff line change
@@ -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()
44 changes: 44 additions & 0 deletions clients/python/main.py
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit f394d79

Please sign in to comment.