Skip to content

buckethead.config.user

Per-user config at ~/.config/buckethead/config.toml (XDG-respecting). Two jobs:

  • [env] — key/value pairs merged into os.environ via setdefault() on CLI startup, so BUCKETHEAD_* env vars can be set once per machine instead of exported per shell.
  • [projects] — persistent project → bucket map, with optional share / share_mode / share_public_base_url fields for projects that have been through buckethead provision share-bucket.

Inspect via buckethead config path / buckethead config show.

buckethead.config.user

User-level config at ~/.config/buckethead/config.toml.

Loaded via pydantic-settings with TomlConfigSettingsSource for the read-side view. Writes are additive: every mutation reads the file through tomlkit, splices just the affected key, and writes back. Untouched sections, unknown tables, comments, and formatting survive the round trip — so multiple projects can safely share one file and schema drift never silently drops fields.

Two known sections:

  1. env — key/value pairs surfaced to BucketHeadSettings via a custom pydantic-settings source (UserConfigEnvSource). Values may be literal strings or secret-store refs like op://vault/item/field; refs are resolved at load time via secret_refs.resolve_value.

  2. projects — persistent project → bucket name map. Values are either a string (primary bucket only) or an inline table {primary = "...", share = "...", share_mode = "...", ...}.

XDG Base Directory spec is honored: $XDG_CONFIG_HOME/buckethead/ wins over the ~/.config/buckethead/ default.

UserConfig

Bases: BaseSettings

load classmethod

load() -> Self

Construct from the TOML file + defaults (read-only view).

Source code in src/buckethead/config/user.py
@classmethod
def load(cls) -> Self:
    """Construct from the TOML file + defaults (read-only view)."""
    return cls()

forget_project

forget_project(
    project: str, *, path: Path | None = None
) -> None

Remove a project mapping (primary + share) and persist.

Source code in src/buckethead/config/user.py
def forget_project(self, project: str, *, path: Path | None = None) -> None:
    """Remove a project mapping (primary + share) and persist."""
    if project in self.projects:
        del self.projects[project]
        unset_project(project, path=path)

bucket_for_project

bucket_for_project(
    project: str, *, path: Path | None = None
) -> str

Return the primary bucket for project; generate and persist on miss.

Source code in src/buckethead/config/user.py
def bucket_for_project(self, project: str, *, path: Path | None = None) -> str:
    """Return the primary bucket for `project`; generate and persist on miss."""
    entry = self.projects.get(project)
    if entry is None:
        bucket = f"{project}-{token_hex(3)}"
        self.projects[project] = bucket
        set_project(project, primary=bucket, path=path)
        return bucket
    if isinstance(entry, str):
        return entry
    return entry["primary"]

configure_share_for_project

configure_share_for_project(
    project: str,
    *,
    share_bucket: str,
    mode: str,
    public_base_url: str | None = None,
    path: Path | None = None,
) -> None

Attach or update share config on an existing project entry.

Raises KeyError if the project isn't present — call bucket_for_project first to create the primary.

Source code in src/buckethead/config/user.py
def configure_share_for_project(
    self,
    project: str,
    *,
    share_bucket: str,
    mode: str,
    public_base_url: str | None = None,
    path: Path | None = None,
) -> None:
    """Attach or update share config on an existing project entry.

    Raises KeyError if the project isn't present — call
    `bucket_for_project` first to create the primary.
    """
    if mode not in ("public", "protected"):
        raise ValueError(f"mode must be 'public' or 'protected', got {mode!r}")
    entry = self.projects.get(project)
    if entry is None:
        raise KeyError(f"project {project!r} not in user config")

    base: dict[str, str] = (
        {"primary": entry} if isinstance(entry, str) else dict(entry)
    )
    base["share"] = share_bucket
    base["share_mode"] = mode
    if public_base_url is None:
        base.pop("share_public_base_url", None)
    else:
        base["share_public_base_url"] = public_base_url
    self.projects[project] = base

    set_project(
        project,
        share=share_bucket,
        share_mode=mode,
        share_public_base_url=public_base_url,
        clear_public_base_url=public_base_url is None,
        path=path,
    )

forget_share_for_project

forget_share_for_project(
    project: str, *, path: Path | None = None
) -> None

Detach the share bucket; demote to string form if only primary remains.

Source code in src/buckethead/config/user.py
def forget_share_for_project(
    self, project: str, *, path: Path | None = None
) -> None:
    """Detach the share bucket; demote to string form if only `primary` remains."""
    entry = self.projects.get(project)
    if not isinstance(entry, dict):
        return
    remaining = {k: v for k, v in entry.items() if k not in _SHARE_FIELDS}
    self.projects[project] = (
        remaining["primary"] if list(remaining.keys()) == ["primary"] else remaining
    )
    unset_share_for_project(project, path=path)

UserConfigEnvSource

Bases: EnvSettingsSource

Pydantic-settings source backed by the [env] table in ~/.config/buckethead/config.toml, with secret-ref resolution.

set_env_var

set_env_var(
    key: str, value: str, *, path: Path | None = None
) -> None

Upsert a single [env] key.

Source code in src/buckethead/config/user.py
def set_env_var(key: str, value: str, *, path: Path | None = None) -> None:
    """Upsert a single `[env]` key."""
    path = path or config_path()
    doc = _load_doc(path)
    if "env" not in doc:
        doc["env"] = tomlkit.table()
    doc["env"][key] = value  # type: ignore[index]
    _write_doc(path, doc)

unset_env_var

unset_env_var(
    key: str, *, path: Path | None = None
) -> None

Remove a single [env] key; drop the section if it becomes empty.

Source code in src/buckethead/config/user.py
def unset_env_var(key: str, *, path: Path | None = None) -> None:
    """Remove a single `[env]` key; drop the section if it becomes empty."""
    path = path or config_path()
    doc = _load_doc(path)
    env = doc.get("env")
    if env is None or key not in env:
        return
    del env[key]
    if len(env) == 0:
        del doc["env"]
    _write_doc(path, doc)

set_project

set_project(
    project: str,
    *,
    primary: str | None = None,
    share: str | None = None,
    share_mode: str | None = None,
    share_public_base_url: str | None = None,
    clear_public_base_url: bool = False,
    path: Path | None = None,
) -> None

Upsert a project entry. Only the passed kwargs mutate; others pass through.

Source code in src/buckethead/config/user.py
def set_project(
    project: str,
    *,
    primary: str | None = None,
    share: str | None = None,
    share_mode: str | None = None,
    share_public_base_url: str | None = None,
    clear_public_base_url: bool = False,
    path: Path | None = None,
) -> None:
    """Upsert a project entry. Only the passed kwargs mutate; others pass through."""
    path = path or config_path()
    doc = _load_doc(path)
    if "projects" not in doc:
        doc["projects"] = tomlkit.table()
    projects = doc["projects"]

    existing = projects.get(project)  # type: ignore[union-attr]
    if existing is None:
        current: dict[str, str] = {}
    elif isinstance(existing, str):
        current = {"primary": existing}
    else:
        current = dict(existing)

    if primary is not None:
        current["primary"] = primary
    if share is not None:
        current["share"] = share
    if share_mode is not None:
        current["share_mode"] = share_mode
    if share_public_base_url is not None:
        current["share_public_base_url"] = share_public_base_url
    elif clear_public_base_url:
        current.pop("share_public_base_url", None)

    if list(current.keys()) == ["primary"]:
        projects[project] = current["primary"]  # type: ignore[index]
    else:
        inline = tomlkit.inline_table()
        for k, v in current.items():
            inline[k] = v
        projects[project] = inline  # type: ignore[index]

    _write_doc(path, doc)

unset_project

unset_project(
    project: str, *, path: Path | None = None
) -> None

Drop a project entirely; drop the section if it becomes empty.

Source code in src/buckethead/config/user.py
def unset_project(project: str, *, path: Path | None = None) -> None:
    """Drop a project entirely; drop the section if it becomes empty."""
    path = path or config_path()
    doc = _load_doc(path)
    projects = doc.get("projects")
    if projects is None or project not in projects:
        return
    del projects[project]
    if len(projects) == 0:
        del doc["projects"]
    _write_doc(path, doc)

unset_share_for_project

unset_share_for_project(
    project: str, *, path: Path | None = None
) -> None

Strip share fields from a project entry; demote to string form if only primary remains.

Source code in src/buckethead/config/user.py
def unset_share_for_project(project: str, *, path: Path | None = None) -> None:
    """Strip share fields from a project entry; demote to string form if only
    `primary` remains."""
    path = path or config_path()
    doc = _load_doc(path)
    projects = doc.get("projects")
    if projects is None or project not in projects:
        return
    entry = projects[project]
    if isinstance(entry, str):
        return

    remaining = {k: v for k, v in entry.items() if k not in _SHARE_FIELDS}
    if list(remaining.keys()) == ["primary"]:
        projects[project] = remaining["primary"]
    else:
        inline = tomlkit.inline_table()
        for k, v in remaining.items():
            inline[k] = v
        projects[project] = inline
    _write_doc(path, doc)