Merge pull request #32 from onyx-and-iris/add-request-error-class

Error handling with base error class
This commit is contained in:
Adem 2023-08-14 14:38:43 +03:00 committed by GitHub
commit 8aa2e78ba6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 100 additions and 33 deletions

View File

@ -9,10 +9,10 @@ Not all endpoints in the official documentation are implemented.
## Requirements ## Requirements
- [OBS Studio](https://obsproject.com/) - [OBS Studio](https://obsproject.com/)
- [OBS Websocket v5 Plugin](https://github.com/obsproject/obs-websocket/releases/tag/5.0.0) - [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. - 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 - Python 3.9 or greater
### How to install using pip ### How to install using pip
@ -24,10 +24,10 @@ pip install obsws-python
By default the clients connect with parameters: By default the clients connect with parameters:
- `host`: "localhost" - `host`: "localhost"
- `port`: 4455 - `port`: 4455
- `password`: "" - `password`: ""
- `timeout`: None - `timeout`: None
You may override these parameters by storing them in a toml config file or passing them as keyword arguments. 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 ### 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: example:
@ -70,13 +70,28 @@ example:
# load conn info from config.toml # load conn info from config.toml
cl = obs.ReqClient() cl = obs.ReqClient()
# GetVersion # GetVersion, returns a response object
resp = cl.get_version() resp = cl.get_version()
# Access it's field as an attribute
print(f"OBS Version: {resp.obs_version}")
# SetCurrentProgramScene # SetCurrentProgramScene
cl.set_current_program_scene("BRB") 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) For a full list of requests refer to [Requests](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#requests)
### Events ### Events
@ -128,11 +143,13 @@ def on_scene_created(data):
### Errors ### Errors
If a request fails an `OBSSDKError` will be raised with a status code. - `OBSSDKError`: Base error class.
- `OBSSDKTimeoutError`: Raised if a timeout occurs during sending/receiving a request or receiving an event
For a full list of status codes refer to [Codes](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#requeststatus) - `OBSSDKRequestError`: Raised when a request returns an error code.
- The following attributes are available:
If a timeout occurs during sending/receiving a request or receiving an event an `OBSSDKTimeoutError` will be raised. - `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 ### Logging
@ -165,4 +182,4 @@ pytest -v
For the full documentation: 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)

View File

@ -43,7 +43,7 @@ class ObsClient:
except ValueError as e: except ValueError as e:
self.logger.error(f"{type(e).__name__}: {e}") self.logger.error(f"{type(e).__name__}: {e}")
raise raise
except (ConnectionRefusedError, WebSocketTimeoutException) as e: except (ConnectionRefusedError, TimeoutError, WebSocketTimeoutException) as e:
self.logger.exception(f"{type(e).__name__}: {e}") self.logger.exception(f"{type(e).__name__}: {e}")
raise raise
@ -97,9 +97,9 @@ class ObsClient:
auth = base64.b64encode( auth = base64.b64encode(
hashlib.sha256( hashlib.sha256(
( (
secret.decode() secret
+ self.server_hello["d"]["authentication"]["challenge"] + self.server_hello["d"]["authentication"]["challenge"].encode()
).encode() )
).digest() ).digest()
).decode() ).decode()
@ -110,7 +110,7 @@ class ObsClient:
response = json.loads(self.ws.recv()) response = json.loads(self.ws.recv())
if response["op"] != 2: if response["op"] != 2:
raise OBSSDKError( 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"] return response["d"]
except json.decoder.JSONDecodeError: except json.decoder.JSONDecodeError:

View File

@ -1,6 +1,18 @@
class OBSSDKError(Exception): 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""" """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)

View File

@ -1,7 +1,7 @@
import logging import logging
from .baseclient import ObsClient from .baseclient import ObsClient
from .error import OBSSDKError from .error import OBSSDKError, OBSSDKRequestError
from .util import as_dataclass from .util import as_dataclass
""" """
@ -43,14 +43,17 @@ class ReqClient:
return type(self).__name__ return type(self).__name__
def send(self, param, data=None, raw=False): def send(self, param, data=None, raw=False):
response = self.base_client.req(param, data) try:
if not response["requestStatus"]["result"]: response = self.base_client.req(param, data)
error = ( if not response["requestStatus"]["result"]:
f"Request {response['requestType']} returned code {response['requestStatus']['code']}", raise OBSSDKRequestError(
) response["requestType"],
if "comment" in response["requestStatus"]: response["requestStatus"]["code"],
error += (f"With message: {response['requestStatus']['comment']}",) response["requestStatus"].get("comment"),
raise OBSSDKError("\n".join(error)) )
except OBSSDKRequestError as e:
self.logger.exception(f"{type(e).__name__}: {e}")
raise
if "responseData" in response: if "responseData" in response:
if raw: if raw:
return response["responseData"] return response["responseData"]

View File

@ -1 +1 @@
version = "1.5.2" version = "1.6.0"

35
tests/test_error.py Normal file
View File

@ -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