Source code for gpp_client.settings

"""
Runtime settings for the installed GPP client package.
"""

from pathlib import Path

import typer
from pydantic import Field, SecretStr
from pydantic_settings import (
    BaseSettings,
    PydanticBaseSettingsSource,
    SettingsConfigDict,
    TomlConfigSettingsSource,
)

from gpp_client.constants import APP_NAME, CONFIG_FILE_NAME
from gpp_client.environment import GPPEnvironment
from gpp_client.exceptions import GPPAuthError, GPPClientError


[docs] class GPPSettings(BaseSettings): """ Effective runtime settings for the installed GPP client package. Notes ----- Supported environment variables: - ``GPP_TOKEN`` - ``GPP_DEVELOPMENT_TOKEN`` - ``GPP_DEBUG`` Token resolution behavior: - Production package uses ``token``. - Development package uses ``development_token``. """ model_config = SettingsConfigDict( env_prefix="GPP_", env_file=".env", extra="ignore", ) token: SecretStr | None = Field( default=None, description="GPP API token for the production environment.", ) development_token: SecretStr | None = Field( default=None, description="GPP API token for the development environment.", ) debug: bool = Field( default=False, description="Whether to enable debug logging for the client." ) environment_override: GPPEnvironment | None = Field( default=None, exclude=True, description="Explicit environment override for tooling and codegen.", ) @property def resolved_token(self) -> str: """ Return the token appropriate for the installed package environment. Returns ------- str Resolved API token. Raises ------ GPPAuthError If no valid token is available for the active environment. """ return _resolve_token( environment=self.environment, token=self.token, development_token=self.development_token, ) @property def environment(self) -> GPPEnvironment: """ Determine the effective package environment. Returns ------- GPPEnvironment Effective package environment, either from the override or the generated constant. """ if self.environment_override is not None: return self.environment_override return _get_packaged_environment()
[docs] @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> tuple[PydanticBaseSettingsSource, ...]: """ Customise the settings sources to ensure the expected precedence order. Parameters ---------- settings_cls : type[BaseSettings] The settings class being constructed. init_settings : PydanticBaseSettingsSource Source for initialization parameters. env_settings : PydanticBaseSettingsSource Source for environment variables. dotenv_settings : PydanticBaseSettingsSource Source for dotenv file. file_secret_settings : PydanticBaseSettingsSource Source for file secrets. Returns ------- tuple[PydanticBaseSettingsSource, ...] Ordered tuple of settings sources. Notes ----- The default precedence order is: 1. Initialization parameters 2. Environment variables 3. Dotenv file 4. App TOML file 5. File secrets """ config_path = get_config_path() toml_sources: tuple[PydanticBaseSettingsSource, ...] = () if config_path.is_file(): toml_sources = ( TomlConfigSettingsSource(settings_cls, toml_file=config_path.resolve()), ) return ( init_settings, env_settings, dotenv_settings, *toml_sources, file_secret_settings, )
# @model_validator(mode="after") # def validate_tokens(self) -> Self: # """ # Ensure a valid token exists for the active environment. # Returns # ------- # Self # Validated settings instance. # """ # try: # _ = self.resolved_token # except GPPAuthError as exc: # raise ValueError(str(exc)) from exc # return self def get_config_path() -> Path: """ Get the path to the configuration file. Returns ------- Path Path to the configuration file. """ return Path(typer.get_app_dir(APP_NAME)) / CONFIG_FILE_NAME def _unwrap(secret: SecretStr | None) -> str | None: """ Helper function to unwrap a ``SecretStr`` or return ``None`` if not provided. """ return secret.get_secret_value() if secret else None def _get_packaged_environment() -> GPPEnvironment: """ Helper function to get the package environment from the generated constant. """ try: from gpp_client.generated.package_environment import PACKAGE_ENVIRONMENT except ModuleNotFoundError as exc: raise GPPClientError( "Generated package environment is unavailable. Pass 'environment_override'" " to GPPSettings or ensure the client is properly installed." ) from exc return GPPEnvironment(PACKAGE_ENVIRONMENT) def _resolve_token( *, environment: GPPEnvironment, token: SecretStr | None, development_token: SecretStr | None, ) -> str: """ Resolve the correct token for the given environment. """ resolved_token = _unwrap(token) resolved_development_token = _unwrap(development_token) if environment is GPPEnvironment.DEVELOPMENT: if resolved_development_token: return resolved_development_token raise GPPAuthError( "A token is required for the development environment. " "Set 'GPP_DEVELOPMENT_TOKEN'." ) if resolved_token: return resolved_token raise GPPAuthError( "A token is required for the production environment. Set 'GPP_TOKEN'." )