diff --git a/obsws_cli/app.py b/obsws_cli/app.py index d623ba4..71c4f06 100644 --- a/obsws_cli/app.py +++ b/obsws_cli/app.py @@ -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) diff --git a/obsws_cli/config.py b/obsws_cli/config.py deleted file mode 100644 index 7973b48..0000000 --- a/obsws_cli/config.py +++ /dev/null @@ -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] diff --git a/obsws_cli/envconfig.py b/obsws_cli/envconfig.py new file mode 100644 index 0000000..0be84f4 --- /dev/null +++ b/obsws_cli/envconfig.py @@ -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) diff --git a/tests/conftest.py b/tests/conftest.py index b4f3f4f..07a4628 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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()