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/
.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
quick.py
config.toml

View File

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

View File

@ -11,11 +11,12 @@ from websocket import WebSocketTimeoutException
from .error import OBSSDKError, OBSSDKTimeoutError
logger = logging.getLogger(__name__)
class ObsClient:
logger = logging.getLogger("baseclient.obsclient")
def __init__(self, **kwargs):
self.logger = logger.getChild(self.__class__.__name__)
defaultkwargs = {
"host": "localhost",
"port": 4455,
@ -42,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
@ -96,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()
@ -107,9 +108,15 @@ class ObsClient:
self.ws.send(json.dumps(payload))
try:
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:
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):
payload = {

View File

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

View File

@ -7,7 +7,7 @@ from websocket import WebSocketTimeoutException
from .baseclient import ObsClient
from .callback import Callback
from .error import OBSSDKTimeoutError
from .error import OBSSDKError, OBSSDKTimeoutError
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
"""
logger = logging.getLogger(__name__)
class EventClient:
logger = logging.getLogger("events.eventclient")
DELAY = 0.001
def __init__(self, **kwargs):
self.logger = logger.getChild(self.__class__.__name__)
defaultkwargs = {"subs": Subs.LOW_VOLUME}
kwargs = defaultkwargs | kwargs
self.base_client = ObsClient(**kwargs)
if self.base_client.authenticate():
self.logger.info(f"Successfully identified {self} with the server")
try:
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.subscribe()

View File

@ -1,7 +1,8 @@
import logging
from warnings import warn
from .baseclient import ObsClient
from .error import OBSSDKError
from .error import OBSSDKError, OBSSDKRequestError
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
"""
logger = logging.getLogger(__name__)
class ReqClient:
logger = logging.getLogger("reqs.reqclient")
def __init__(self, **kwargs):
self.logger = logger.getChild(self.__class__.__name__)
self.base_client = ObsClient(**kwargs)
if self.base_client.authenticate():
self.logger.info(f"Successfully identified {self} with the server")
try:
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):
return self
@ -36,14 +44,17 @@ class ReqClient:
return type(self).__name__
def send(self, param, data=None, raw=False):
try:
response = self.base_client.req(param, data)
if not response["requestStatus"]["result"]:
error = (
f"Request {response['requestType']} returned code {response['requestStatus']['code']}",
raise OBSSDKRequestError(
response["requestType"],
response["requestStatus"]["code"],
response["requestStatus"].get("comment"),
)
if "comment" in response["requestStatus"]:
error += (f"With message: {response['requestStatus']['comment']}",)
raise OBSSDKError("\n".join(error))
except OBSSDKRequestError as e:
self.logger.exception(f"{type(e).__name__}: {e}")
raise
if "responseData" in response:
if raw:
return response["responseData"]
@ -1928,3 +1939,66 @@ class ReqClient:
"""
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()
obsbasic_hotkey_list = [
"OBSBasic.SelectScene",
"OBSBasic.SelectScene",
"OBSBasic.SelectScene",
"OBSBasic.SelectScene",
"OBSBasic.QuickTransition.1",
"OBSBasic.QuickTransition.2",
"OBSBasic.QuickTransition.3",
"OBSBasic.StartStreaming",
"OBSBasic.StopStreaming",
"OBSBasic.ForceStopStreaming",
@ -25,15 +25,17 @@ class TestRequests:
"OBSBasic.StopRecording",
"OBSBasic.PauseRecording",
"OBSBasic.UnpauseRecording",
"OBSBasic.SplitFile",
"OBSBasic.StartReplayBuffer",
"OBSBasic.StopReplayBuffer",
"OBSBasic.StartVirtualCam",
"OBSBasic.StopVirtualCam",
"OBSBasic.EnablePreview",
"OBSBasic.DisablePreview",
"OBSBasic.EnablePreviewProgram",
"OBSBasic.DisablePreviewProgram",
"OBSBasic.ShowContextBar",
"OBSBasic.HideContextBar",
"OBSBasic.TogglePreviewProgram",
"OBSBasic.Transition",
"OBSBasic.ResetStats",
"OBSBasic.Screenshot",