Compare commits

..

No commits in common. "main" and "v0.24.2" have entirely different histories.

27 changed files with 179 additions and 243 deletions

View File

@ -5,12 +5,6 @@ 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/), 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
# [0.24.6] - 2026-01-26
### Changed
- environment variables should now be prefixed with 'OBSWS_CLI_', see [Environment Variables](https://github.com/onyx-and-iris/obsws-cli?tab=readme-ov-file#environment-variables)
# [0.24.0] - 2026-01-09 # [0.24.0] - 2026-01-09
### Added ### Added

View File

@ -62,9 +62,9 @@ Store and load environment variables from:
- `user home directory / .config / obsws-cli / obsws.env` - `user home directory / .config / obsws-cli / obsws.env`
```env ```env
OBSWS_CLI_HOST=localhost OBS_HOST=localhost
OBSWS_CLI_PORT=4455 OBS_PORT=4455
OBSWS_CLI_PASSWORD=<websocket password> OBS_PASSWORD=<websocket password>
``` ```
Flags can be used to override environment variables. Flags can be used to override environment variables.
@ -96,8 +96,8 @@ obsws-cli --style="cyan" --no-border sceneitem list
Or with environment variables: Or with environment variables:
```env ```env
OBSWS_CLI_STYLE=cyan OBS_STYLE=cyan
OBSWS_CLI_STYLE_NO_BORDER=true OBS_STYLE_NO_BORDER=true
``` ```
## Root Typer ## Root Typer

View File

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2025-present onyx-and-iris <code@onyxandiris.online> # SPDX-FileCopyrightText: 2025-present onyx-and-iris <code@onyxandiris.online>
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
__version__ = '0.24.6' __version__ = '0.24.2'

View File

@ -24,8 +24,6 @@ class RootTyperAliasGroup(typer.core.TyperGroup):
cmd_name = 'hotkey' cmd_name = 'hotkey'
case 'i': case 'i':
cmd_name = 'input' cmd_name = 'input'
case 'm':
cmd_name = 'media'
case 'prf': case 'prf':
cmd_name = 'profile' cmd_name = 'profile'
case 'prj': case 'prj':
@ -42,8 +40,6 @@ class RootTyperAliasGroup(typer.core.TyperGroup):
cmd_name = 'sceneitem' cmd_name = 'sceneitem'
case 'ss': case 'ss':
cmd_name = 'screenshot' cmd_name = 'screenshot'
case 'set':
cmd_name = 'settings'
case 'st': case 'st':
cmd_name = 'stream' cmd_name = 'stream'
case 'sm': case 'sm':

View File

@ -2,7 +2,6 @@
import importlib import importlib
import logging import logging
import pkgutil
from typing import Annotated from typing import Annotated
import obsws_python as obsws import obsws_python as obsws
@ -10,15 +9,32 @@ import typer
from obsws_cli.__about__ import __version__ as version from obsws_cli.__about__ import __version__ as version
from . import commands, console, envconfig, styles from . import config, console, styles
from .alias import RootTyperAliasGroup from .alias import RootTyperAliasGroup
app = typer.Typer(cls=RootTyperAliasGroup) app = typer.Typer(cls=RootTyperAliasGroup)
for importer, modname, ispkg in pkgutil.iter_modules( for sub_typer in (
commands.__path__, commands.__name__ + '.' 'filter',
'group',
'hotkey',
'input',
'media',
'profile',
'projector',
'record',
'replaybuffer',
'settings',
'scene',
'scenecollection',
'sceneitem',
'screenshot',
'stream',
'studiomode',
'text',
'virtualcam',
): ):
subtyper = importlib.import_module(modname) module = importlib.import_module(f'.{sub_typer}', package=__package__)
app.add_typer(subtyper.app, name=modname.split('.')[-1]) app.add_typer(module.app, name=sub_typer)
def version_callback(value: bool): def version_callback(value: bool):
@ -54,62 +70,62 @@ def main(
typer.Option( typer.Option(
'--host', '--host',
'-H', '-H',
envvar='OBSWS_CLI_HOST', envvar='OBS_HOST',
help='WebSocket host', help='WebSocket host',
show_default='localhost', show_default='localhost',
), ),
] = envconfig.get('host'), ] = config.get('host'),
port: Annotated[ port: Annotated[
int, int,
typer.Option( typer.Option(
'--port', '--port',
'-P', '-P',
envvar='OBSWS_CLI_PORT', envvar='OBS_PORT',
help='WebSocket port', help='WebSocket port',
show_default=4455, show_default=4455,
), ),
] = envconfig.get('port'), ] = config.get('port'),
password: Annotated[ password: Annotated[
str, str,
typer.Option( typer.Option(
'--password', '--password',
'-p', '-p',
envvar='OBSWS_CLI_PASSWORD', envvar='OBS_PASSWORD',
help='WebSocket password', help='WebSocket password',
show_default=False, show_default=False,
), ),
] = envconfig.get('password'), ] = config.get('password'),
timeout: Annotated[ timeout: Annotated[
int, int,
typer.Option( typer.Option(
'--timeout', '--timeout',
'-T', '-T',
envvar='OBSWS_CLI_TIMEOUT', envvar='OBS_TIMEOUT',
help='WebSocket timeout', help='WebSocket timeout',
show_default=5, show_default=5,
), ),
] = envconfig.get('timeout'), ] = config.get('timeout'),
style: Annotated[ style: Annotated[
str, str,
typer.Option( typer.Option(
'--style', '--style',
'-s', '-s',
envvar='OBSWS_CLI_STYLE', envvar='OBS_STYLE',
help='Set the style for the CLI output', help='Set the style for the CLI output',
show_default='disabled', show_default='disabled',
callback=validate_style, callback=validate_style,
), ),
] = envconfig.get('style'), ] = config.get('style'),
no_border: Annotated[ no_border: Annotated[
bool, bool,
typer.Option( typer.Option(
'--no-border', '--no-border',
'-b', '-b',
envvar='OBSWS_CLI_STYLE_NO_BORDER', envvar='OBS_STYLE_NO_BORDER',
help='Disable table border styling in the CLI output', help='Disable table border styling in the CLI output',
show_default=False, show_default=False,
), ),
] = envconfig.get('style_no_border'), ] = config.get('style_no_border'),
version: Annotated[ version: Annotated[
bool, bool,
typer.Option( typer.Option(
@ -126,14 +142,14 @@ def main(
typer.Option( typer.Option(
'--debug', '--debug',
'-d', '-d',
envvar='OBSWS_CLI_DEBUG', envvar='OBS_DEBUG',
is_eager=True, is_eager=True,
help='Enable debug logging', help='Enable debug logging',
show_default=False, show_default=False,
callback=setup_logging, callback=setup_logging,
hidden=True, hidden=True,
), ),
] = envconfig.get('debug'), ] = config.get('debug'),
): ):
"""obsws_cli is a command line interface for the OBS WebSocket API.""" """obsws_cli is a command line interface for the OBS WebSocket API."""
ctx.ensure_object(dict) ctx.ensure_object(dict)

80
obsws_cli/config.py Normal file
View File

@ -0,0 +1,80 @@
"""module for settings management for obsws-cli."""
from collections import UserDict
from pathlib import Path
from dotenv import dotenv_values
ConfigValue = str | int
class Config(UserDict):
"""A class to manage config for obsws-cli.
This class extends UserDict to provide a dictionary-like interface for config.
It loads config from environment variables and .env files.
The config values are expected to be in uppercase and should start with 'OBS_'.
Example:
-------
config = Config()
host = config['OBS_HOST']
config['OBS_PORT'] = 4455
"""
PREFIX = 'OBS_'
def __init__(self, *args, **kwargs):
"""Initialize the Settings object."""
kwargs.update(
{
**dotenv_values('.env'),
**dotenv_values(Path.home() / '.config' / 'obsws-cli' / 'obsws.env'),
}
)
super().__init__(*args, **kwargs)
def __getitem__(self, key: str) -> ConfigValue:
"""Get a setting value by key."""
key = key.upper()
if not key.startswith(Config.PREFIX):
key = f'{Config.PREFIX}{key}'
return self.data[key]
def __setitem__(self, key: str, value: ConfigValue):
"""Set a setting value by key."""
key = key.upper()
if not key.startswith(Config.PREFIX):
key = f'{Config.PREFIX}{key}'
self.data[key] = value
_config = Config(
OBS_HOST='localhost',
OBS_PORT=4455,
OBS_PASSWORD='',
OBS_TIMEOUT=5,
OBS_DEBUG=False,
OBS_STYLE='disabled',
OBS_STYLE_NO_BORDER=False,
)
def get(key: str) -> ConfigValue:
"""Get a setting value by key.
Args:
----
key (str): The key of the config to retrieve.
Returns:
-------
The value of the config.
Raises:
------
KeyError: If the key does not exist in the config.
"""
return _config[key]

View File

@ -1,146 +0,0 @@
"""module for settings management for obsws-cli."""
from collections import UserDict
from pathlib import Path
from typing import Any, Union
from dotenv import dotenv_values
ConfigValue = Union[str, int, bool]
class EnvConfig(UserDict):
"""A class to manage .env config for obsws-cli.
This class extends UserDict to provide a dictionary-like interface for config.
It loads config from .env files in the following priority:
1. Local .env file
2. User config file (~/.config/obsws-cli/obsws.env)
3. Default values
Note, typer handles reading from environment variables automatically. They take precedence
over values set in this class.
The config values are expected to be in uppercase and should start with 'OBSWS_CLI_'.
Example:
-------
config = EnvConfig()
host = config['OBSWS_CLI_HOST']
config['OBSWS_CLI_PORT'] = 4455
# Or with defaults
timeout = config.get('OBSWS_CLI_TIMEOUT', 30)
# Keys will be normalised to uppercase with prefix
debug = config.get('debug', False) # Equivalent to 'OBSWS_CLI_DEBUG'
"""
PREFIX = 'OBSWS_CLI_'
def __init__(self, *args, **kwargs):
"""Initialize the Config object with hierarchical loading."""
kwargs.update(
{
**dotenv_values(Path.home() / '.config' / 'obsws-cli' / 'obsws.env'),
**dotenv_values('.env'),
}
)
super().__init__(*args, **self._convert_types(kwargs))
def _convert_types(self, config_data: dict[str, Any]) -> dict[str, ConfigValue]:
"""Convert string values to appropriate types."""
converted = {}
for key, value in config_data.items():
if isinstance(value, str):
if value.lower() in ('true', 'false'):
converted[key] = value.lower() == 'true'
elif value.isdigit():
converted[key] = int(value)
else:
converted[key] = value
else:
converted[key] = value
return converted
def __getitem__(self, key: str) -> ConfigValue:
"""Get a setting value by key."""
normalised_key = self._normalise_key(key)
try:
return self.data[normalised_key]
except KeyError as e:
raise KeyError(
f"Config key '{key}' not found. Available keys: {list(self.data.keys())}"
) from e
def __setitem__(self, key: str, value: ConfigValue):
"""Set a setting value by key."""
normalised_key = self._normalise_key(key)
self.data[normalised_key] = value
def _normalise_key(self, key: str) -> str:
"""Normalise a key to uppercase with OBS_ prefix."""
key = key.upper()
if not key.startswith(self.PREFIX):
key = f'{self.PREFIX}{key}'
return key
def get(self, key: str, default=None) -> ConfigValue:
"""Get a config value with optional default.
Args:
----
key (str): The key to retrieve
default: Default value if key is not found
Returns:
-------
The config value or default
"""
normalised_key = self._normalise_key(key)
if not self.has_key(normalised_key):
return default
return self[normalised_key]
def has_key(self, key: str) -> bool:
"""Check if a config key exists.
Args:
----
key (str): The key to check
Returns:
-------
bool: True if key exists, False otherwise
"""
normalised_key = self._normalise_key(key)
return normalised_key in self.data
_envconfig = EnvConfig(
OBSWS_CLI_HOST='localhost',
OBSWS_CLI_PORT=4455,
OBSWS_CLI_PASSWORD='',
OBSWS_CLI_TIMEOUT=5,
OBSWS_CLI_DEBUG=False,
OBSWS_CLI_STYLE='disabled',
OBSWS_CLI_STYLE_NO_BORDER=False,
)
def get(key: str) -> ConfigValue:
"""Get a setting value by key from the global config instance.
Args:
----
key (str): The key of the config to retrieve.
default: Default value if key is not found.
Returns:
-------
The value of the config or default value.
"""
return _envconfig.get(key)

View File

@ -7,8 +7,8 @@ import typer
from rich.table import Table from rich.table import Table
from rich.text import Text from rich.text import Text
from obsws_cli import console, util from . import console, util
from obsws_cli.alias import SubTyperAliasGroup from .alias import SubTyperAliasGroup
app = typer.Typer(cls=SubTyperAliasGroup) app = typer.Typer(cls=SubTyperAliasGroup)

View File

@ -6,9 +6,9 @@ import typer
from rich.table import Table from rich.table import Table
from rich.text import Text from rich.text import Text
from obsws_cli import console, util, validate from . import console, util, validate
from obsws_cli.alias import SubTyperAliasGroup from .alias import SubTyperAliasGroup
from obsws_cli.protocols import DataclassProtocol from .protocols import DataclassProtocol
app = typer.Typer(cls=SubTyperAliasGroup) app = typer.Typer(cls=SubTyperAliasGroup)

View File

@ -6,8 +6,8 @@ import typer
from rich.table import Table from rich.table import Table
from rich.text import Text from rich.text import Text
from obsws_cli import console from . import console
from obsws_cli.alias import SubTyperAliasGroup from .alias import SubTyperAliasGroup
app = typer.Typer(cls=SubTyperAliasGroup) app = typer.Typer(cls=SubTyperAliasGroup)

View File

@ -7,8 +7,8 @@ import typer
from rich.table import Table from rich.table import Table
from rich.text import Text from rich.text import Text
from obsws_cli import console, util, validate from . import console, util, validate
from obsws_cli.alias import SubTyperAliasGroup from .alias import SubTyperAliasGroup
app = typer.Typer(cls=SubTyperAliasGroup) app = typer.Typer(cls=SubTyperAliasGroup)

View File

@ -4,8 +4,8 @@ from typing import Annotated, Optional
import typer import typer
from obsws_cli import console, util, validate from . import console, util, validate
from obsws_cli.alias import SubTyperAliasGroup from .alias import SubTyperAliasGroup
app = typer.Typer(cls=SubTyperAliasGroup) app = typer.Typer(cls=SubTyperAliasGroup)

View File

@ -6,8 +6,8 @@ import typer
from rich.table import Table from rich.table import Table
from rich.text import Text from rich.text import Text
from obsws_cli import console, util, validate from . import console, util, validate
from obsws_cli.alias import SubTyperAliasGroup from .alias import SubTyperAliasGroup
app = typer.Typer(cls=SubTyperAliasGroup) app = typer.Typer(cls=SubTyperAliasGroup)

View File

@ -6,8 +6,8 @@ import typer
from rich.table import Table from rich.table import Table
from rich.text import Text from rich.text import Text
from obsws_cli import console from . import console
from obsws_cli.alias import SubTyperAliasGroup from .alias import SubTyperAliasGroup
app = typer.Typer(cls=SubTyperAliasGroup) app = typer.Typer(cls=SubTyperAliasGroup)

View File

@ -5,8 +5,8 @@ from typing import Annotated, Optional
import typer import typer
from obsws_cli import console from . import console
from obsws_cli.alias import SubTyperAliasGroup from .alias import SubTyperAliasGroup
app = typer.Typer(cls=SubTyperAliasGroup) app = typer.Typer(cls=SubTyperAliasGroup)

View File

@ -2,8 +2,8 @@
import typer import typer
from obsws_cli import console from . import console
from obsws_cli.alias import SubTyperAliasGroup from .alias import SubTyperAliasGroup
app = typer.Typer(cls=SubTyperAliasGroup) app = typer.Typer(cls=SubTyperAliasGroup)

View File

@ -6,8 +6,8 @@ import typer
from rich.table import Table from rich.table import Table
from rich.text import Text from rich.text import Text
from obsws_cli import console, util, validate from . import console, util, validate
from obsws_cli.alias import SubTyperAliasGroup from .alias import SubTyperAliasGroup
app = typer.Typer(cls=SubTyperAliasGroup) app = typer.Typer(cls=SubTyperAliasGroup)
@ -70,14 +70,14 @@ def list_(
def current( def current(
ctx: typer.Context, ctx: typer.Context,
preview: Annotated[ preview: Annotated[
bool, bool, typer.Option(help='Get the preview scene instead of the program scene')
typer.Option(
help='Get the preview scene instead of the program scene',
callback=validate.studio_mode_enabled,
),
] = False, ] = False,
): ):
"""Get the current program scene or preview scene.""" """Get the current program scene or preview scene."""
if preview and not validate.studio_mode_enabled(ctx):
console.err.print('Studio mode is not enabled, cannot get preview scene.')
raise typer.Exit(1)
if preview: if preview:
resp = ctx.obj['obsws'].get_current_preview_scene() resp = ctx.obj['obsws'].get_current_preview_scene()
console.out.print( console.out.print(
@ -103,13 +103,14 @@ def switch(
], ],
preview: Annotated[ preview: Annotated[
bool, bool,
typer.Option( typer.Option(help='Switch to the preview scene instead of the program scene'),
help='Switch to the preview scene instead of the program scene',
callback=validate.studio_mode_enabled,
),
] = False, ] = False,
): ):
"""Switch to a scene.""" """Switch to a scene."""
if preview and not validate.studio_mode_enabled(ctx):
console.err.print('Studio mode is not enabled, cannot set the preview scene.')
raise typer.Exit(1)
if preview: if preview:
ctx.obj['obsws'].set_current_preview_scene(scene_name) ctx.obj['obsws'].set_current_preview_scene(scene_name)
console.out.print( console.out.print(

View File

@ -5,8 +5,8 @@ from typing import Annotated
import typer import typer
from rich.table import Table from rich.table import Table
from obsws_cli import console, validate from . import console, validate
from obsws_cli.alias import SubTyperAliasGroup from .alias import SubTyperAliasGroup
app = typer.Typer(cls=SubTyperAliasGroup) app = typer.Typer(cls=SubTyperAliasGroup)

View File

@ -5,8 +5,8 @@ from typing import Annotated, Optional
import typer import typer
from rich.table import Table from rich.table import Table
from obsws_cli import console, util, validate from . import console, util, validate
from obsws_cli.alias import SubTyperAliasGroup from .alias import SubTyperAliasGroup
app = typer.Typer(cls=SubTyperAliasGroup) app = typer.Typer(cls=SubTyperAliasGroup)

View File

@ -6,8 +6,8 @@ from typing import Annotated
import obsws_python as obsws import obsws_python as obsws
import typer import typer
from obsws_cli import console from . import console
from obsws_cli.alias import SubTyperAliasGroup from .alias import SubTyperAliasGroup
app = typer.Typer(cls=SubTyperAliasGroup) app = typer.Typer(cls=SubTyperAliasGroup)

View File

@ -6,8 +6,8 @@ import typer
from rich.table import Table from rich.table import Table
from rich.text import Text from rich.text import Text
from obsws_cli import console, util from . import console, util
from obsws_cli.alias import SubTyperAliasGroup from .alias import SubTyperAliasGroup
app = typer.Typer(cls=SubTyperAliasGroup) app = typer.Typer(cls=SubTyperAliasGroup)

View File

@ -2,8 +2,8 @@
import typer import typer
from obsws_cli import console from . import console
from obsws_cli.alias import SubTyperAliasGroup from .alias import SubTyperAliasGroup
app = typer.Typer(cls=SubTyperAliasGroup) app = typer.Typer(cls=SubTyperAliasGroup)

View File

@ -2,8 +2,8 @@
import typer import typer
from obsws_cli import console from . import console
from obsws_cli.alias import SubTyperAliasGroup from .alias import SubTyperAliasGroup
app = typer.Typer(cls=SubTyperAliasGroup) app = typer.Typer(cls=SubTyperAliasGroup)

View File

@ -4,8 +4,8 @@ from typing import Annotated, Optional
import typer import typer
from obsws_cli import console, validate from . import console, validate
from obsws_cli.alias import SubTyperAliasGroup from .alias import SubTyperAliasGroup
app = typer.Typer(cls=SubTyperAliasGroup) app = typer.Typer(cls=SubTyperAliasGroup)

View File

@ -10,7 +10,7 @@ from . import console
skipped_option = typer.Option(parser=lambda _: _, hidden=True, expose_value=False) skipped_option = typer.Option(parser=lambda _: _, hidden=True, expose_value=False)
def input_in_inputs(ctx: typer.Context, input_name: str) -> str: def input_in_inputs(ctx: typer.Context, input_name: str) -> bool:
"""Ensure the given input exists in the list of inputs.""" """Ensure the given input exists in the list of inputs."""
resp = ctx.obj['obsws'].get_input_list() resp = ctx.obj['obsws'].get_input_list()
if not any(input.get('inputName') == input_name for input in resp.inputs): if not any(input.get('inputName') == input_name for input in resp.inputs):
@ -19,7 +19,7 @@ def input_in_inputs(ctx: typer.Context, input_name: str) -> str:
return input_name return input_name
def input_not_in_inputs(ctx: typer.Context, input_name: str) -> str: def input_not_in_inputs(ctx: typer.Context, input_name: str) -> bool:
"""Ensure an input does not already exist in the list of inputs.""" """Ensure an input does not already exist in the list of inputs."""
resp = ctx.obj['obsws'].get_input_list() resp = ctx.obj['obsws'].get_input_list()
if any(input.get('inputName') == input_name for input in resp.inputs): if any(input.get('inputName') == input_name for input in resp.inputs):
@ -31,7 +31,7 @@ def input_not_in_inputs(ctx: typer.Context, input_name: str) -> str:
def scene_in_scenes(ctx: typer.Context, scene_name: Optional[str]) -> str | None: def scene_in_scenes(ctx: typer.Context, scene_name: Optional[str]) -> str | None:
"""Check if a scene exists in the list of scenes.""" """Check if a scene exists in the list of scenes."""
if scene_name is None: if scene_name is None:
return return scene_name
resp = ctx.obj['obsws'].get_scene_list() resp = ctx.obj['obsws'].get_scene_list()
if not any(scene.get('sceneName') == scene_name for scene in resp.scenes): if not any(scene.get('sceneName') == scene_name for scene in resp.scenes):
@ -40,15 +40,10 @@ def scene_in_scenes(ctx: typer.Context, scene_name: Optional[str]) -> str | None
return scene_name return scene_name
def studio_mode_enabled(ctx: typer.Context, preview: bool) -> bool: def studio_mode_enabled(ctx: typer.Context) -> bool:
"""Ensure studio mode is enabled if preview option is used.""" """Check if studio mode is enabled."""
resp = ctx.obj['obsws'].get_studio_mode_enabled() resp = ctx.obj['obsws'].get_studio_mode_enabled()
if preview and not resp.studio_mode_enabled: return resp.studio_mode_enabled
console.err.print(
'Studio mode is disabled. This action requires it to be enabled.'
)
raise typer.Exit(1)
return preview
def scene_collection_in_scene_collections( def scene_collection_in_scene_collections(
@ -119,7 +114,7 @@ def kind_in_input_kinds(ctx: typer.Context, input_kind: str) -> str:
def timecode_format(ctx: typer.Context, timecode: Optional[str]) -> str | None: def timecode_format(ctx: typer.Context, timecode: Optional[str]) -> str | None:
"""Validate that a timecode is in HH:MM:SS or MM:SS format.""" """Validate that a timecode is in HH:MM:SS or MM:SS format."""
if timecode is None: if timecode is None:
return return timecode
match timecode.split(':'): match timecode.split(':'):
case [mm, ss]: case [mm, ss]:

View File

@ -2,8 +2,8 @@
import typer import typer
from obsws_cli import console from . import console
from obsws_cli.alias import SubTyperAliasGroup from .alias import SubTyperAliasGroup
app = typer.Typer(cls=SubTyperAliasGroup) app = typer.Typer(cls=SubTyperAliasGroup)

View File

@ -21,9 +21,9 @@ def pytest_sessionstart(session):
""" """
# Initialize the OBS WebSocket client # Initialize the OBS WebSocket client
session.obsws = obsws.ReqClient( session.obsws = obsws.ReqClient(
host=os.environ['OBSWS_CLI_HOST'], host=os.environ['OBS_HOST'],
port=os.environ['OBSWS_CLI_PORT'], port=os.environ['OBS_PORT'],
password=os.environ['OBSWS_CLI_PASSWORD'], password=os.environ['OBS_PASSWORD'],
timeout=5, timeout=5,
) )
resp = session.obsws.get_version() resp = session.obsws.get_version()