rename config module to envconfig

envconfig:
- add method for normalising the keys
- add has_key method
- update env var prefix to OBSWS_CLI_

update tests to reflect changes
This commit is contained in:
onyx-and-iris 2026-01-25 16:19:12 +00:00
parent 1fc0bef237
commit 3a8d4ef0f0
4 changed files with 164 additions and 98 deletions

View File

@ -10,7 +10,7 @@ import typer
from obsws_cli.__about__ import __version__ as version
from . import commands, config, console, styles
from . import commands, console, envconfig, styles
from .alias import RootTyperAliasGroup
app = typer.Typer(cls=RootTyperAliasGroup)
@ -54,62 +54,62 @@ def main(
typer.Option(
'--host',
'-H',
envvar='OBS_HOST',
envvar='OBSWS_CLI_HOST',
help='WebSocket host',
show_default='localhost',
),
] = config.get('host'),
] = envconfig.get('host'),
port: Annotated[
int,
typer.Option(
'--port',
'-P',
envvar='OBS_PORT',
envvar='OBSWS_CLI_PORT',
help='WebSocket port',
show_default=4455,
),
] = config.get('port'),
] = envconfig.get('port'),
password: Annotated[
str,
typer.Option(
'--password',
'-p',
envvar='OBS_PASSWORD',
envvar='OBSWS_CLI_PASSWORD',
help='WebSocket password',
show_default=False,
),
] = config.get('password'),
] = envconfig.get('password'),
timeout: Annotated[
int,
typer.Option(
'--timeout',
'-T',
envvar='OBS_TIMEOUT',
envvar='OBSWS_CLI_TIMEOUT',
help='WebSocket timeout',
show_default=5,
),
] = config.get('timeout'),
] = envconfig.get('timeout'),
style: Annotated[
str,
typer.Option(
'--style',
'-s',
envvar='OBS_STYLE',
envvar='OBSWS_CLI_STYLE',
help='Set the style for the CLI output',
show_default='disabled',
callback=validate_style,
),
] = config.get('style'),
] = envconfig.get('style'),
no_border: Annotated[
bool,
typer.Option(
'--no-border',
'-b',
envvar='OBS_STYLE_NO_BORDER',
envvar='OBSWS_CLI_STYLE_NO_BORDER',
help='Disable table border styling in the CLI output',
show_default=False,
),
] = config.get('style_no_border'),
] = envconfig.get('style_no_border'),
version: Annotated[
bool,
typer.Option(
@ -126,14 +126,14 @@ def main(
typer.Option(
'--debug',
'-d',
envvar='OBS_DEBUG',
envvar='OBSWS_CLI_DEBUG',
is_eager=True,
help='Enable debug logging',
show_default=False,
callback=setup_logging,
hidden=True,
),
] = config.get('debug'),
] = envconfig.get('debug'),
):
"""obsws_cli is a command line interface for the OBS WebSocket API."""
ctx.ensure_object(dict)

View File

@ -1,80 +0,0 @@
"""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]

146
obsws_cli/envconfig.py Normal file
View File

@ -0,0 +1,146 @@
"""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

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