diff --git a/README.md b/README.md index 4ae670b..0eb65a4 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,10 @@ Not all endpoints in the official documentation are implemented. ## Requirements -- [OBS Studio](https://obsproject.com/) -- [OBS Websocket v5 Plugin](https://github.com/obsproject/obs-websocket/releases/tag/5.0.0) - - With the release of OBS Studio version 28, Websocket plugin is included by default. But it should be manually installed for earlier versions of OBS. -- Python 3.9 or greater +- [OBS Studio](https://obsproject.com/) +- [OBS Websocket v5 Plugin](https://github.com/obsproject/obs-websocket/releases/tag/5.0.0) + - With the release of OBS Studio version 28, Websocket plugin is included by default. But it should be manually installed for earlier versions of OBS. +- Python 3.9 or greater ### How to install using pip @@ -24,10 +24,10 @@ pip install obsws-python By default the clients connect with parameters: -- `host`: "localhost" -- `port`: 4455 -- `password`: "" -- `timeout`: None +- `host`: "localhost" +- `port`: 4455 +- `password`: "" +- `timeout`: None You may override these parameters by storing them in a toml config file or passing them as keyword arguments. @@ -62,7 +62,7 @@ cl.toggle_input_mute('Mic/Aux') ### Requests -Method names for requests match the API calls but snake cased. +Method names for requests match the API calls but snake cased. If a successful call is made with the Request client and the response is expected to contain fields then a response object will be returned. You may then access the response fields as class attributes. They will be snake cased. example: @@ -70,13 +70,28 @@ example: # load conn info from config.toml cl = obs.ReqClient() -# GetVersion +# GetVersion, returns a response object resp = cl.get_version() +# Access it's field as an attribute +print(f"OBS Version: {resp.obs_version}") + # SetCurrentProgramScene cl.set_current_program_scene("BRB") ``` +#### `send(param, data=None, raw=False)` + +If you prefer to work with the JSON data directly the {ReqClient}.send() method accepts an argument, `raw`. If set to True the raw response data will be returned, instead of a response object. + +example: + +```python +resp = cl_req.send("GetVersion", raw=True) + +print(f"response data: {resp}") +``` + For a full list of requests refer to [Requests](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#requests) ### Events @@ -128,11 +143,13 @@ def on_scene_created(data): ### Errors -If a request fails an `OBSSDKError` will be raised with a status code. - -For a full list of status codes refer to [Codes](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#requeststatus) - -If a timeout occurs during sending/receiving a request or receiving an event an `OBSSDKTimeoutError` will be raised. +- `OBSSDKError`: Base error class. +- `OBSSDKTimeoutError`: Raised if a timeout occurs during sending/receiving a request or receiving an event +- `OBSSDKRequestError`: Raised when a request returns an error code. + - The following attributes are available: + - `req_name`: name of the request. + - `code`: request status code. + - For a full list of status codes refer to [Codes](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#requeststatus) ### Logging @@ -165,4 +182,4 @@ pytest -v For the full documentation: -- [OBS Websocket SDK](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#obs-websocket-501-protocol) +- [OBS Websocket SDK](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#obs-websocket-501-protocol) diff --git a/obsws_python/baseclient.py b/obsws_python/baseclient.py index ca9ac1e..27bcc09 100644 --- a/obsws_python/baseclient.py +++ b/obsws_python/baseclient.py @@ -43,7 +43,7 @@ class ObsClient: except ValueError as e: self.logger.error(f"{type(e).__name__}: {e}") raise - except (ConnectionRefusedError, WebSocketTimeoutException) as e: + except (ConnectionRefusedError, TimeoutError, WebSocketTimeoutException) as e: self.logger.exception(f"{type(e).__name__}: {e}") raise @@ -97,9 +97,9 @@ class ObsClient: auth = base64.b64encode( hashlib.sha256( ( - secret.decode() - + self.server_hello["d"]["authentication"]["challenge"] - ).encode() + secret + + self.server_hello["d"]["authentication"]["challenge"].encode() + ) ).digest() ).decode() @@ -110,7 +110,7 @@ class ObsClient: response = json.loads(self.ws.recv()) if response["op"] != 2: raise OBSSDKError( - "failed to identify client with the server, expected response with OpCode 2 Identified" + "failed to identify client with the server, expected response with OpCode 2" ) return response["d"] except json.decoder.JSONDecodeError: diff --git a/obsws_python/error.py b/obsws_python/error.py index 42719b1..f1a2f6c 100644 --- a/obsws_python/error.py +++ b/obsws_python/error.py @@ -1,6 +1,18 @@ class OBSSDKError(Exception): - """Exception raised when general errors occur""" + """Base class for OBSSDK errors""" -class OBSSDKTimeoutError(Exception): +class OBSSDKTimeoutError(OBSSDKError): """Exception raised when a connection times out""" + + +class OBSSDKRequestError(OBSSDKError): + """Exception raised when a request returns an error code""" + + def __init__(self, req_name, code, comment): + self.req_name = req_name + self.code = code + message = f"Request {self.req_name} returned code {self.code}." + if comment: + message += f" With message: {comment}" + super().__init__(message) diff --git a/obsws_python/reqs.py b/obsws_python/reqs.py index 5037913..4321d29 100644 --- a/obsws_python/reqs.py +++ b/obsws_python/reqs.py @@ -1,7 +1,7 @@ import logging from .baseclient import ObsClient -from .error import OBSSDKError +from .error import OBSSDKError, OBSSDKRequestError from .util import as_dataclass """ @@ -43,14 +43,17 @@ class ReqClient: return type(self).__name__ def send(self, param, data=None, raw=False): - response = self.base_client.req(param, data) - if not response["requestStatus"]["result"]: - error = ( - f"Request {response['requestType']} returned code {response['requestStatus']['code']}", - ) - if "comment" in response["requestStatus"]: - error += (f"With message: {response['requestStatus']['comment']}",) - raise OBSSDKError("\n".join(error)) + try: + response = self.base_client.req(param, data) + if not response["requestStatus"]["result"]: + raise OBSSDKRequestError( + response["requestType"], + response["requestStatus"]["code"], + response["requestStatus"].get("comment"), + ) + except OBSSDKRequestError as e: + self.logger.exception(f"{type(e).__name__}: {e}") + raise if "responseData" in response: if raw: return response["responseData"] diff --git a/obsws_python/version.py b/obsws_python/version.py index 10cd127..38ec8ed 100644 --- a/obsws_python/version.py +++ b/obsws_python/version.py @@ -1 +1 @@ -version = "1.5.2" +version = "1.6.0" diff --git a/tests/test_error.py b/tests/test_error.py new file mode 100644 index 0000000..e083187 --- /dev/null +++ b/tests/test_error.py @@ -0,0 +1,35 @@ +import pytest + +import obsws_python as obsws +from tests import req_cl + + +class TestErrors: + __test__ = True + + def test_it_raises_an_obssdk_error_on_incorrect_password(self): + bad_conn = {"host": "localhost", "port": 4455, "password": "incorrectpassword"} + with pytest.raises( + obsws.error.OBSSDKError, + match="failed to identify client with the server, please check connection settings", + ): + obsws.ReqClient(**bad_conn) + + def test_it_raises_an_obssdk_error_if_auth_enabled_but_no_password_provided(self): + bad_conn = {"host": "localhost", "port": 4455, "password": ""} + with pytest.raises( + obsws.error.OBSSDKError, + match="authentication enabled but no password provided", + ): + obsws.ReqClient(**bad_conn) + + def test_it_raises_a_request_error_on_invalid_request(self): + with pytest.raises( + obsws.error.OBSSDKRequestError, + match="Request SetCurrentProgramScene returned code 600. With message: No source was found by the name of `invalid`.", + ) as exc_info: + req_cl.set_current_program_scene("invalid") + + e = exc_info.value + assert e.req_name == "SetCurrentProgramScene" + assert e.code == 600