"""
Configuration class for the GPP client.
"""
__all__ = ["GPPConfig"]
import logging
from pathlib import Path
import toml
import typer
from pydantic import ValidationError
from toml import TomlDecodeError
from gpp_client.config.defaults import GPPDefaults
from gpp_client.config.environment import GPPEnvironment
from gpp_client.config.models import ConfigFile
from gpp_client.exceptions import GPPClientError, GPPValidationError
logger = logging.getLogger(__name__)
[docs]
class GPPConfig:
"""
Manage loading, saving, and updating GPP client configuration.
This class manages user tokens and environment settings, stored in a TOML file.
"""
def __init__(self) -> None:
self.path = self._get_app_dir()
self.directory = self.path.parent
self._data = self._load()
@staticmethod
def _get_app_dir() -> Path:
"""
Get the path to the configuration file using ``typer.get_app_dir()``.
Returns
-------
Path
Full path to the configuration file.
"""
return (
Path(typer.get_app_dir(GPPDefaults.app_name, force_posix=True))
/ GPPDefaults.config_filename
)
def _load(self) -> ConfigFile:
"""
Load configuration data from disk.
Returns
-------
ConfigFile
The validated configuration model.
Raises
------
GPPValidationError
If the TOML content is invalid.
"""
if self.exists():
try:
data = toml.load(self.path)
return ConfigFile(**data)
except (ValidationError, TomlDecodeError) as exc:
logger.error("Invalid configuration file at %s: %s", self.path, exc)
raise GPPValidationError(f"{exc}") from None
return ConfigFile()
[docs]
def exists(self) -> bool:
"""
Whether the configuration file exists.
Returns
-------
bool
``True`` if the config file exists, ``False`` otherwise.
"""
return self.path.exists()
[docs]
def save(self) -> None:
"""
Save the current configuration data to disk.
"""
logger.debug("Saving config to %s", self.path)
self.path.parent.mkdir(parents=True, exist_ok=True)
self.path.write_text(toml.dumps(self.to_dict()))
self._data = self._load()
@property
def active_env(self) -> GPPEnvironment:
"""
Return the currently active environment.
Returns
-------
GPPEnvironment
The active GPP environment.
"""
return self._data.env
@property
def active_token(self) -> str | None:
"""
Return the token for the currently active environment.
Returns
-------
str | None
The token for the active environment, or ``None`` if not set.
"""
return self.get_token_for(self.active_env)
@property
def has_credentials(self) -> bool:
"""
Checks whether credentials are set for the current environment.
Returns
-------
bool
``True`` if a token is set for the active environment, ``False`` otherwise.
"""
token = self.active_token
return bool(token and token.strip())
[docs]
def get_token_for(self, env: GPPEnvironment | str) -> str | None:
"""
Return the token for a specific environment.
Parameters
----------
env : GPPEnvironment | str
The environment to get the token for.
Returns
-------
str | None
The token for the specified environment, or ``None`` if not set.
"""
env = GPPEnvironment(env)
return getattr(self._data.tokens, env.value)
[docs]
def get_all_envs_with_tokens(self) -> dict[GPPEnvironment, str]:
"""
Return all environments with non-empty tokens.
Returns
-------
dict[GPPEnvironment, str]
A dictionary mapping environment names to their non-empty tokens.
"""
return {
GPPEnvironment(key): token
for key, token in self._data.tokens.model_dump().items()
if token and token.strip()
}
[docs]
def set_token(
self, env: GPPEnvironment | str, token: str, save: bool = False
) -> None:
"""
Store a token for the given environment without activating it.
Parameters
----------
env : GPPEnvironment | str
The environment to store the token for.
token : str
The bearer token.
save : bool, default=False
Whether to save the configuration to disk immediately.
Raises
------
GPPValidationError
If the provided token is empty or whitespace.
"""
if not token or not token.strip():
raise GPPValidationError(
"Token cannot be empty or whitespace. Use 'clear_token()' to remove it."
)
logger.debug("Setting token for %s", env)
env = GPPEnvironment(env)
setattr(self._data.tokens, env.value, token)
if save:
self.save()
[docs]
def clear_token(self, env: GPPEnvironment | str, save: bool = False) -> None:
"""
Clear the token for the given environment.
Parameters
----------
env : GPPEnvironment | str
The environment to clear the token for.
save : bool, default=False
Whether to save the configuration to disk immediately.
"""
logger.debug("Clearing token for %s", env)
env = GPPEnvironment(env)
setattr(self._data.tokens, env.value, None)
if save:
self.save()
[docs]
def clear_tokens(self, save: bool = False) -> None:
"""
Clear all stored tokens for all environments.
Parameters
----------
save : bool, default=False
Whether to save the configuration to disk immediately.
"""
logger.warning("Clearing all stored tokens")
for env in GPPEnvironment:
setattr(self._data.tokens, env.value, None)
if save:
self.save()
[docs]
def activate(self, env: GPPEnvironment | str, save: bool = False) -> None:
"""
Activate the given environment.
Parameters
----------
env : GPPEnvironment | str
The environment to activate.
save : bool, default=False
Whether to save the configuration to disk immediately.
"""
logger.info("Activating environment %s", env)
env = GPPEnvironment(env)
self._data.env = env
if save:
self.save()
[docs]
def set_credentials(
self,
env: GPPEnvironment | str,
token: str,
activate: bool = True,
save: bool = False,
) -> None:
"""
Set the token for a given environment and optionally activate it. By default, the environment will be activated.
Parameters
----------
env : GPPEnvironment | str
The environment to store the token for.
token : str
The bearer token.
activate : bool, default=True
Whether to activate the given environment.
save : bool, default=False
Whether to save the configuration to disk immediately.
Raises
------
GPPValidationError
If the provided token is empty or whitespace.
"""
logger.info("Setting credentials for %s", env)
self.set_token(env, token, save=False)
if activate:
self.activate(env, save=save)
elif save:
self.save()
[docs]
def disable_env_vars(self, save: bool = False) -> None:
"""
Disable the use of environment variables for configuration.
Parameters
----------
save : bool, default=False
Whether to save the configuration to disk immediately.
"""
logger.debug("Disabling environment variables for configuration")
self._data.disable_env_vars = True
if save:
self.save()
[docs]
def enable_env_vars(self, save: bool = False) -> None:
"""
Enable the use of environment variables for configuration.
Parameters
----------
save : bool, default=False
Whether to save the configuration to disk immediately.
"""
logger.debug("Enabling environment variables for configuration")
self._data.disable_env_vars = False
if save:
self.save()
[docs]
def use_env_vars(self) -> bool:
"""
Whether to use environment variables based on config.
Returns
-------
bool
``True`` if environment variables should be used, ``False`` otherwise.
"""
return not self._data.disable_env_vars
[docs]
@staticmethod
def create_default_config_file() -> None:
"""
Create an empty configuration file with placeholder tokens.
"""
logger.debug("Creating default config file at %s", GPPConfig._get_app_dir())
default = ConfigFile()
path = GPPConfig._get_app_dir()
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(toml.dumps(default.model_dump()))
logger.info("Default config file created at %s", path)
[docs]
def to_dict(self) -> dict:
"""
Return the configuration as a dictionary.
Returns
-------
dict
The configuration data as a dictionary.
"""
try:
return self._data.model_dump()
except Exception as exc:
logger.error("Failed to convert config to dict: %s", exc)
raise GPPClientError(f"Failed to convert config to dict: {exc}") from None
[docs]
def to_json(self) -> str:
"""
Return the configuration as a JSON string.
Returns
-------
str
The configuration data as a JSON string.
"""
try:
return self._data.model_dump_json()
except Exception as exc:
logger.error("Failed to convert config to JSON: %s", exc)
raise GPPClientError(f"Failed to convert config to JSON: {exc}") from None
[docs]
def to_toml(self) -> str:
"""
Return the configuration as a TOML string.
Returns
-------
str
The configuration data as a TOML string.
"""
try:
return toml.dumps(self.to_dict())
except Exception as exc:
logger.error("Failed to convert config to TOML: %s", exc)
raise GPPClientError(f"Failed to convert config to TOML: {exc}") from None