From 2b62987a0c94911b850d6e38b9ba48def4423185 Mon Sep 17 00:00:00 2001 From: Adam Ladachowski Date: Tue, 3 Feb 2026 20:55:19 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=AC=20Commit=20message:=20Update=20202?= =?UTF-8?q?6-02-03=2020:55:19,=205=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .coverage | Bin 53248 -> 53248 bytes pyproject.toml | 1 + tensors.py | 54 ++++++++++++++++++++++++++++++++++++++---- tests/test_tensors.py | 27 +++++++++++++++++++-- uv.lock | 38 +++++++++++++++++++++++++++++ 5 files changed, 113 insertions(+), 7 deletions(-) diff --git a/.coverage b/.coverage index 9bb1222701e61fa7bdab522799957a3725ffbdec..1d9f032775d221623ed27a198cb375aaa9883980 100644 GIT binary patch literal 53248 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 NfCkV28u))V@V~yaXd(ar literal 53248 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.4.0", "httpx>=0.27.0", "rich>=13.0.0", + "typer>=0.15.0", ] [project.scripts] diff --git a/tensors.py b/tensors.py index 5644669..9c148c8 100644 --- a/tensors.py +++ b/tensors.py @@ -12,6 +12,7 @@ import os import re import struct import sys +import tomllib from pathlib import Path from typing import Any @@ -31,7 +32,12 @@ from rich.table import Table console = Console() -RC_FILE = Path.home() / ".sftrc" +# XDG Base Directory spec: ~/.config/tensors/config.toml +CONFIG_DIR = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "tensors" +CONFIG_FILE = CONFIG_DIR / "config.toml" + +# Legacy config for migration +LEGACY_RC_FILE = Path.home() / ".sftrc" # Default download paths by model type DEFAULT_PATHS: dict[str, Path] = { @@ -41,16 +47,54 @@ DEFAULT_PATHS: dict[str, Path] = { } +def load_config() -> dict[str, Any]: + """Load configuration from TOML config file.""" + if CONFIG_FILE.exists(): + with CONFIG_FILE.open("rb") as f: + return tomllib.load(f) + return {} + + +def save_config(config: dict[str, Any]) -> None: + """Save configuration to TOML config file.""" + CONFIG_DIR.mkdir(parents=True, exist_ok=True) + + lines: list[str] = [] + for key, value in config.items(): + if isinstance(value, dict): + lines.append(f"[{key}]") + for k, v in value.items(): + if isinstance(v, str): + lines.append(f'{k} = "{v}"') + else: + lines.append(f"{k} = {v}") + lines.append("") + elif isinstance(value, str): + lines.append(f'{key} = "{value}"') + else: + lines.append(f"{key} = {value}") + + CONFIG_FILE.write_text("\n".join(lines) + "\n") + + def load_api_key() -> str | None: - """Load API key from ~/.sftrc or CIVITAI_API_KEY env var.""" + """Load API key from config file or CIVITAI_API_KEY env var.""" # Check environment variable first env_key = os.environ.get("CIVITAI_API_KEY") if env_key: return env_key - # Fall back to RC file - if RC_FILE.exists(): - content = RC_FILE.read_text().strip() + # Check TOML config file + config = load_config() + api_section = config.get("api", {}) + if isinstance(api_section, dict): + key = api_section.get("civitai_key") + if key: + return str(key) + + # Fall back to legacy RC file for migration + if LEGACY_RC_FILE.exists(): + content = LEGACY_RC_FILE.read_text().strip() if content: return content return None diff --git a/tests/test_tensors.py b/tests/test_tensors.py index 2ab988d..9672d6c 100644 --- a/tests/test_tensors.py +++ b/tests/test_tensors.py @@ -110,6 +110,29 @@ class TestLoadApiKey: def test_returns_none_if_no_key(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: """Test that None is returned when no key is available.""" monkeypatch.delenv("CIVITAI_API_KEY", raising=False) - # Temporarily point RC_FILE to nonexistent file - monkeypatch.setattr(tensors, "RC_FILE", tmp_path / "nonexistent") + # Point config and legacy files to nonexistent paths + monkeypatch.setattr(tensors, "CONFIG_FILE", tmp_path / "nonexistent" / "config.toml") + monkeypatch.setattr(tensors, "LEGACY_RC_FILE", tmp_path / "nonexistent") assert load_api_key() is None + + def test_returns_key_from_config_file( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + """Test that key is loaded from TOML config file.""" + monkeypatch.delenv("CIVITAI_API_KEY", raising=False) + config_file = tmp_path / "config.toml" + config_file.write_text('[api]\ncivitai_key = "key-from-config"\n') + monkeypatch.setattr(tensors, "CONFIG_FILE", config_file) + monkeypatch.setattr(tensors, "LEGACY_RC_FILE", tmp_path / "nonexistent") + assert load_api_key() == "key-from-config" + + def test_returns_key_from_legacy_file( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + """Test that key is loaded from legacy RC file when no config exists.""" + monkeypatch.delenv("CIVITAI_API_KEY", raising=False) + legacy_file = tmp_path / ".sftrc" + legacy_file.write_text("legacy-key") + monkeypatch.setattr(tensors, "CONFIG_FILE", tmp_path / "nonexistent" / "config.toml") + monkeypatch.setattr(tensors, "LEGACY_RC_FILE", legacy_file) + assert load_api_key() == "legacy-key" diff --git a/uv.lock b/uv.lock index 05f12cd..dcfac2c 100644 --- a/uv.lock +++ b/uv.lock @@ -33,6 +33,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, ] +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -539,6 +551,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" }, ] +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + [[package]] name = "tensors" version = "0.1.0" @@ -547,6 +568,7 @@ dependencies = [ { name = "httpx" }, { name = "rich" }, { name = "safetensors" }, + { name = "typer" }, ] [package.dev-dependencies] @@ -564,6 +586,7 @@ requires-dist = [ { name = "httpx", specifier = ">=0.27.0" }, { name = "rich", specifier = ">=13.0.0" }, { name = "safetensors", specifier = ">=0.4.0" }, + { name = "typer", specifier = ">=0.15.0" }, ] [package.metadata.requires-dev] @@ -576,6 +599,21 @@ dev = [ { name = "ruff", specifier = ">=0.9.0" }, ] +[[package]] +name = "typer" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/bf/8825b5929afd84d0dabd606c67cd57b8388cb3ec385f7ef19c5cc2202069/typer-0.21.1.tar.gz", hash = "sha256:ea835607cd752343b6b2b7ce676893e5a0324082268b48f27aa058bdb7d2145d", size = 110371, upload-time = "2026-01-06T11:21:10.989Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381, upload-time = "2026-01-06T11:21:09.824Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0"