Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adding python cac and experimentation client wrapper #182

Merged
merged 1 commit into from
Aug 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Loading