Compare commits

..

9 Commits

Author SHA1 Message Date
99ab0f00f8 reword 2023-06-28 04:31:54 +01:00
591391aeb7 return raw message if raw.
patch bump
2023-06-28 04:29:02 +01:00
48f7be2962 add link to changelog in readme 2023-06-28 03:39:51 +01:00
e0a55e8cd9 reword 2023-06-28 03:38:45 +01:00
466f34f6a5 1.0.0 section added to changelog
readme updated to reflect changes.
2023-06-28 03:36:38 +01:00
418b97254f debug script added to pyproject.toml
bump to major version 1.0.0

min python required version changed to 3.8
2023-06-28 02:49:15 +01:00
4349b28123 msg renamed to data 2023-06-28 02:48:12 +01:00
a611f67f49 add debug example 2023-06-28 02:47:23 +01:00
5dc69cc655 lazy load tomllib/tomli
raise SteamlabsSIOConnectionError on connection error

Path.home() / ".config" / "streamlabsio" / "config.toml"
added to config filepaths

raw kwarg added.

module level logger added.
2023-06-28 02:45:04 +01:00
11 changed files with 459 additions and 65 deletions

9
.gitignore vendored
View File

@ -128,5 +128,12 @@ dmypy.json
# Pyre type checker # Pyre type checker
.pyre/ .pyre/
.vscode/
# toml config # toml config
config.toml config.toml
# test
quick.py
logging.json
tests/

30
CHANGELOG.md Normal file
View File

@ -0,0 +1,30 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
- [x]
## [1.0.0] - 2023-06-28
The only potential breaking change, a new error class raised if the initial connection fails.
### Added
- tomllib/tomli now lazy loaded in _token_from_toml(). Allows possibility to run package without tomli.
- module level logger
- debug example
- raw kwarg for receiving the raw json data.
- `Path.home() / ".config" / "streamlabsio" / "config.toml"` added to config.toml filepaths.
### Changed
- Python minimum required version changed to 3.8
- new error class
- `SteamlabsSIOConnectionError` raised on a connection error

View File

@ -5,11 +5,15 @@
# A Python client for Streamlabs Socket API # A Python client for Streamlabs Socket API
For an outline of past/future changes refer to: [CHANGELOG](CHANGELOG.md)
### Requirements ### Requirements
- A Streamlabs Socket API key. - A Streamlabs Socket API key.
- You can acquire this by logging into your Streamlabs.com dashboard then `Settings->Api Settings->API Tokens` - You can acquire this by logging into your Streamlabs.com dashboard then `Settings->Api Settings->API Tokens`
- Python 3.8 or greater
### How to install using pip ### How to install using pip
``` ```
@ -37,8 +41,8 @@ Example `__main__.py`:
import streamlabsio import streamlabsio
def on_twitch_event(event, msg): def on_twitch_event(event, data):
print(f"{event}: {msg.attrs()}") print(f"{event}: {data.attrs()}")
def main(): def main():
@ -58,17 +62,33 @@ if __name__ == "__main__":
From the [SocketIO docs](https://python-socketio.readthedocs.io/en/latest/client.html#managing-background-tasks), `client.sio.wait()` may be used if your application has nothing to do in the main thread. From the [SocketIO docs](https://python-socketio.readthedocs.io/en/latest/client.html#managing-background-tasks), `client.sio.wait()` may be used if your application has nothing to do in the main thread.
### Client class
`streamlabsio.connect(token="<apikey>", raw=False)`
The following keyword arguments may be passed:
- `token`: str Streamlabs SocketIO api token.
- `raw`: boolean=False Receive raw data messages as json objects.
### Attributes ### Attributes
For event messages you may inspect the available attributes using `attrs()`. For event data you may inspect the available attributes using `attrs()`.
example: example:
```python ```python
def on_twitch_event(event, msg): def on_twitch_event(event, data):
print(f"{event}: {msg.attrs()}") print(f"{event}: {data.attrs()}")
``` ```
### Errors
- `SteamlabsSIOConnectionError`: Exception raised when connection errors occur
### Logging
To view raw incoming event data set logging level to DEBUG. Check `debug` example.
### Official Documentation ### Official Documentation
- [Streamlabs Socket API](https://dev.streamlabs.com/docs/socket-api) - [Streamlabs Socket API](https://dev.streamlabs.com/docs/socket-api)

View File

@ -1,21 +1,19 @@
import logging
import streamlabsio import streamlabsio
logging.basicConfig(level=logging.INFO)
def on_youtube_event(event, data):
print(f"{event}: {data.attrs()}")
def on_youtube_event(event, msg): def on_twitch_event(event, data):
print(f"{event}: {msg.attrs()}")
def on_twitch_event(event, msg):
if event == "follow": if event == "follow":
print(f"Received follow from {msg.name}") print(f"Received follow from {data.name}")
elif event == "bits": elif event == "bits":
print(f"{msg.name} donated {msg.amount} bits! With message: {msg.message}") print(f"{data.name} donated {data.amount} bits! With message: {data.message}")
elif event == "donation": elif event == "donation":
print(f"{msg.name} donated {msg.formatted_amount}! With message: {msg.message}") print(
f"{data.name} donated {data.formatted_amount}! With message: {data.message}"
)
def main(): def main():

9
examples/debug/README.md Normal file
View File

@ -0,0 +1,9 @@
## About
The underlying socketio and engineio packages emit a lot of logs so it may be useful to filter out streamlabsio logs.
This example prints raw messages whenever Client.event_handler() receives data.
## Use
Run the script and trigger any of the events with `Test Widgets` in the Streamlabs GUI.

View File

@ -0,0 +1,50 @@
from logging import config
import streamlabsio
config.dictConfig(
{
"version": 1,
"formatters": {
"standard": {
"format": "%(asctime)s,%(msecs)d %(name)s %(levelname)s %(message)s"
}
},
"handlers": {
"stream": {
"level": "DEBUG",
"class": "logging.StreamHandler",
"formatter": "standard",
}
},
"loggers": {"streamlabsio.client": {"handlers": ["stream"], "level": "DEBUG"}},
}
)
def on_youtube_event(event, data):
print(f"{event}: {data.attrs()}")
def on_twitch_event(event, data):
if event == "follow":
print(f"Received follow from {data.name}")
elif event == "bits":
print(f"{data.name} donated {data.amount} bits! With message: {data.message}")
elif event == "donation":
print(
f"{data.name} donated {data.formatted_amount}! With message: {data.message}"
)
def main():
with streamlabsio.connect() as client:
client.obs.on("streamlabs", on_twitch_event)
client.obs.on("twitch_account", on_twitch_event)
client.obs.on("youtube_account", on_youtube_event)
client.sio.sleep(30)
if __name__ == "__main__":
main()

283
poetry.lock generated
View File

@ -1,6 +1,6 @@
[[package]] [[package]]
name = "black" name = "black"
version = "22.10.0" version = "22.12.0"
description = "The uncompromising code formatter." description = "The uncompromising code formatter."
category = "dev" category = "dev"
optional = false optional = false
@ -12,6 +12,8 @@ mypy-extensions = ">=0.4.3"
pathspec = ">=0.9.0" pathspec = ">=0.9.0"
platformdirs = ">=2" platformdirs = ">=2"
tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""}
typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""}
typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}
[package.extras] [package.extras]
colorama = ["colorama (>=0.4.3)"] colorama = ["colorama (>=0.4.3)"]
@ -19,24 +21,37 @@ d = ["aiohttp (>=3.7.4)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"] uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "cachetools"
version = "5.3.1"
description = "Extensible memoizing collections and decorators"
category = "dev"
optional = false
python-versions = ">=3.7"
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2022.9.24" version = "2023.5.7"
description = "Python package for providing Mozilla's CA Bundle." description = "Python package for providing Mozilla's CA Bundle."
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
[[package]]
name = "chardet"
version = "5.1.0"
description = "Universal encoding detector for Python 3"
category = "dev"
optional = false
python-versions = ">=3.7"
[[package]] [[package]]
name = "charset-normalizer" name = "charset-normalizer"
version = "2.1.1" version = "3.1.0"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.6.0" python-versions = ">=3.7.0"
[package.extras]
unicode_backport = ["unicodedata2"]
[[package]] [[package]]
name = "click" name = "click"
@ -48,6 +63,7 @@ python-versions = ">=3.7"
[package.dependencies] [package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""} colorama = {version = "*", markers = "platform_system == \"Windows\""}
importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
[[package]] [[package]]
name = "colorama" name = "colorama"
@ -57,6 +73,37 @@ category = "dev"
optional = false optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
[[package]]
name = "distlib"
version = "0.3.6"
description = "Distribution utilities"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "exceptiongroup"
version = "1.1.1"
description = "Backport of PEP 654 (exception groups)"
category = "dev"
optional = false
python-versions = ">=3.7"
[package.extras]
test = ["pytest (>=6)"]
[[package]]
name = "filelock"
version = "3.12.2"
description = "A platform independent file lock."
category = "dev"
optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["furo (>=2023.5.20)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)", "sphinx (>=7.0.1)"]
testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)", "pytest (>=7.3.1)"]
[[package]] [[package]]
name = "idna" name = "idna"
version = "3.4" version = "3.4"
@ -65,27 +112,52 @@ category = "main"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
[[package]]
name = "importlib-metadata"
version = "6.7.0"
description = "Read metadata from Python packages"
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
zipp = ">=0.5"
[package.extras]
docs = ["sphinx (>=3.5)", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "furo", "sphinx-lint", "jaraco.tidelift (>=1.4)"]
perf = ["ipython"]
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-ruff", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"]
[[package]]
name = "iniconfig"
version = "2.0.0"
description = "brain-dead simple config-ini parsing"
category = "dev"
optional = false
python-versions = ">=3.7"
[[package]] [[package]]
name = "isort" name = "isort"
version = "5.10.1" version = "5.11.5"
description = "A Python utility / library to sort Python imports." description = "A Python utility / library to sort Python imports."
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.6.1,<4.0" python-versions = ">=3.7.0"
[package.extras] [package.extras]
pipfile_deprecated_finder = ["pipreqs", "requirementslib"] pipfile-deprecated-finder = ["pipreqs", "requirementslib", "pip-shims (>=0.5.2)"]
requirements_deprecated_finder = ["pipreqs", "pip-api"] requirements-deprecated-finder = ["pipreqs", "pip-api"]
colors = ["colorama (>=0.4.3,<0.5.0)"] colors = ["colorama (>=0.4.3,<0.5.0)"]
plugins = ["setuptools"] plugins = ["setuptools"]
[[package]] [[package]]
name = "mypy-extensions" name = "mypy-extensions"
version = "0.4.3" version = "1.0.0"
description = "Experimental type system extensions for programs checked with the mypy typechecker." description = "Type system extensions for programs checked with the mypy type checker."
category = "dev" category = "dev"
optional = false optional = false
python-versions = "*" python-versions = ">=3.5"
[[package]] [[package]]
name = "observable" name = "observable"
@ -95,9 +167,17 @@ category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
[[package]]
name = "packaging"
version = "23.1"
description = "Core utilities for Python packages"
category = "dev"
optional = false
python-versions = ">=3.7"
[[package]] [[package]]
name = "pathspec" name = "pathspec"
version = "0.10.2" version = "0.11.1"
description = "Utility library for gitignore style pattern matching of file paths." description = "Utility library for gitignore style pattern matching of file paths."
category = "dev" category = "dev"
optional = false optional = false
@ -105,15 +185,69 @@ python-versions = ">=3.7"
[[package]] [[package]]
name = "platformdirs" name = "platformdirs"
version = "2.5.4" version = "3.8.0"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
[package.dependencies]
typing-extensions = {version = ">=4.6.3", markers = "python_version < \"3.8\""}
[package.extras] [package.extras]
docs = ["furo (>=2022.9.29)", "proselint (>=0.13)", "sphinx-autodoc-typehints (>=1.19.4)", "sphinx (>=5.3)"] docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)", "sphinx (>=7.0.1)"]
test = ["appdirs (==1.4.4)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest (>=7.2)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest (>=7.3.1)"]
[[package]]
name = "pluggy"
version = "1.2.0"
description = "plugin and hook calling mechanisms for python"
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "pyproject-api"
version = "1.5.2"
description = "API to interact with the python pyproject.toml based projects"
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
packaging = ">=23.1"
tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
[package.extras]
docs = ["furo (>=2023.5.20)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)", "sphinx (>=7.0.1)"]
testing = ["covdefaults (>=2.3)", "importlib-metadata (>=6.6)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest (>=7.3.1)", "setuptools (>=67.8)", "wheel (>=0.40)"]
[[package]]
name = "pytest"
version = "7.4.0"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
colorama = {version = "*", markers = "sys_platform == \"win32\""}
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<2.0"
tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
[package.extras]
testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
[[package]] [[package]]
name = "python-engineio" name = "python-engineio"
@ -150,17 +284,17 @@ client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"]
[[package]] [[package]]
name = "requests" name = "requests"
version = "2.28.1" version = "2.31.0"
description = "Python HTTP for Humans." description = "Python HTTP for Humans."
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7, <4" python-versions = ">=3.7"
[package.dependencies] [package.dependencies]
certifi = ">=2017.4.17" certifi = ">=2017.4.17"
charset-normalizer = ">=2,<3" charset-normalizer = ">=2,<4"
idna = ">=2.5,<4" idna = ">=2.5,<4"
urllib3 = ">=1.21.1,<1.27" urllib3 = ">=1.21.1,<3"
[package.extras] [package.extras]
socks = ["PySocks (>=1.5.6,!=1.5.7)"] socks = ["PySocks (>=1.5.6,!=1.5.7)"]
@ -182,22 +316,83 @@ category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
[[package]]
name = "tox"
version = "4.6.3"
description = "tox is a generic virtualenv management and test command line tool"
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
cachetools = ">=5.3.1"
chardet = ">=5.1"
colorama = ">=0.4.6"
filelock = ">=3.12.2"
importlib-metadata = {version = ">=6.6", markers = "python_version < \"3.8\""}
packaging = ">=23.1"
platformdirs = ">=3.5.3"
pluggy = ">=1"
pyproject-api = ">=1.5.2"
tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
typing-extensions = {version = ">=4.6.3", markers = "python_version < \"3.8\""}
virtualenv = ">=20.23.1"
[package.extras]
docs = ["furo (>=2023.5.20)", "sphinx-argparse-cli (>=1.11.1)", "sphinx-autodoc-typehints (>=1.23.2,!=1.23.4)", "sphinx-copybutton (>=0.5.2)", "sphinx-inline-tabs (>=2023.4.21)", "sphinx (>=7.0.1)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
testing = ["build[virtualenv] (>=0.10)", "covdefaults (>=2.3)", "detect-test-pollution (>=1.1.1)", "devpi-process (>=0.3.1)", "diff-cover (>=7.6)", "distlib (>=0.3.6)", "flaky (>=3.7)", "hatch-vcs (>=0.3)", "hatchling (>=1.17.1)", "psutil (>=5.9.5)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-xdist (>=3.3.1)", "pytest (>=7.3.2)", "re-assert (>=1.1)", "time-machine (>=2.10)", "wheel (>=0.40)"]
[[package]]
name = "typed-ast"
version = "1.5.4"
description = "a fork of Python 2 and 3 ast modules with type comment support"
category = "dev"
optional = false
python-versions = ">=3.6"
[[package]]
name = "typing-extensions"
version = "4.6.3"
description = "Backported and Experimental Type Hints for Python 3.7+"
category = "dev"
optional = false
python-versions = ">=3.7"
[[package]] [[package]]
name = "urllib3" name = "urllib3"
version = "1.26.12" version = "2.0.3"
description = "HTTP library with thread-safe connection pooling, file post, and more." description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "main" category = "main"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" python-versions = ">=3.7"
[package.extras] [package.extras]
brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "urllib3-secure-extra", "ipaddress"] secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"]
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "virtualenv"
version = "20.23.1"
description = "Virtual Python Environment builder"
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
distlib = ">=0.3.6,<1"
filelock = ">=3.12,<4"
importlib-metadata = {version = ">=6.6", markers = "python_version < \"3.8\""}
platformdirs = ">=3.5.1,<4"
[package.extras]
docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx-argparse (>=0.4)", "sphinx (>=7.0.1)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
test = ["covdefaults (>=2.3)", "coverage-enable-subprocess (>=1)", "coverage (>=7.2.7)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest-env (>=0.8.1)", "pytest-freezer (>=0.4.6)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "pytest (>=7.3.1)", "setuptools (>=67.8)", "time-machine (>=2.9)"]
[[package]] [[package]]
name = "websocket-client" name = "websocket-client"
version = "1.4.2" version = "1.6.1"
description = "WebSocket client for Python with low level API options" description = "WebSocket client for Python with low level API options"
category = "main" category = "main"
optional = false optional = false
@ -208,27 +403,55 @@ docs = ["Sphinx (>=3.4)", "sphinx-rtd-theme (>=0.5)"]
optional = ["python-socks", "wsaccel"] optional = ["python-socks", "wsaccel"]
test = ["websockets"] test = ["websockets"]
[[package]]
name = "zipp"
version = "3.15.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
category = "dev"
optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["sphinx (>=3.5)", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "furo", "sphinx-lint", "jaraco.tidelift (>=1.4)"]
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "flake8 (<5)", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "jaraco.functools", "more-itertools", "big-o", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "pytest-flake8"]
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.10" python-versions = "^3.7"
content-hash = "fd2bc058c27869f25132aa1baf2599c482e6f926ee75743cfe7ac1fa1042d4bd" content-hash = "9d75c174c1d055013bab4669a426bbbcd1d5fe31243e1569c8aff0524c143496"
[metadata.files] [metadata.files]
black = [] black = []
cachetools = []
certifi = [] certifi = []
chardet = []
charset-normalizer = [] charset-normalizer = []
click = [] click = []
colorama = [] colorama = []
distlib = []
exceptiongroup = []
filelock = []
idna = [] idna = []
importlib-metadata = []
iniconfig = []
isort = [] isort = []
mypy-extensions = [] mypy-extensions = []
observable = [] observable = []
packaging = []
pathspec = [] pathspec = []
platformdirs = [] platformdirs = []
pluggy = []
pyproject-api = []
pytest = []
python-engineio = [] python-engineio = []
python-socketio = [] python-socketio = []
requests = [] requests = []
six = [] six = []
tomli = [] tomli = []
tox = []
typed-ast = []
typing-extensions = []
urllib3 = [] urllib3 = []
virtualenv = []
websocket-client = [] websocket-client = []
zipp = []

View File

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "streamlabsio" name = "streamlabsio"
version = "0.1.3" version = "1.0.1"
description = "Get real time Twitch/Youtube events through Streamlabs SocketIO API" description = "Get real time Twitch/Youtube events through Streamlabs SocketIO API"
authors = ["onyx-and-iris <code@onyxandiris.online>"] authors = ["onyx-and-iris <code@onyxandiris.online>"]
license = "MIT" license = "MIT"
@ -8,16 +8,34 @@ readme = "README.md"
repository = "https://github.com/onyx-and-iris/streamlabs-socketio-py" repository = "https://github.com/onyx-and-iris/streamlabs-socketio-py"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.10" python = "^3.8"
tomli = { version = "^2.0.1", python = "<3.11" } tomli = { version = "^2.0.1", python = "<3.11" }
python-engineio = "3.14.2" python-engineio = "3.14.2"
python-socketio = {version = "4.6.0", extras = ["client"]} python-socketio = { version = "4.6.0", extras = ["client"] }
observable = "^1.0.3" observable = "^1.0.3"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
black = "^22.10.0" black = "^22.10.0"
isort = "^5.10.1" isort = "^5.10.1"
tox = "^4.6.3"
pytest = "^7.4.0"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
[tool.poetry.scripts]
debug = "scripts:ex_debug"
[tool.tox]
legacy_tox_ini = """
[tox]
envlist = py38,py39,py310,py311
[testenv]
allowlist_externals = poetry
commands =
poetry install -v
poetry run pytest tests/
"""

7
scripts.py Normal file
View File

@ -0,0 +1,7 @@
import subprocess
from pathlib import Path
def ex_debug():
path = Path.cwd() / "examples" / "debug" / "."
subprocess.run(["py", str(path)])

View File

@ -1,22 +1,21 @@
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Optional
import socketio import socketio
from observable import Observable from observable import Observable
try: from .error import SteamlabsSIOConnectionError
import tomllib
except ModuleNotFoundError:
import tomli as tomllib
from .models import as_dataclass from .models import as_dataclass
logger = logging.getLogger(__name__)
class Client: class Client:
logger = logging.getLogger("socketio.client") def __init__(self, token=None, raw=False):
self.logger = logger.getChild(self.__class__.__name__)
def __init__(self, token=None):
self.token = token or self._token_from_toml() self.token = token or self._token_from_toml()
self._raw = raw
self.sio = socketio.Client() self.sio = socketio.Client()
self.sio.on("connect", self.connect_handler) self.sio.on("connect", self.connect_handler)
self.sio.on("event", self.event_handler) self.sio.on("event", self.event_handler)
@ -27,15 +26,43 @@ class Client:
self.youtube = ("follow", "subscription", "superchat") self.youtube = ("follow", "subscription", "superchat")
def __enter__(self): def __enter__(self):
self.sio.connect(f"https://sockets.streamlabs.com?token={self.token}") try:
self.sio.connect(f"https://sockets.streamlabs.com?token={self.token}")
except socketio.exceptions.ConnectionError as e:
self.logger.exception(f"{type(e).__name__}: {e}")
raise SteamlabsSIOConnectionError(
"no connection could be established to the Streamlabs SIO server"
) from e
return self return self
def _token_from_toml(self) -> str: def _token_from_toml(self) -> str:
filepath = Path.cwd() / "config.toml" try:
with open(filepath, "rb") as f: import tomllib
conn = tomllib.load(f) except ModuleNotFoundError:
assert "token" in conn.get("streamlabs") import tomli as tomllib
return conn["streamlabs"].get("token")
def get_filepath() -> Optional[Path]:
filepaths = (
Path.cwd() / "config.toml",
Path.home() / ".config" / "streamlabsio" / "config.toml",
)
for filepath in filepaths:
if filepath.exists():
return filepath
try:
filepath = get_filepath()
if not filepath:
raise FileNotFoundError("config.toml was not found")
with open(filepath, "rb") as f:
conn = tomllib.load(f)
assert "token" in conn.get(
"streamlabs"
), "token not found in config.toml"
return conn["streamlabs"].get("token")
except (FileNotFoundError, tomllib.TOMLDecodeError) as e:
self.logger.error(f"{type(e).__name__}: {e}")
raise
def connect_handler(self): def connect_handler(self):
self.logger.info("Connected to Streamlabs Socket API") self.logger.info("Connected to Streamlabs Socket API")
@ -47,8 +74,11 @@ class Client:
self.obs.trigger( self.obs.trigger(
data["for"], data["for"],
data["type"], data["type"],
as_dataclass(data["type"], *data["message"]), *data["message"]
if self._raw
else as_dataclass(data["type"], *data["message"]),
) )
self.logger.debug(data)
def disconnect_handler(self): def disconnect_handler(self):
self.logger.info("Disconnected from Streamlabs Socket API") self.logger.info("Disconnected from Streamlabs Socket API")

2
streamlabsio/error.py Normal file
View File

@ -0,0 +1,2 @@
class SteamlabsSIOConnectionError(Exception):
"""Exception raised when connection errors occur"""