From 5db7a705c5b25d04b016f0302555b89d59db2eca Mon Sep 17 00:00:00 2001 From: onyx-and-iris Date: Fri, 11 Aug 2023 22:31:03 +0100 Subject: [PATCH 1/9] log and rethrow TimeoutError on connection we can just encode challenge here. shorten opcode != 2 message --- obsws_python/baseclient.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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: From f3e75c0ddfe9ea600daedee5c2b7b71e038814d6 Mon Sep 17 00:00:00 2001 From: onyx-and-iris Date: Fri, 11 Aug 2023 22:32:50 +0100 Subject: [PATCH 2/9] OBSSDKError is now the base custom error class OBSSDKTimeoutError and OBSSDKRequestError subclass it req_name and error code set as error class attributes. --- obsws_python/error.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/obsws_python/error.py b/obsws_python/error.py index 42719b1..ae310f7 100644 --- a/obsws_python/error.py +++ b/obsws_python/error.py @@ -1,6 +1,21 @@ 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, message=None): + self.req_name = req_name + self.code = code + self.message = " ".join( + [ + f"Request {self.req_name} returned code {self.code}.", + f"With message: {message}" if message else "", + ] + ) + super().__init__(self.message) From ffd215aadf998566ebadc90dc3106d13f722b134 Mon Sep 17 00:00:00 2001 From: onyx-and-iris Date: Fri, 11 Aug 2023 22:33:41 +0100 Subject: [PATCH 3/9] send now raises an OBSSDKRequestError it is then logged and rethrown --- obsws_python/reqs.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) 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"] From 6fa24fe609b937894f803275c414d181bdadef7d Mon Sep 17 00:00:00 2001 From: onyx-and-iris Date: Fri, 11 Aug 2023 22:33:56 +0100 Subject: [PATCH 4/9] error tests added --- tests/test_error.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 tests/test_error.py diff --git a/tests/test_error.py b/tests/test_error.py new file mode 100644 index 0000000..721c965 --- /dev/null +++ b/tests/test_error.py @@ -0,0 +1,31 @@ +import pytest + +import obsws_python as obsws +from tests import req_cl + + +class TestErrors: + __test__ = True + + def test_it_raises_an_obssdk_error_on_bad_connection_info(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`.", + ): + req_cl.set_current_program_scene("invalid") From f88e8ee3a6c5a570fa0f166bdc9d5b29d2dcbb2c Mon Sep 17 00:00:00 2001 From: onyx-and-iris Date: Fri, 11 Aug 2023 22:35:25 +0100 Subject: [PATCH 5/9] Errors section in readme updated --- README.md | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 4ae670b..4526967 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. @@ -128,7 +128,9 @@ def on_scene_created(data): ### Errors -If a request fails an `OBSSDKError` will be raised with a status code. +A base error class `OBSSDKError` may be used to catch OBSSDK error types. + +If a request returns an error code an `OBSSDKRequestError` will be raised. For a full list of status codes refer to [Codes](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#requeststatus) @@ -165,4 +167,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) From 013cf15024c7ab979866b900ed7ac976c199baa7 Mon Sep 17 00:00:00 2001 From: onyx-and-iris Date: Sat, 12 Aug 2023 14:51:44 +0100 Subject: [PATCH 6/9] check req_name and code for OBSSDKRequestError class --- tests/test_error.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/test_error.py b/tests/test_error.py index 721c965..e083187 100644 --- a/tests/test_error.py +++ b/tests/test_error.py @@ -7,7 +7,7 @@ from tests import req_cl class TestErrors: __test__ = True - def test_it_raises_an_obssdk_error_on_bad_connection_info(self): + 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, @@ -27,5 +27,9 @@ class TestErrors: 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 From a7ef61018b94f73224b2fbdfe46a00bbcdf09a25 Mon Sep 17 00:00:00 2001 From: onyx-and-iris Date: Mon, 14 Aug 2023 00:44:59 +0100 Subject: [PATCH 7/9] refactor OBSSDKRequestError reword error section in README --- README.md | 14 +++++++------- obsws_python/error.py | 13 +++++-------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 4526967..ab55d12 100644 --- a/README.md +++ b/README.md @@ -128,13 +128,13 @@ def on_scene_created(data): ### Errors -A base error class `OBSSDKError` may be used to catch OBSSDK error types. - -If a request returns an error code an `OBSSDKRequestError` will be raised. - -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 diff --git a/obsws_python/error.py b/obsws_python/error.py index ae310f7..f1a2f6c 100644 --- a/obsws_python/error.py +++ b/obsws_python/error.py @@ -9,13 +9,10 @@ class OBSSDKTimeoutError(OBSSDKError): class OBSSDKRequestError(OBSSDKError): """Exception raised when a request returns an error code""" - def __init__(self, req_name, code, message=None): + def __init__(self, req_name, code, comment): self.req_name = req_name self.code = code - self.message = " ".join( - [ - f"Request {self.req_name} returned code {self.code}.", - f"With message: {message}" if message else "", - ] - ) - super().__init__(self.message) + message = f"Request {self.req_name} returned code {self.code}." + if comment: + message += f" With message: {comment}" + super().__init__(message) From 70a422696ebb7a62e688e3adc50ccfaec7489466 Mon Sep 17 00:00:00 2001 From: onyx-and-iris Date: Mon, 14 Aug 2023 11:11:46 +0100 Subject: [PATCH 8/9] expand the Requests section in README add a section about the {ReqClient}.send() method. --- README.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ab55d12..0eb65a4 100644 --- a/README.md +++ b/README.md @@ -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 From 780f07e25f5d54a316fb44784a6f5096c8ac66d9 Mon Sep 17 00:00:00 2001 From: onyx-and-iris Date: Mon, 14 Aug 2023 12:18:29 +0100 Subject: [PATCH 9/9] minor version bump --- obsws_python/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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"