-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: adding python cac and experimentation client wrapper
- Loading branch information
Showing
6 changed files
with
293 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from .client import CacClient |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from .client import ExperimentationClient |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |