Compare commits

...

26 Commits

Author SHA1 Message Date
4e45de17ea
Merge pull request #27 from aatikturk/25-question-set-timeout-for-connection-request
Added 'timeout' option for  baseclient
2023-06-19 18:25:12 +01:00
491a26aaf7 minor ver bump 2023-06-19 17:51:16 +01:00
d84d30b752 update readme Errors section 2023-06-19 17:46:43 +01:00
9e3c1d3f37 raise timeout errors.
added some error/exception logging.

added timeout parameter to repr methods.
2023-06-19 17:45:49 +01:00
82b6cdcd04 add error class OBSSDKTimeoutError 2023-06-19 17:44:10 +01:00
Adem
64a7c2b753 update readme and base client 2023-06-14 01:09:44 +03:00
Adem
15559fdb33 updated readme 2023-05-29 10:48:41 +00:00
Adem
3adf094481 Added 'timeout' option for baseclient. bumped version 2023-05-29 10:34:40 +00:00
Adem
9c41f2bb59
Merge pull request #24 from onyx-and-iris/dev
check user home directory for config.toml
2023-03-11 22:48:43 +03:00
d1c7462cc6 patch bump 2023-03-09 01:38:53 +00:00
2de7151739 update README
advises placing config.toml in user home dir
2023-03-09 01:36:21 +00:00
91ba90056c adds get_filepath
traverses a list of paths for config.toml
2023-03-09 01:34:44 +00:00
Adem
5e68262a80 fix sceneItemIndex key in the payload for set_scene_item_index method 2023-01-08 20:21:12 +03:00
Adem
ef0f770c0c
Merge pull request #20 from onyx-and-iris/dev
add conn info to __repr__ methods, lower required python ver to 3.9 + other small changes
2022-12-05 22:17:03 +03:00
48e90c82fb alter format of __repr__ in Req + Event clients
password now defaults to empty string, not None.
2022-12-05 18:18:10 +00:00
cc9b1e2c72 lower min python required version to 3.9
python ver test matrix added to hatch config

minor version bump
2022-12-05 16:49:17 +00:00
41b0dfbe4b ensure studio mode is disabled at end of test run 2022-12-05 16:43:07 +00:00
cf888b0c4a conn paramters added to __repr__ magic methods
add __str__ override (used in logger)
2022-12-05 16:41:34 +00:00
92e2c29bd6 enum.py renamed to subs.py.
No changes to file contents.

relative import changed in __init__.py
2022-12-05 16:39:33 +00:00
335fa42948
Merge pull request #17 from kamalmostafa/no-tomllib
allow use without installing tomllib
2022-12-04 19:37:25 +00:00
83afe31e04
Update baseclient.py
lazy load tomli/tomllib as suggested in #17
2022-12-04 19:34:55 +00:00
5294e1afe2
Merge pull request #18 from kamalmostafa/send-raw
send(..., raw=True) returns raw responseData
2022-12-04 18:07:08 +00:00
c6cbe1c894
Merge pull request #19 from kamalmostafa/fix-SendStreamCaption
SendStreamCaption requires payload "captionText"
2022-12-04 18:06:42 +00:00
Kamal Mostafa
13ef8108df SendStreamCaption requires payload "captionText" 2022-11-30 06:47:15 +00:00
Kamal Mostafa
3786739eee send(..., raw=True) returns raw responseData
Passing raw=True returns unprocessed responseData payload, allowing
for application-level handling of websocket commands unimplemented
by the library or for testing.
2022-11-30 06:46:16 +00:00
Kamal Mostafa
71c1e65483 allow use without installing tomllib
When ObsClient(host='...', port='...', password='...') are provided,
importing tomllib is not actually necessary.  Allow for tomllib to
not be installed at all, and only raise a tomllib ModuleNotFoundError
if (host, port, password) are not provided.
2022-11-30 06:09:19 +00:00
16 changed files with 120 additions and 40 deletions

5
.gitignore vendored
View File

@ -47,4 +47,7 @@ venv.bak/
# Test/config
quick.py
config.toml
config.toml
obsws.log
.vscode/

View File

@ -5,7 +5,6 @@
# A Python SDK for OBS Studio WebSocket v5.0
This is a wrapper around OBS Websocket.
Not all endpoints in the official documentation are implemented.
## Requirements
@ -13,7 +12,7 @@ Not all endpoints in the official documentation are implemented.
- [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.10 or greater
- Python 3.9 or greater
### How to install using pip
@ -27,7 +26,8 @@ By default the clients connect with parameters:
- `host`: "localhost"
- `port`: 4455
- `password`: None
- `password`: ""
- `timeout`: None
You may override these parameters by storing them in a toml config file or passing them as keyword arguments.
@ -44,7 +44,7 @@ port = 4455
password = "mystrongpass"
```
It should be placed next to your `__main__.py` file.
It should be placed in your user home directory.
#### Otherwise:
@ -54,7 +54,7 @@ Example `__main__.py`:
import obsws_python as obs
# pass conn info if not in config.toml
cl = obs.ReqClient(host='localhost', port=4455, password='mystrongpass')
cl = obs.ReqClient(host='localhost', port=4455, password='mystrongpass', timeout=3)
# Toggle the mute state of your Mic input
cl.toggle_input_mute('Mic/Aux')
@ -132,6 +132,8 @@ 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.
### Logging
If you want to see the raw messages simply set log level to DEBUG

View File

@ -6,7 +6,7 @@ Registers a list of callback functions to hook into OBS events.
Simply run the code and trigger the events, press `<Enter>` to exit.
This example assumes the existence of a `config.toml`, placed next to `__main__.py`:
This example assumes the existence of a `config.toml`, placed in your user home directory:
```toml
[connection]

View File

@ -8,7 +8,7 @@ Requires [Python Keyboard library](https://github.com/boppreh/keyboard).
Simply run the code and press the assigned hotkeys. Press `ctrl+enter` to exit.
This example assumes the existence of a `config.toml`, placed next to `__main__.py`:
This example assumes the existence of a `config.toml`, placed in your user home directory:
```toml
[connection]

View File

@ -4,7 +4,7 @@ Prints POSTFADER level values for audio device `Desktop Audio`. If mute toggled
## Use
This example assumes the existence of a `config.toml`, placed next to `__main__.py`:
This example assumes the existence of a `config.toml`, placed in your user home directory:
```toml
[connection]

View File

@ -4,7 +4,7 @@ Collects the names of all available scenes, rotates through them and prints thei
## Use
This example assumes the existence of a `config.toml`, placed next to `__main__.py`:
This example assumes the existence of a `config.toml`, placed in your user home directory:
```toml
[connection]

View File

@ -1,6 +1,6 @@
from .enum import Subs
from .events import EventClient
from .reqs import ReqClient
from .subs import Subs
from .version import version as __version__
__ALL__ = ["ReqClient", "EventClient", "Subs"]

View File

@ -4,22 +4,25 @@ import json
import logging
from pathlib import Path
from random import randint
try:
import tomllib
except ModuleNotFoundError:
import tomli as tomllib
from typing import Optional
import websocket
from websocket import WebSocketTimeoutException
from .error import OBSSDKError
from .error import OBSSDKError, OBSSDKTimeoutError
class ObsClient:
logger = logging.getLogger("baseclient.obsclient")
def __init__(self, **kwargs):
defaultkwargs = {"host": "localhost", "port": 4455, "password": None, "subs": 0}
defaultkwargs = {
"host": "localhost",
"port": 4455,
"password": "",
"subs": 0,
"timeout": None,
}
if not any(key in kwargs for key in ("host", "port", "password")):
kwargs |= self._conn_from_toml()
kwargs = defaultkwargs | kwargs
@ -27,21 +30,47 @@ class ObsClient:
setattr(self, attr, val)
self.logger.info(
"Connecting with parameters: {host} {port} {password} {subs}".format(
"Connecting with parameters: host='{host}' port={port} password='{password}' subs={subs} timeout={timeout}".format(
**self.__dict__
)
)
self.ws = websocket.WebSocket()
self.ws.connect(f"ws://{self.host}:{self.port}")
self.server_hello = json.loads(self.ws.recv())
try:
self.ws = websocket.WebSocket()
self.ws.connect(f"ws://{self.host}:{self.port}", timeout=self.timeout)
self.server_hello = json.loads(self.ws.recv())
except ValueError as e:
self.logger.error(f"{type(e).__name__}: {e}")
raise
except (ConnectionRefusedError, WebSocketTimeoutException) as e:
self.logger.exception(f"{type(e).__name__}: {e}")
raise
def _conn_from_toml(self) -> dict:
try:
import tomllib
except ModuleNotFoundError:
import tomli as tomllib
def get_filepath() -> Optional[Path]:
"""
traverses a list of paths for a 'config.toml'
returns the first config file found or None.
"""
filepaths = [
Path.cwd() / "config.toml",
Path.home() / "config.toml",
Path.home() / ".config" / "obsws-python" / "config.toml",
]
for filepath in filepaths:
if filepath.exists():
return filepath
conn = {}
filepath = Path.cwd() / "config.toml"
if filepath.exists():
if filepath := get_filepath():
with open(filepath, "rb") as f:
conn = tomllib.load(f)
self.logger.info(f"loading config from {filepath}")
return conn["connection"] if "connection" in conn else conn
def authenticate(self):
@ -90,7 +119,11 @@ class ObsClient:
if req_data:
payload["d"]["requestData"] = req_data
self.logger.debug(f"Sending request {payload}")
self.ws.send(json.dumps(payload))
response = json.loads(self.ws.recv())
try:
self.ws.send(json.dumps(payload))
response = json.loads(self.ws.recv())
except WebSocketTimeoutException as e:
self.logger.exception(f"{type(e).__name__}: {e}")
raise OBSSDKTimeoutError("Timeout while trying to send the request") from e
self.logger.debug(f"Response received {response}")
return response["d"]

View File

@ -1,4 +1,6 @@
class OBSSDKError(Exception):
"""general errors"""
"""Exception raised when general errors occur"""
pass
class OBSSDKTimeoutError(Exception):
"""Exception raised when a connection times out"""

View File

@ -3,9 +3,12 @@ import logging
import time
from threading import Thread
from websocket import WebSocketTimeoutException
from .baseclient import ObsClient
from .callback import Callback
from .enum import Subs
from .error import OBSSDKTimeoutError
from .subs import Subs
"""
A class to interact with obs-websocket events
@ -28,6 +31,13 @@ class EventClient:
self.subscribe()
def __repr__(self):
return type(
self
).__name__ + "(host='{host}', port={port}, password='{password}', subs={subs}, timeout={timeout})".format(
**self.base_client.__dict__,
)
def __str__(self):
return type(self).__name__
def subscribe(self):
@ -42,7 +52,11 @@ class EventClient:
"""
self.running = True
while self.running:
event = json.loads(self.base_client.ws.recv())
try:
event = json.loads(self.base_client.ws.recv())
except WebSocketTimeoutException as e:
self.logger.exception(f"{type(e).__name__}: {e}")
raise OBSSDKTimeoutError("Timeout while waiting for event") from e
self.logger.debug(f"Event received {event}")
type_, data = (
event["d"].get("eventType"),

View File

@ -26,9 +26,16 @@ class ReqClient:
self.base_client.ws.close()
def __repr__(self):
return type(
self
).__name__ + "(host='{host}', port={port}, password='{password}', timeout={timeout})".format(
**self.base_client.__dict__,
)
def __str__(self):
return type(self).__name__
def send(self, param, data=None):
def send(self, param, data=None, raw=False):
response = self.base_client.req(param, data)
if not response["requestStatus"]["result"]:
error = (
@ -38,6 +45,8 @@ class ReqClient:
error += (f"With message: {response['requestStatus']['comment']}",)
raise OBSSDKError("\n".join(error))
if "responseData" in response:
if raw:
return response["responseData"]
return as_dataclass(response["requestType"], response["responseData"])
def get_version(self):
@ -1485,7 +1494,7 @@ class ReqClient:
payload = {
"sceneName": scene_name,
"sceneItemId": item_id,
"sceneItemLocked": item_index,
"sceneItemIndex": item_index,
}
self.send("SetSceneItemIndex", payload)
@ -1729,7 +1738,10 @@ class ReqClient:
"""
self.send("SendStreamCaption")
payload = {
"captionText": caption,
}
self.send("SendStreamCaption", payload)
def get_record_status(self):
"""

View File

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

View File

@ -8,7 +8,7 @@ dynamic = ["version"]
description = "A Python SDK for OBS Studio WebSocket v5.0"
readme = "README.md"
license = "GPL-3.0-only"
requires-python = ">=3.10"
requires-python = ">=3.9"
authors = [
{ name = "Adem Atikturk", email = "aatikturk@gmail.com" },
]
@ -37,7 +37,18 @@ include = [
]
[tool.hatch.envs.e.scripts]
events = "py {root}\\examples\\events\\."
hotkeys = "py {root}\\examples\\hotkeys\\."
levels = "py {root}\\examples\\levels\\."
scene_rotate = "py {root}\\examples\\scene_rotate\\."
events = "python {root}\\examples\\events\\."
hotkeys = "python {root}\\examples\\hotkeys\\."
levels = "python {root}\\examples\\levels\\."
scene_rotate = "python {root}\\examples\\scene_rotate\\."
[tool.hatch.envs.test]
dependencies = [
"pytest",
]
[tool.hatch.envs.test.scripts]
run = 'pytest -v'
[[tool.hatch.envs.test.matrix]]
python = ["39", "310", "311"]

View File

@ -40,7 +40,7 @@ EXTRAS_REQUIRE = {
}
# Python version requirement
PYTHON_REQUIRES = ">=3.10"
PYTHON_REQUIRES = ">=3.9"
setup(
name=PACKAGE_NAME,

View File

@ -13,4 +13,7 @@ def teardown_module():
req_cl.remove_scene("START_TEST")
req_cl.remove_scene("BRB_TEST")
req_cl.remove_scene("END_TEST")
resp = req_cl.get_studio_mode_enabled()
if resp.studio_mode_enabled:
req_cl.set_studio_mode_enabled(False)
req_cl.base_client.ws.close()