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