diff --git a/src/openlifu/cloud/api/api.py b/src/openlifu/cloud/api/api.py index 0948ad8c..956251f6 100644 --- a/src/openlifu/cloud/api/api.py +++ b/src/openlifu/cloud/api/api.py @@ -16,8 +16,8 @@ class Api: - def __init__(self): - self._request = Request() + def __init__(self, api_url: str): + self._request = Request(api_url) self._request.debug_log = True self._databases = DatabasesApi(self._request) self._protocols = ProtocolsApi(self._request) diff --git a/src/openlifu/cloud/api/request.py b/src/openlifu/cloud/api/request.py index 8ed8520e..3c55811a 100644 --- a/src/openlifu/cloud/api/request.py +++ b/src/openlifu/cloud/api/request.py @@ -7,7 +7,6 @@ import urllib3 from requests.adapters import HTTPAdapter -from openlifu.cloud.const import API_URL from openlifu.cloud.utils import logger_cloud, to_json urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) @@ -34,7 +33,8 @@ def init_poolmanager(self, *args, **kwargs): class Request: TIMEOUT = (5, 300) - def __init__(self): + def __init__(self, api_url: str): + self._api_url = api_url self.headers = {} self.session = requests.Session() adapter = SlicerAdapter( @@ -48,7 +48,7 @@ def _log_request(self, method: str, url: str, start_time: float, status_code: in def get(self, url: str) -> str: start = time.perf_counter() - response = self.session.get(API_URL + url, headers=self.headers, timeout=self.TIMEOUT, verify=False) + response = self.session.get(self._api_url + url, headers=self.headers, timeout=self.TIMEOUT, verify=False) self._log_request("GET", url, start, response.status_code) logger_cloud.debug(f"GET: {url}, status_code: {response.status_code}\nresponse: {response.text}") @@ -57,7 +57,7 @@ def get(self, url: str) -> str: def get_bytes(self, url: str) -> bytes: start = time.perf_counter() - response = self.session.get(API_URL + url, headers=self.headers, timeout=self.TIMEOUT, verify=False) + response = self.session.get(self._api_url + url, headers=self.headers, timeout=self.TIMEOUT, verify=False) self._log_request("GET_BYTES", url, start, response.status_code) logger_cloud.debug(f"GET bytes: {url}, status_code: {response.status_code}") @@ -66,7 +66,7 @@ def get_bytes(self, url: str) -> bytes: def post(self, url: str, dto) -> str: start = time.perf_counter() - response = self.session.post(API_URL + url, data=to_json(dto), headers=self.headers, timeout=self.TIMEOUT, verify=False) + response = self.session.post(self._api_url + url, data=to_json(dto), headers=self.headers, timeout=self.TIMEOUT, verify=False) self._log_request("POST", url, start, response.status_code) logger_cloud.debug(f"POST: {url}, body: {to_json(dto)}, status_code: {response.status_code}\nresponse: {response.text}") @@ -75,7 +75,7 @@ def post(self, url: str, dto) -> str: def post_bytes(self, url: str, data) -> str: start = time.perf_counter() - response = self.session.post(API_URL + url, data=data, headers=self.headers, timeout=self.TIMEOUT, verify=False) + response = self.session.post(self._api_url + url, data=data, headers=self.headers, timeout=self.TIMEOUT, verify=False) self._log_request("POST_BYTES", url, start, response.status_code) logger_cloud.debug(f"POST bytes: {url}, status_code: {response.status_code}\nresponse: {response.text}") @@ -84,7 +84,7 @@ def post_bytes(self, url: str, data) -> str: def put(self, url: str, dto) -> str: start = time.perf_counter() - response = self.session.put(API_URL + url, data=to_json(dto), headers=self.headers, timeout=self.TIMEOUT, verify=False) + response = self.session.put(self._api_url + url, data=to_json(dto), headers=self.headers, timeout=self.TIMEOUT, verify=False) self._log_request("PUT", url, start, response.status_code) logger_cloud.debug(f"PUT: {url}, body: {to_json(dto)}, status_code: {response.status_code}\nresponse: {response.text}") @@ -93,7 +93,7 @@ def put(self, url: str, dto) -> str: def delete(self, url: str) -> str: start = time.perf_counter() - response = self.session.delete(API_URL + url, headers=self.headers, timeout=self.TIMEOUT, verify=False) + response = self.session.delete(self._api_url + url, headers=self.headers, timeout=self.TIMEOUT, verify=False) self._log_request("DELETE", url, start, response.status_code) logger_cloud.debug(f"DELETE: {url}, status_code: {response.status_code}\nresponse: {response.text}") diff --git a/src/openlifu/cloud/cloud.py b/src/openlifu/cloud/cloud.py index 5ccddcae..b2aab90c 100644 --- a/src/openlifu/cloud/cloud.py +++ b/src/openlifu/cloud/cloud.py @@ -21,6 +21,7 @@ from openlifu.cloud.components.transducers import Transducers from openlifu.cloud.components.users import Users from openlifu.cloud.components.volumes import Volumes +from openlifu.cloud.const import API_URL_DEV, API_URL_PROD, ENV_DEV, ENV_PROD from openlifu.cloud.filesystem_observer import FilesystemObserver from openlifu.cloud.status import Status from openlifu.cloud.sync_thread import SyncThread @@ -30,10 +31,16 @@ class Cloud: - def __init__(self): + def __init__(self, environment: str = ENV_PROD): + if environment == ENV_DEV: + api_url = API_URL_DEV + elif environment == ENV_PROD: + api_url = API_URL_PROD + else: + raise ValueError(f"Unsupported cloud environment {environment!r}. Expected {ENV_DEV!r} or {ENV_PROD!r}.") self._filesystem_observer = FilesystemObserver(self._on_file_system_update) - self._api = Api() - self._websocket = Websocket(self._on_websocket_update) + self._api = Api(api_url) + self._websocket = Websocket(api_url, self._on_websocket_update) self._components: List[AbstractComponent] = [] self._sync_thread = SyncThread(self._on_status_changed) self._db_path: Path | None = None @@ -172,7 +179,7 @@ def _create_components(self): logger_cloud.setLevel(logging.DEBUG) logger_cloud.addHandler(logging.StreamHandler(sys.stdout)) - cloud = Cloud() + cloud = Cloud(ENV_DEV) token = os.getenv("TOKEN") db_path = os.getenv("DB_PATH") diff --git a/src/openlifu/cloud/const.py b/src/openlifu/cloud/const.py index 731e7af6..8089fcbb 100644 --- a/src/openlifu/cloud/const.py +++ b/src/openlifu/cloud/const.py @@ -1,6 +1,10 @@ from __future__ import annotations -API_URL = "https://api.openwater.health" +API_URL_PROD = "https://api.openwater.health" +API_URL_DEV = "https://dev.api.openwater.health" + +ENV_PROD = "prod" +ENV_DEV = "dev" CONFIG_FILE = "config" DATA_FILE = "data" diff --git a/src/openlifu/cloud/ws.py b/src/openlifu/cloud/ws.py index cd41f05b..d3195c82 100644 --- a/src/openlifu/cloud/ws.py +++ b/src/openlifu/cloud/ws.py @@ -5,14 +5,14 @@ import socketio from socketio import exceptions -from openlifu.cloud.const import API_URL from openlifu.cloud.utils import logger_cloud DATABASE_UPDATES_NS = "/database_updates" class Websocket: - def __init__(self, update_callback: Callable[[dict], None]): + def __init__(self, api_url: str, update_callback: Callable[[dict], None]): + self._api_url = api_url self._sio: socketio.Client | None = None self._database_id = None self._auth = {} @@ -28,7 +28,7 @@ def authenticate(self, access_token: str): self.connect(self._database_id) def connect(self, database_id: int): - self.log(f"Attempting connection to {API_URL} for DB {database_id}") + self.log(f"Attempting connection to {self._api_url} for DB {database_id}") if self._sio is not None: self.disconnect() @@ -71,7 +71,7 @@ def on_update(data): try: self._sio.connect( - f"{API_URL}/socket.io", + f"{self._api_url}/socket.io", auth=self._auth, namespaces=[DATABASE_UPDATES_NS], transports=["websocket"], diff --git a/tests/test_cloud.py b/tests/test_cloud.py new file mode 100644 index 00000000..c2393207 --- /dev/null +++ b/tests/test_cloud.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +import pytest + +from openlifu.cloud.cloud import Cloud +from openlifu.cloud.const import API_URL_DEV, API_URL_PROD + + +@pytest.mark.parametrize(("kwargs", "api_url"), [ + ({}, API_URL_PROD), + ({"environment": "prod"}, API_URL_PROD), + ({"environment": "dev"}, API_URL_DEV), +]) +def test_environment_routes_http_and_websocket(mocker, tmp_path, kwargs, api_url): + send = mocker.patch("requests.Session.send") + send.return_value.status_code = 200 + send.return_value.text = '{"id": 42}' + websocket = mocker.patch("openlifu.cloud.ws.socketio.Client") + mocker.patch("openlifu.cloud.cloud.get_mac_address", return_value="00:11:22:33:44:55") + mocker.patch("openlifu.cloud.cloud.SyncThread.start") + + cloud = Cloud(**kwargs) + try: + cloud.set_access_token("test-token") + cloud.start(tmp_path) + + request = send.call_args.args[0] + assert request.method == "PUT" + assert request.url == api_url + "/databases/claim" + assert request.headers["Authorization"] == "Bearer test-token" + connection = websocket.return_value.connect.call_args + assert connection.args == (api_url + "/socket.io",) + assert connection.kwargs["auth"] == {"token": "Bearer test-token"} + finally: + cloud.stop() + + +@pytest.mark.parametrize("environment", ["DEV", "deev", "dev ", "staging", "", None]) +def test_invalid_environment(environment): + with pytest.raises(ValueError, match="Unsupported cloud environment"): + Cloud(environment=environment)