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.

Parameters:

Name Type Description Default
data QshClientConfig

Parsed config data model.

required
context_name str | None

Optional override for the active context name.

None
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()
    )

context_name property

context_name: str

Get the currently active context name.

Returns:

Type Description
str

Active context name.

context_names property

context_names: list[str]

Get the names of all configured contexts.

Returns:

Type Description
list[str]

List of context name strings.

contexts property

contexts: list[Context]

Get all configured contexts.

Returns:

Type Description
list[Context]

List of Context instances.

current_context property

current_context: Context

Get the currently active context.

Returns:

Type Description
Context

Context instance for the active context.

qpu property

qpu: str

Get QPU from current context, with env override.

Returns:

Type Description
str

QPU identifier string.

get_auth_providers

get_auth_providers() -> List | None

Get list of auth providers for the current context.

Returns:

Type Description
List | None

List of provider configs or None.

Source code in qlam_core/config/config.py
256
257
258
259
260
261
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.

Returns:

Type Description
Path

Absolute path to config JSON file.

Source code in qlam_core/config/config.py
122
123
124
125
126
127
128
@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.

Parameters:

Name Type Description Default
all_plugin_names set[str]

All discovered plugin names.

required

Returns:

Type Description
set[str]

Enabled plugin names after filters.

Source code in qlam_core/config/config.py
237
238
239
240
241
242
243
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.

Parameters:

Name Type Description Default
plugin_name str

Plugin name key.

required

Returns:

Type Description
PluginConfig | None

PluginConfig or None if not configured.

Source code in qlam_core/config/config.py
245
246
247
248
249
250
251
252
253
254
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.

Parameters:

Name Type Description Default
context_name str | None

Optional context name to activate.

None

Returns:

Type Description
Config

Loaded Config wrapper instance.

Source code in qlam_core/config/config.py
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
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
@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.

Returns:

Type Description
None

None

Source code in qlam_core/config/config.py
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
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.
                # exclude_unset writes only what was loaded or assigned: unset optional
                # fields would otherwise be written as null, which the schema rejects.
                json.dump(self._data.model_dump(mode="json", exclude_unset=True), 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).

Parameters:

Name Type Description Default
name str

Context name to activate.

required
Source code in qlam_core/config/config.py
115
116
117
118
119
120
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.

Parameters:

Name Type Description Default
plugin_name str

The plugin name to test.

required

Returns:

Type Description
bool

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
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
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