diff --git a/README.md b/README.md index ada4121..27228cd 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://github.com/aatikturk/obsstudio_sdk/blob/main/LICENSE) +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/) + # A Python SDK for OBS Studio WebSocket v5.0 This is a wrapper around OBS Websocket. @@ -36,7 +40,7 @@ Import and start using, keyword arguments are as follows: - `port`: port to access server - `password`: obs websocket server password -Example `__main__.py` +Example `__main__.py`: ```python import obsstudio_sdk as obs @@ -48,6 +52,90 @@ cl = obs.ReqClient(host='localhost', port=4455, password='mystrongpass') cl.toggle_input_mute('Mic/Aux') ``` +### Requests + +Method names for requests match the API calls but snake cased. + +example: + +```python +cl = ReqClient() + +# GetVersion +resp = cl.get_version() + +# SetCurrentProgramScene +cl.set_current_program_scene() +``` + +For a full list of requests refer to [Requests](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#requests) + +### Events + +When registering a function callback use the name of the expected API event in snake case form. + +example: + +```python +cl = EventClient() + +def scene_created(data): + ... + +# SceneCreated +cl.callback.register(scene_created) + +def input_mute_state_changed(data): + ... + +# InputMuteStateChanged +cl.callback.register(input_mute_state_changed) + +# returns a list of currently registered events +print(cl.callback.get()) + +# You may also deregister a callback +cl.callback.deregister(input_mute_state_changed) +``` + +`register(fns)` and `deregister(fns)` accept both single functions and lists of functions. + +For a full list of events refer to [Events](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#events) + +### Attributes + +For both request responses and event data you may inspect the available attributes using `attrs()`. + +example: + +```python +resp = cl.get_version() +print(resp.attrs()) + +def scene_created(data): + print(data.attrs()) +``` + +### 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) + +### Tests + +First install development dependencies: + +`pip install -e .['dev']` + +To run all tests: + +``` +pytest -v +``` + ### Official Documentation +For the full documentation: + - [OBS Websocket SDK](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#obs-websocket-501-protocol) diff --git a/obsstudio_sdk/__init__.py b/obsstudio_sdk/__init__.py index d0ba897..6ab08c5 100644 --- a/obsstudio_sdk/__init__.py +++ b/obsstudio_sdk/__init__.py @@ -1,4 +1,4 @@ from .events import EventClient from .reqs import ReqClient -__ALL__ = ["ReqClient", "EventsClient"] +__ALL__ = ["ReqClient", "EventClient"] diff --git a/obsstudio_sdk/callback.py b/obsstudio_sdk/callback.py index 1924848..28e090d 100644 --- a/obsstudio_sdk/callback.py +++ b/obsstudio_sdk/callback.py @@ -17,7 +17,7 @@ class Callback: return [to_camel_case(fn.__name__[2:]) for fn in self._callbacks] def trigger(self, event, data): - """trigger callback on update""" + """trigger callback on event""" for fn in self._callbacks: if fn.__name__ == f"on_{to_snake_case(event)}": @@ -36,7 +36,7 @@ class Callback: self._callbacks.append(fns) def deregister(self, fns: Union[Iterable, Callable]): - """deregisters a callback from _callbacks""" + """deregisters callback functions""" try: iterator = iter(fns) diff --git a/obsstudio_sdk/reqs.py b/obsstudio_sdk/reqs.py index 46679f1..4e95891 100644 --- a/obsstudio_sdk/reqs.py +++ b/obsstudio_sdk/reqs.py @@ -592,7 +592,7 @@ class ReqClient: } self.send("SetSceneSceneTransitionOverride", payload) - def get_input_list(self, kind): + def get_input_list(self, kind=None): """ Gets a list of all inputs in OBS. diff --git a/setup.py b/setup.py index 111984e..34dfced 100644 --- a/setup.py +++ b/setup.py @@ -1,39 +1,50 @@ import pathlib -from setuptools import setup, find_packages + +from setuptools import find_packages, setup HERE = pathlib.Path(__file__).parent -VERSION = '1.0.1' -PACKAGE_NAME = 'obsstudio_sdk' -AUTHOR = 'Adem Atikturk' -AUTHOR_EMAIL = 'aatikturk@gmail.com' -URL = 'https://github.com/aatikturk/obsstudio_sdk' -LICENSE = 'GNU General Public License v3.0' -DESCRIPTION = 'A Python SDK for OBS Studio WebSocket v5.0' +VERSION = "1.0.1" +PACKAGE_NAME = "obsstudio_sdk" +AUTHOR = "Adem Atikturk" +AUTHOR_EMAIL = "aatikturk@gmail.com" +URL = "https://github.com/aatikturk/obsstudio_sdk" +LICENSE = "GNU General Public License v3.0" +DESCRIPTION = "A Python SDK for OBS Studio WebSocket v5.0" LONG_DESCRIPTION = (HERE / "README.md").read_text() LONG_DESC_TYPE = "text/markdown" # Dependencies for the package -INSTALL_REQUIRES = [ - 'websocket-client' -] +INSTALL_REQUIRES = ["websocket-client"] + +# Development dependencies +EXTRAS_REQUIRE = { + "dev": [ + "pytest", + "pytest-randomly", + "black", + "isort", + ] +} # Python version requirement -PYTHON_REQUIRES = '>=3.11' +PYTHON_REQUIRES = ">=3.11" -setup(name=PACKAGE_NAME, - version=VERSION, - description=DESCRIPTION, - long_description=LONG_DESCRIPTION, - long_description_content_type=LONG_DESC_TYPE, - author=AUTHOR, - license=LICENSE, - author_email=AUTHOR_EMAIL, - url=URL, - install_requires=INSTALL_REQUIRES, - python_requires=PYTHON_REQUIRES, - packages=find_packages() - ) +setup( + name=PACKAGE_NAME, + version=VERSION, + description=DESCRIPTION, + long_description=LONG_DESCRIPTION, + long_description_content_type=LONG_DESC_TYPE, + author=AUTHOR, + license=LICENSE, + author_email=AUTHOR_EMAIL, + url=URL, + install_requires=INSTALL_REQUIRES, + extras_require=EXTRAS_REQUIRE, + python_requires=PYTHON_REQUIRES, + packages=find_packages(), +) diff --git a/tests/__init__.py b/tests/__init__.py index d341e9e..af852cd 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -4,8 +4,13 @@ req_cl = obs.ReqClient() def setup_module(): - pass + req_cl.create_scene("START_TEST") + req_cl.create_scene("BRB_TEST") + req_cl.create_scene("END_TEST") def teardown_module(): + req_cl.remove_scene("START_TEST") + req_cl.remove_scene("BRB_TEST") + req_cl.remove_scene("END_TEST") req_cl.base_client.ws.close() diff --git a/tests/test_attrs.py b/tests/test_attrs.py index 689ae8a..6fe1617 100644 --- a/tests/test_attrs.py +++ b/tests/test_attrs.py @@ -1,5 +1,3 @@ -import pytest - from tests import req_cl diff --git a/tests/test_request.py b/tests/test_request.py index b7cc14a..5996056 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -11,34 +11,13 @@ class TestRequests: assert hasattr(resp, "obs_version") assert hasattr(resp, "obs_web_socket_version") - @pytest.mark.parametrize( - "scene", - [ - ("START"), - ("BRB"), - ("END"), - ], - ) - def test_current_program_scene(self, scene): - req_cl.set_current_program_scene(scene) - resp = req_cl.get_current_program_scene() - assert resp.current_program_scene_name == scene - - @pytest.mark.parametrize( - "state", - [ - (False), - (True), - ], - ) - def test_studio_mode_enabled(self, state): - req_cl.set_studio_mode_enabled(state) - resp = req_cl.get_studio_mode_enabled() - assert resp.studio_mode_enabled == state - def test_get_hot_key_list(self): resp = req_cl.get_hot_key_list() - hotkey_list = [ + obsbasic_hotkey_list = [ + "OBSBasic.SelectScene", + "OBSBasic.SelectScene", + "OBSBasic.SelectScene", + "OBSBasic.SelectScene", "OBSBasic.StartStreaming", "OBSBasic.StopStreaming", "OBSBasic.ForceStopStreaming", @@ -59,29 +38,8 @@ class TestRequests: "OBSBasic.ResetStats", "OBSBasic.Screenshot", "OBSBasic.SelectedSourceScreenshot", - "libobs.mute", - "libobs.unmute", - "libobs.push-to-mute", - "libobs.push-to-talk", - "libobs.mute", - "libobs.unmute", - "libobs.push-to-mute", - "libobs.push-to-talk", - "OBSBasic.SelectScene", - "OBSBasic.SelectScene", - "OBSBasic.SelectScene", - "OBSBasic.SelectScene", - "libobs.show_scene_item.Colour Source 2", - "libobs.hide_scene_item.Colour Source 2", - "libobs.show_scene_item.Colour Source 3", - "libobs.hide_scene_item.Colour Source 3", - "libobs.show_scene_item.Colour Source", - "libobs.hide_scene_item.Colour Source", - "OBSBasic.QuickTransition.1", - "OBSBasic.QuickTransition.2", - "OBSBasic.QuickTransition.3", ] - assert all(x in resp.hotkeys for x in hotkey_list) + assert all(x in resp.hotkeys for x in obsbasic_hotkey_list) @pytest.mark.parametrize( "name,data", @@ -103,9 +61,53 @@ class TestRequests: resp = req_cl.get_profile_list() assert "test" not in resp.profiles + def test_stream_service_settings(self): + settings = { + "server": "rtmp://addressofrtmpserver", + "key": "live_myvery_secretkey", + } + req_cl.set_stream_service_settings( + "rtmp_common", + settings, + ) + resp = req_cl.get_stream_service_settings() + assert resp.stream_service_type == "rtmp_common" + assert resp.stream_service_settings == { + "server": "rtmp://addressofrtmpserver", + "key": "live_myvery_secretkey", + } + + @pytest.mark.parametrize( + "scene", + [ + ("START_TEST"), + ("BRB_TEST"), + ("END_TEST"), + ], + ) + def test_current_program_scene(self, scene): + req_cl.set_current_program_scene(scene) + resp = req_cl.get_current_program_scene() + assert resp.current_program_scene_name == scene + + def test_input_list(self): + req_cl.create_input( + "START_TEST", "test", "color_source_v3", {"color": 4294945535}, True + ) + resp = req_cl.get_input_list() + assert { + "inputKind": "color_source_v3", + "inputName": "test", + "unversionedInputKind": "color_source", + } in resp.inputs + resp = req_cl.get_input_settings("test") + assert resp.input_kind == "color_source_v3" + assert resp.input_settings == {"color": 4294945535} + req_cl.remove_input("test") + def test_source_filter(self): - req_cl.create_source_filter("START", "test", "color_key_filter_v2") - resp = req_cl.get_source_filter_list("START") + req_cl.create_source_filter("START_TEST", "test", "color_key_filter_v2") + resp = req_cl.get_source_filter_list("START_TEST") assert resp.filters == [ { "filterEnabled": True, @@ -115,4 +117,16 @@ class TestRequests: "filterSettings": {}, } ] - req_cl.remove_source_filter("START", "test") + req_cl.remove_source_filter("START_TEST", "test") + + @pytest.mark.parametrize( + "state", + [ + (False), + (True), + ], + ) + def test_studio_mode_enabled(self, state): + req_cl.set_studio_mode_enabled(state) + resp = req_cl.get_studio_mode_enabled() + assert resp.studio_mode_enabled == state