Skip to content

Config

cxg.config

YAML configuration file support for cxg CLI.

DefaultsConfig

Bases: BaseModel

Default filter values applied when CLI flags are omitted.

Source code in src/cxg/config.py
class DefaultsConfig(BaseModel):
    """Default filter values applied when CLI flags are omitted."""

    model_config = ConfigDict(extra="forbid")

    organism: list[str] = Field(default_factory=list, description="Default organism filter.")
    tissue: list[str] = Field(default_factory=list, description="Default tissue filter.")
    tissue_type: list[str] = Field(default_factory=list, description="Default tissue type filter.")
    assay: list[str] = Field(default_factory=list, description="Default assay filter.")
    cell_type: list[str] = Field(default_factory=list, description="Default cell type filter.")
    disease: list[str] = Field(default_factory=list, description="Default disease filter.")
    self_reported_ethnicity: list[str] = Field(
        default_factory=list, description="Default self-reported ethnicity filter."
    )
    suspension_type: list[str] = Field(
        default_factory=list, description="Default suspension type filter."
    )

DisplayConfig

Bases: BaseModel

Display preferences for list output.

Source code in src/cxg/config.py
class DisplayConfig(BaseModel):
    """Display preferences for list output."""

    model_config = ConfigDict(extra="forbid")

    output: Literal["table", "json", "tsv"] | None = Field(None, description="Output format.")
    columns: str | None = Field(None, description="Comma-separated list of columns to display.")
    limit: int | None = Field(None, ge=1, description="Maximum number of results to show.")
    sort_by: str | None = Field(None, description="Sort key and direction, e.g. 'cell_count desc'.")

CacheConfig

Bases: BaseModel

Cache behavior settings.

Source code in src/cxg/config.py
class CacheConfig(BaseModel):
    """Cache behavior settings."""

    model_config = ConfigDict(extra="forbid")

    ttl: int | None = Field(None, ge=0, description="Cache time-to-live in seconds.")
    dir: str | None = Field(None, description="Override cache directory path.")

ApiConfig

Bases: BaseModel

API connection settings.

Source code in src/cxg/config.py
class ApiConfig(BaseModel):
    """API connection settings."""

    model_config = ConfigDict(extra="forbid")

    base_url: str | None = Field(None, description="CELLxGene API base URL.")

CxgConfig

Bases: BaseModel

Top-level cxg configuration.

Source code in src/cxg/config.py
class CxgConfig(BaseModel):
    """Top-level cxg configuration."""

    model_config = ConfigDict(extra="forbid")

    defaults: DefaultsConfig = Field(default_factory=DefaultsConfig)
    display: DisplayConfig = Field(default_factory=DisplayConfig)
    cache: CacheConfig = Field(default_factory=CacheConfig)
    api: ApiConfig = Field(default_factory=ApiConfig)

user_config_path()

Return the user config file path, respecting CXG_CONFIG_FILE env var.

Source code in src/cxg/config.py
def user_config_path() -> Path:
    """Return the user config file path, respecting CXG_CONFIG_FILE env var."""
    override = os.environ.get("CXG_CONFIG_FILE")
    if override:
        return Path(override).expanduser()
    return Path(user_config_dir("cxg")) / "config.yml"

project_config_path()

Walk up from cwd looking for cxg.yml.

Returns None if no project config is found.

Source code in src/cxg/config.py
def project_config_path() -> Path | None:
    """Walk up from cwd looking for cxg.yml.

    Returns None if no project config is found.
    """
    current = Path.cwd()
    while True:
        candidate = current / PROJECT_CONFIG_NAME
        if candidate.is_file():
            return candidate
        parent = current.parent
        if parent == current:  # filesystem root
            break
        current = parent
    return None

config_path_for_level(level)

Return the config file path for a given level.

Parameters:

Name Type Description Default
level str

Either "user" or "project".

required

Returns:

Type Description
Path | None

The path, or None if no project config is found.

Source code in src/cxg/config.py
def config_path_for_level(level: str) -> Path | None:
    """Return the config file path for a given level.

    Args:
        level: Either "user" or "project".

    Returns:
        The path, or None if no project config is found.
    """
    if level == "user":
        return user_config_path()
    if level == "project":
        return project_config_path()
    msg = f"Unknown config level: {level!r} (expected 'user' or 'project')"
    raise ValueError(msg)

schema_path()

Return the path to the bundled JSON Schema file.

Source code in src/cxg/config.py
def schema_path() -> Path:
    """Return the path to the bundled JSON Schema file."""
    return Path(str(files("cxg.schemas") / "config.schema.json"))

load_config()

Load, merge, and validate config files from all levels.

Precedence (highest to lowest): project config > user config > defaults. The result is cached for the lifetime of the process.

Source code in src/cxg/config.py
def load_config() -> CxgConfig:
    """Load, merge, and validate config files from all levels.

    Precedence (highest to lowest): project config > user config > defaults.
    The result is cached for the lifetime of the process.
    """
    global _cached  # noqa: PLW0603
    if _cached is not None:
        return _cached

    merged: dict[str, Any] = {}
    for path in [user_config_path(), project_config_path()]:
        if path is None or not path.exists():
            continue
        raw = _load_yaml(path)
        if raw is not None:
            merged = _deep_merge(merged, raw)

    if not merged:
        _cached = CxgConfig()
        return _cached

    try:
        _cached = CxgConfig.model_validate(merged)
    except ValidationError as exc:
        _err.print("[yellow]Warning: invalid merged configuration:[/yellow]")
        for error in exc.errors():
            loc = ".".join(str(part) for part in error["loc"])
            _err.print(f"[yellow]  {loc}: {error['msg']}[/yellow]")
        _cached = CxgConfig()

    return _cached

load_config_raw(level=None)

Load a config file as a plain dict.

Parameters:

Name Type Description Default
level str | None

"user" or "project" to read a specific file. None returns the merged config across all levels.

None
Source code in src/cxg/config.py
def load_config_raw(level: str | None = None) -> dict[str, Any]:
    """Load a config file as a plain dict.

    Args:
        level: "user" or "project" to read a specific file.
            None returns the merged config across all levels.
    """
    if level is not None:
        path = config_path_for_level(level)
        if path is None or not path.exists():
            return {}
        return _load_yaml(path) or {}

    # Merged across all levels
    merged: dict[str, Any] = {}
    for path in [user_config_path(), project_config_path()]:
        if path is None or not path.exists():
            continue
        raw = _load_yaml(path)
        if raw is not None:
            merged = _deep_merge(merged, raw)
    return merged

save_config(data, level='user')

Atomically write config data as YAML.

Parameters:

Name Type Description Default
data dict[str, Any]

The config data to write.

required
level str

"user" or "project". Project level writes to cwd.

'user'
Source code in src/cxg/config.py
def save_config(data: dict[str, Any], level: str = "user") -> None:
    """Atomically write config data as YAML.

    Args:
        data: The config data to write.
        level: "user" or "project". Project level writes to cwd.
    """
    if level == "project":
        path = Path.cwd() / PROJECT_CONFIG_NAME
    else:
        path = user_config_path()

    path.parent.mkdir(parents=True, exist_ok=True)

    content = yaml.dump(data, default_flow_style=False, sort_keys=False, allow_unicode=True)

    fd, temp_name = tempfile.mkstemp(dir=path.parent, prefix=f".{path.name}.", suffix=".tmp")
    temp_path = Path(temp_name)
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as handle:
            handle.write(content)
            handle.flush()
            os.fsync(handle.fileno())
        temp_path.replace(path)
    finally:
        temp_path.unlink(missing_ok=True)

    # Invalidate cached config since a config file changed
    _reset_cache()

get_value(data, dotted_key)

Traverse a nested dict by dot-separated key path.

Raises KeyError if any segment is missing.

Source code in src/cxg/config.py
def get_value(data: dict[str, Any], dotted_key: str) -> Any:
    """Traverse a nested dict by dot-separated key path.

    Raises KeyError if any segment is missing.
    """
    keys = dotted_key.split(".")
    current: Any = data
    for key in keys:
        if not isinstance(current, dict) or key not in current:
            raise KeyError(dotted_key)
        current = current[key]
    return current

set_value(data, dotted_key, raw_value)

Set a value in a nested dict, creating intermediate dicts as needed.

List-valued keys (defaults.*) are split on commas. Integer-valued keys (cache.ttl, display.limit) are coerced to int.

Source code in src/cxg/config.py
def set_value(data: dict[str, Any], dotted_key: str, raw_value: str) -> None:
    """Set a value in a nested dict, creating intermediate dicts as needed.

    List-valued keys (defaults.*) are split on commas.
    Integer-valued keys (cache.ttl, display.limit) are coerced to int.
    """
    keys = dotted_key.split(".")
    current = data
    for key in keys[:-1]:
        if key not in current or not isinstance(current[key], dict):
            current[key] = {}
        current = current[key]

    leaf = keys[-1]
    if dotted_key in _LIST_KEYS:
        current[leaf] = [v.strip() for v in raw_value.split(",") if v.strip()]
    elif dotted_key in _INT_KEYS:
        current[leaf] = int(raw_value)
    else:
        current[leaf] = raw_value