Skip to content

Configuration

Configuration management for QLAM Core.

Config management using generated models

This module wraps the Pydantic-generated config_models to provide convenience methods for context selection, plugin filtering, and persistence.

Example::

from qlam_core.config.config import Config

config = Config.load()
print(config.current_context.name)
config.save()

Config

Config(data: QshClientConfig, context_name: str | None = None)

Config wrapper that provides additional methods for the generated models.

:param data: Parsed config data model. :param context_name: Optional override for the active context name.

Source code in qlam_core/config/config.py
40
41
42
43
44
def __init__(self, data: QshClientConfig, context_name: str | None = None) -> None:
    self._data = data
    self._context_name = (
        context_name or data.current_context or self._get_default_context_name()
    )

auth_token property

auth_token: str | None

Get auth token from current context, with env override.

:return: Token string if available, otherwise None.

context_name property

context_name: str

Get the currently active context name.

:return: Active context name.

context_names property

context_names: list[str]

Get the names of all configured contexts.

:return: List of context name strings.

contexts property

contexts: list[Context]

Get all configured contexts.

:return: List of Context instances.

current_context property

current_context: Context

Get the currently active context.

:return: Context instance for the active context.

qpu property

qpu: str

Get QPU from current context, with env override.

:return: QPU identifier string.

timeout property

timeout: float

Get request timeout from current context.

:return: Timeout in seconds (defaults to 30.0).

get_auth_providers

get_auth_providers() -> List | None

Get list of auth providers for the current context.

:return: List of provider configs or None.

Source code in qlam_core/config/config.py
295
296
297
298
299
300
def get_auth_providers(self) -> List | None:
    """Get list of auth providers for the current context.

    :return: List of provider configs or None.
    """
    return self.current_context.auth_providers

get_config_path classmethod

get_config_path() -> Path

Get the path to the config file.

:return: Absolute path to config JSON file.

Source code in qlam_core/config/config.py
158
159
160
161
162
163
164
@classmethod
def get_config_path(cls) -> Path:
    """Get the path to the config file.

    :return: Absolute path to config JSON file.
    """
    return _default_paths.get_config_file()

get_enabled_plugins

get_enabled_plugins(all_plugin_names: set[str]) -> set[str]

Get the set of plugin names that should be enabled.

:param all_plugin_names: All discovered plugin names. :return: Enabled plugin names after filters.

Source code in qlam_core/config/config.py
276
277
278
279
280
281
282
def get_enabled_plugins(self, all_plugin_names: set[str]) -> set[str]:
    """Get the set of plugin names that should be enabled.

    :param all_plugin_names: All discovered plugin names.
    :return: Enabled plugin names after filters.
    """
    return {name for name in all_plugin_names if self.should_load_plugin(name)}

get_plugin_config

get_plugin_config(plugin_name: str) -> PluginConfig | None

Get configuration for a specific plugin.

:param plugin_name: Plugin name key. :return: PluginConfig or None if not configured.

Source code in qlam_core/config/config.py
284
285
286
287
288
289
290
291
292
293
def get_plugin_config(self, plugin_name: str) -> PluginConfig | None:
    """Get configuration for a specific plugin.

    :param plugin_name: Plugin name key.
    :return: `PluginConfig` or None if not configured.
    """
    context = self.current_context
    if not context.plugins:
        return None
    return context.plugins.get(plugin_name)

load classmethod

load(context_name: str | None = None) -> Config

Load config from config file with optional context selection.

:param context_name: Optional context name to activate. :return: Loaded Config wrapper instance.

Source code in qlam_core/config/config.py
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
@classmethod
def load(cls, context_name: str | None = None) -> Config:
    """Load config from config file with optional context selection.

    :param context_name: Optional context name to activate.
    :return: Loaded `Config` wrapper instance.
    """
    # Ensure the config directory exists
    _default_paths.ensure_config_dir()
    config_path = cls.get_config_path()
    logger = logging.getLogger(__name__)

    # Load from file if it exists
    if config_path.exists():
        try:
            # Check and fix file permissions if needed
            current_perms = config_path.stat().st_mode & 0o777
            if current_perms != 0o600:
                try:
                    config_path.chmod(0o600)
                    logger.debug(f"Fixed permissions on {config_path}")
                except PermissionError:
                    pass  # May not have permission to change, continue anyway

            with open(config_path, "r") as f:
                data_dict = json.load(f)
            data = QshClientConfig.model_validate(data_dict)
            logger.debug(f"Loaded config from {config_path}")
        except (OSError, TypeError, ValueError) as e:
            logger.debug("Failed to load config from %s", config_path, exc_info=True)
            logger.warning(f"Failed to load config from {config_path}: {e}")
            data = cls._create_default_config()
    else:
        # Fallback: try loading from old settings.json location
        legacy_settings_path = _default_paths.get_config_dir() / "settings.json"
        if legacy_settings_path.exists():
            try:
                logger.warning(
                    f"Config file {config_path} not found. "
                    f"Loading from legacy settings.json. "
                    f"Please migrate to config.json by running: "
                    f"mv {legacy_settings_path} {config_path}"
                )

                with open(legacy_settings_path, "r") as f:
                    data_dict = json.load(f)
                data = QshClientConfig.model_validate(data_dict)
                logger.debug(f"Loaded config from legacy {legacy_settings_path}")
            except (OSError, TypeError, ValueError) as e:
                logger.debug(
                    "Failed to load legacy settings from %s", legacy_settings_path, exc_info=True
                )
                logger.warning(
                    f"Failed to load legacy settings from {legacy_settings_path}: {e}"
                )
                data = cls._create_default_config()
        else:
            logger.debug(f"Config file {config_path} not found, using defaults")
            data = cls._create_default_config()

    return cls(data, context_name)

save

save() -> None

Save config to config file with proper permissions.

:return: None

Source code in qlam_core/config/config.py
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
def save(self) -> None:
    """Save config to config file with proper permissions.

    :return: None
    """
    config_path = self.get_config_path()

    try:
        # Update current_context in the data
        self._data.current_context = self._context_name

        # Write to temporary file first
        import os
        import tempfile

        tmp_fd, tmp_path = tempfile.mkstemp(dir=str(config_path.parent))
        try:
            with os.fdopen(tmp_fd, "w") as f:
                # Use JSON mode so pydantic types (e.g., AnyUrl) are serialized safely.
                json.dump(self._data.model_dump(mode="json"), f, indent=2)
                f.flush()
                os.fsync(f.fileno())

            # Set permissions before moving into place
            os.chmod(tmp_path, 0o600)

            # Atomically replace the file
            os.replace(tmp_path, config_path)

        finally:
            # Clean up temp file if it still exists
            try:
                if os.path.exists(tmp_path):
                    os.remove(tmp_path)
            except OSError:
                pass

        logger = logging.getLogger(__name__)
        logger.debug(f"Saved config to {config_path}")
    except (OSError, TypeError, ValueError, OverflowError):
        logger = logging.getLogger(__name__)
        logger.debug("Failed to save config to %s", config_path, exc_info=True)
        raise

set_context

set_context(name: str) -> None

Set the active context name (call save() to persist).

:param name: Context name to activate.

Source code in qlam_core/config/config.py
151
152
153
154
155
156
def set_context(self, name: str) -> None:
    """Set the active context name (call save() to persist).

    :param name: Context name to activate.
    """
    self._context_name = name

should_load_plugin

should_load_plugin(plugin_name: str) -> bool

Determine if a plugin should be loaded based on include/exclude settings.

:param plugin_name: The plugin name to test. :return: True if the plugin should be enabled.

Logic: 1. If include_plugins is present and non-empty, only those plugins are enabled 2. If exclude_plugins is present, remove those names from the enabled set 3. If both are present, apply in order: include first, then exclude

Source code in qlam_core/config/config.py
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
def should_load_plugin(self, plugin_name: str) -> bool:
    """Determine if a plugin should be loaded based on include/exclude settings.

    :param plugin_name: The plugin name to test.
    :return: True if the plugin should be enabled.

    Logic:
    1. If include_plugins is present and non-empty, only those plugins are enabled
    2. If exclude_plugins is present, remove those names from the enabled set
    3. If both are present, apply in order: include first, then exclude
    """
    include_plugins = self._data.include_plugins
    exclude_plugins = self._data.exclude_plugins

    # Start with enabled=True by default
    enabled = True

    # Apply include_plugins filter if present (even if empty)
    if include_plugins is not None:
        enabled = plugin_name in include_plugins

    # Apply exclude_plugins filter if present
    if exclude_plugins and enabled:
        enabled = plugin_name not in exclude_plugins

    return enabled

Context

Bases: BaseModel


              flowchart TD
              qlam_core.config.config.Context[Context]

              

              click qlam_core.config.config.Context href "" "qlam_core.config.config.Context"
            

A QSH Client context configuration

auth_providers class-attribute instance-attribute

auth_providers: list[Annotated[BasicAuthProvider | OauthAuthProvider | TokenAuthProvider, Field(discriminator='provider')]] | None = None

List of authentication providers available in this context

name instance-attribute

name: str

Unique name for this context

plugins class-attribute instance-attribute

plugins: dict[str, PluginConfig] | None = None

Per-plugin configuration overrides

qpu instance-attribute

qpu: str

Default QPU for this context