Compare commits

..

22 Commits

Author SHA1 Message Date
3c36619173
Merge pull request #36 from onyx-and-iris/add-projector-methods
Add projector methods
2023-10-10 17:38:53 +01:00
c4cf817042 split at full stop 2023-10-09 22:34:05 +01:00
ba5da8dfef upd obsbasic hotkey list in tests 2023-10-09 22:29:18 +01:00
83577e2d61 adds projector methods with a deprecation warning
patch bump

closes #35
2023-10-09 22:06:18 +01:00
Adem
8aa2e78ba6
Merge pull request #32 from onyx-and-iris/add-request-error-class
Error handling with base error class
2023-08-14 14:38:43 +03:00
780f07e25f minor version bump 2023-08-14 12:18:29 +01:00
70a422696e expand the Requests section in README
add a section about the {ReqClient}.send() method.
2023-08-14 11:11:46 +01:00
a7ef61018b refactor OBSSDKRequestError
reword error section in README
2023-08-14 00:44:59 +01:00
013cf15024 check req_name and code
for OBSSDKRequestError class
2023-08-12 14:51:44 +01:00
f88e8ee3a6 Errors section in readme updated 2023-08-11 22:35:25 +01:00
6fa24fe609 error tests added 2023-08-11 22:33:56 +01:00
ffd215aadf send now raises an OBSSDKRequestError
it is then logged and rethrown
2023-08-11 22:33:41 +01:00
f3e75c0ddf OBSSDKError is now the base custom error class
OBSSDKTimeoutError and OBSSDKRequestError subclass it

req_name and error code set as error class attributes.
2023-08-11 22:32:50 +01:00
5db7a705c5 log and rethrow TimeoutError on connection
we can just encode challenge here.

shorten opcode != 2 message
2023-08-11 22:31:03 +01:00
ca72b92eb3
Merge pull request #30 from aatikturk/client_auth_loggers
auth logger for clients
2023-07-04 17:17:44 +01:00
98b17b6749 add .python-version to .gitignore 2023-06-30 22:44:50 +01:00
5462c47b65 log errors raised in authenticate() 2023-06-28 17:56:56 +01:00
126e5cb0a4 raise OBSSDKError if auth reponse opcode != 2 2023-06-28 17:56:29 +01:00
Adem
4ced7193df patch bump 2023-06-23 01:53:02 +03:00
Adem
468c63f697 auth logger for clients
added RpcVersion in auth loggers for both requests and events clients.
removed the check in baseclient auth function and returned the whole response.
2023-06-23 01:48:45 +03:00
Adem
24f8487d93
Merge pull request #29 from onyx-and-iris/dev
added module level loggers.
2023-06-23 00:26:30 +03:00
2c07f242ad added module level loggers.
class loggers implemented as child loggers.

patch bump
2023-06-22 22:17:20 +01:00
9 changed files with 208 additions and 48 deletions

5
.gitignore vendored
View File

@ -45,6 +45,11 @@ env.bak/
venv.bak/ venv.bak/
.hatch .hatch
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
.python-version
# Test/config # Test/config
quick.py quick.py
config.toml config.toml

View File

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

View File

@ -11,11 +11,12 @@ from websocket import WebSocketTimeoutException
from .error import OBSSDKError, OBSSDKTimeoutError from .error import OBSSDKError, OBSSDKTimeoutError
logger = logging.getLogger(__name__)
class ObsClient: class ObsClient:
logger = logging.getLogger("baseclient.obsclient")
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.logger = logger.getChild(self.__class__.__name__)
defaultkwargs = { defaultkwargs = {
"host": "localhost", "host": "localhost",
"port": 4455, "port": 4455,
@ -42,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
@ -96,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()
@ -107,9 +108,15 @@ class ObsClient:
self.ws.send(json.dumps(payload)) self.ws.send(json.dumps(payload))
try: try:
response = json.loads(self.ws.recv()) response = json.loads(self.ws.recv())
return response["op"] == 2 if response["op"] != 2:
raise OBSSDKError(
"failed to identify client with the server, expected response with OpCode 2"
)
return response["d"]
except json.decoder.JSONDecodeError: except json.decoder.JSONDecodeError:
raise OBSSDKError("failed to identify client with the server") raise OBSSDKError(
"failed to identify client with the server, please check connection settings"
)
def req(self, req_type, req_data=None): def req(self, req_type, req_data=None):
payload = { payload = {

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

@ -7,7 +7,7 @@ from websocket import WebSocketTimeoutException
from .baseclient import ObsClient from .baseclient import ObsClient
from .callback import Callback from .callback import Callback
from .error import OBSSDKTimeoutError from .error import OBSSDKError, OBSSDKTimeoutError
from .subs import Subs from .subs import Subs
""" """
@ -16,17 +16,25 @@ defined in official github repo
https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#events https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#events
""" """
logger = logging.getLogger(__name__)
class EventClient: class EventClient:
logger = logging.getLogger("events.eventclient")
DELAY = 0.001 DELAY = 0.001
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.logger = logger.getChild(self.__class__.__name__)
defaultkwargs = {"subs": Subs.LOW_VOLUME} defaultkwargs = {"subs": Subs.LOW_VOLUME}
kwargs = defaultkwargs | kwargs kwargs = defaultkwargs | kwargs
self.base_client = ObsClient(**kwargs) self.base_client = ObsClient(**kwargs)
if self.base_client.authenticate(): try:
self.logger.info(f"Successfully identified {self} with the server") success = self.base_client.authenticate()
self.logger.info(
f"Successfully identified {self} with the server using RPC version:{success['negotiatedRpcVersion']}"
)
except OBSSDKError as e:
self.logger.error(f"{type(e).__name__}: {e}")
raise
self.callback = Callback() self.callback = Callback()
self.subscribe() self.subscribe()

View File

@ -1,7 +1,8 @@
import logging import logging
from warnings import warn
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
""" """
@ -10,14 +11,21 @@ defined in official github repo
https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#Requests https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#Requests
""" """
logger = logging.getLogger(__name__)
class ReqClient: class ReqClient:
logger = logging.getLogger("reqs.reqclient")
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.logger = logger.getChild(self.__class__.__name__)
self.base_client = ObsClient(**kwargs) self.base_client = ObsClient(**kwargs)
if self.base_client.authenticate(): try:
self.logger.info(f"Successfully identified {self} with the server") success = self.base_client.authenticate()
self.logger.info(
f"Successfully identified {self} with the server using RPC version:{success['negotiatedRpcVersion']}"
)
except OBSSDKError as e:
self.logger.error(f"{type(e).__name__}: {e}")
raise
def __enter__(self): def __enter__(self):
return self return self
@ -36,14 +44,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):
try:
response = self.base_client.req(param, data) response = self.base_client.req(param, data)
if not response["requestStatus"]["result"]: if not response["requestStatus"]["result"]:
error = ( raise OBSSDKRequestError(
f"Request {response['requestType']} returned code {response['requestStatus']['code']}", response["requestType"],
response["requestStatus"]["code"],
response["requestStatus"].get("comment"),
) )
if "comment" in response["requestStatus"]: except OBSSDKRequestError as e:
error += (f"With message: {response['requestStatus']['comment']}",) self.logger.exception(f"{type(e).__name__}: {e}")
raise OBSSDKError("\n".join(error)) raise
if "responseData" in response: if "responseData" in response:
if raw: if raw:
return response["responseData"] return response["responseData"]
@ -1928,3 +1939,66 @@ class ReqClient:
""" """
return self.send("GetMonitorList") return self.send("GetMonitorList")
def open_video_mix_projector(
self, video_mix_type, monitor_index=-1, projector_geometry=None
):
"""
Opens a projector for a specific output video mix.
The available mix types are:
OBS_WEBSOCKET_VIDEO_MIX_TYPE_PREVIEW
OBS_WEBSOCKET_VIDEO_MIX_TYPE_PROGRAM
OBS_WEBSOCKET_VIDEO_MIX_TYPE_MULTIVIEW
:param video_mix_type: Type of mix to open.
:type video_mix_type: str
:param monitor_index: Monitor index, use GetMonitorList to obtain index
:type monitor_index: int
:param projector_geometry:
Size/Position data for a windowed projector, in Qt Base64 encoded format.
Mutually exclusive with monitorIndex
:type projector_geometry: str
"""
warn(
"open_video_mix_projector request serves to provide feature parity with 4.x. "
"It is very likely to be changed/deprecated in a future release.",
DeprecationWarning,
stacklevel=2,
)
payload = {
"videoMixType": video_mix_type,
"monitorIndex": monitor_index,
"projectorGeometry": projector_geometry,
}
self.send("OpenVideoMixProjector", payload)
def open_source_projector(
self, source_name, monitor_index=-1, projector_geometry=None
):
"""
Opens a projector for a source.
:param source_name: Name of the source to open a projector for
:type source_name: str
:param monitor_index: Monitor index, use GetMonitorList to obtain index
:type monitor_index: int
:param projector_geometry:
Size/Position data for a windowed projector, in Qt Base64 encoded format.
Mutually exclusive with monitorIndex
:type projector_geometry: str
"""
warn(
"open_source_projector request serves to provide feature parity with 4.x. "
"It is very likely to be changed/deprecated in a future release.",
DeprecationWarning,
stacklevel=2,
)
payload = {
"sourceName": source_name,
"monitorIndex": monitor_index,
"projectorGeometry": projector_geometry,
}
self.send("OpenSourceProjector", payload)

View File

@ -1 +1 @@
version = "1.5.0" version = "1.6.1"

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

View File

@ -15,9 +15,9 @@ class TestRequests:
resp = req_cl.get_hot_key_list() resp = req_cl.get_hot_key_list()
obsbasic_hotkey_list = [ obsbasic_hotkey_list = [
"OBSBasic.SelectScene", "OBSBasic.SelectScene",
"OBSBasic.SelectScene", "OBSBasic.QuickTransition.1",
"OBSBasic.SelectScene", "OBSBasic.QuickTransition.2",
"OBSBasic.SelectScene", "OBSBasic.QuickTransition.3",
"OBSBasic.StartStreaming", "OBSBasic.StartStreaming",
"OBSBasic.StopStreaming", "OBSBasic.StopStreaming",
"OBSBasic.ForceStopStreaming", "OBSBasic.ForceStopStreaming",
@ -25,15 +25,17 @@ class TestRequests:
"OBSBasic.StopRecording", "OBSBasic.StopRecording",
"OBSBasic.PauseRecording", "OBSBasic.PauseRecording",
"OBSBasic.UnpauseRecording", "OBSBasic.UnpauseRecording",
"OBSBasic.SplitFile",
"OBSBasic.StartReplayBuffer", "OBSBasic.StartReplayBuffer",
"OBSBasic.StopReplayBuffer", "OBSBasic.StopReplayBuffer",
"OBSBasic.StartVirtualCam", "OBSBasic.StartVirtualCam",
"OBSBasic.StopVirtualCam", "OBSBasic.StopVirtualCam",
"OBSBasic.EnablePreview", "OBSBasic.EnablePreview",
"OBSBasic.DisablePreview", "OBSBasic.DisablePreview",
"OBSBasic.EnablePreviewProgram",
"OBSBasic.DisablePreviewProgram",
"OBSBasic.ShowContextBar", "OBSBasic.ShowContextBar",
"OBSBasic.HideContextBar", "OBSBasic.HideContextBar",
"OBSBasic.TogglePreviewProgram",
"OBSBasic.Transition", "OBSBasic.Transition",
"OBSBasic.ResetStats", "OBSBasic.ResetStats",
"OBSBasic.Screenshot", "OBSBasic.Screenshot",