Skip to content

Commit 74ff0c3

Browse files
Scott KostolniScott Kostolni
authored andcommitted
fix(cli): normalize config data paths
1 parent 422429b commit 74ff0c3

6 files changed

Lines changed: 75 additions & 10 deletions

File tree

limitless_tools/cli/main.py

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
import sys
77
from zoneinfo import ZoneInfo
88

9+
from pathlib import Path
10+
911
from limitless_tools.config.config import default_config_path, get_profile, load_config
1012
from limitless_tools.config.env import load_env
1113
from limitless_tools.config.logging import setup_logging
12-
from limitless_tools.config.paths import default_data_dir
14+
from limitless_tools.config.paths import default_data_dir, expand_path
1315
from limitless_tools.services.lifelog_service import LifelogService
1416

1517

@@ -93,6 +95,14 @@ def _build_parser() -> argparse.ArgumentParser:
9395
return parser
9496

9597

98+
def _normalize_data_dir(value: str | None, *, base_dir: str | None = None) -> str:
99+
"""Return a data_dir resolved relative to an optional base directory."""
100+
normalized = expand_path(value, base_dir=base_dir)
101+
if normalized:
102+
return normalized
103+
return default_data_dir()
104+
105+
96106
def main(argv: list[str] | None = None) -> int:
97107
# Ensure .env and related environment variables are loaded before parsing
98108
load_env()
@@ -108,11 +118,13 @@ def main(argv: list[str] | None = None) -> int:
108118

109119
# Load config and resolve profile
110120
# Allow env var overrides for config path and profile
111-
config_path = args.config or os.getenv("LIMITLESS_CONFIG")
121+
config_path_arg = args.config or os.getenv("LIMITLESS_CONFIG")
122+
resolved_config_path = expand_path(config_path_arg) or default_config_path()
112123
profile_name = args.profile or os.getenv("LIMITLESS_PROFILE") or "default"
113124

114-
cfg = load_config(config_path)
125+
cfg = load_config(resolved_config_path)
115126
prof = get_profile(cfg, profile_name)
127+
config_base_dir = str(Path(resolved_config_path).expanduser().parent)
116128

117129
# Precedence: CLI flags > environment variables > config > defaults
118130
argv_list = argv or []
@@ -121,9 +133,11 @@ def _provided(opt: str) -> bool:
121133
return opt in argv_list
122134

123135
# data_dir precedence
136+
data_dir_from_config = False
124137
if not _provided("--data-dir") and not os.getenv("LIMITLESS_DATA_DIR"):
125138
if isinstance(prof.get("data_dir"), str):
126139
setattr(args, "data_dir", prof["data_dir"])
140+
data_dir_from_config = True
127141

128142
# batch_size precedence for fetch/sync
129143
if not _provided("--batch-size") and isinstance(prof.get("batch_size"), (int, float)):
@@ -139,6 +153,11 @@ def _provided(opt: str) -> bool:
139153
resolved_api_key = os.getenv("LIMITLESS_API_KEY") or (prof.get("api_key") if isinstance(prof.get("api_key"), str) else None)
140154
resolved_api_url = os.getenv("LIMITLESS_API_URL") or (prof.get("api_url") if isinstance(prof.get("api_url"), str) else None)
141155

156+
args.data_dir = _normalize_data_dir(
157+
getattr(args, "data_dir", None),
158+
base_dir=config_base_dir if data_dir_from_config else None,
159+
)
160+
142161
if args.command == "fetch":
143162
service = LifelogService(
144163
api_key=resolved_api_key,
@@ -257,7 +276,11 @@ def _provided(opt: str) -> bool:
257276
# Combined per-date export to a single file
258277
if args.combine:
259278
# Determine effective output directory: CLI --write-dir > config profile output_dir
260-
cfg_output_dir = prof.get("output_dir") if isinstance(prof.get("output_dir"), str) else None
279+
cfg_output_dir = (
280+
expand_path(prof.get("output_dir"), base_dir=config_base_dir)
281+
if isinstance(prof.get("output_dir"), str)
282+
else None
283+
)
261284
eff_write_dir = args.write_dir or cfg_output_dir
262285
if not args.date or not eff_write_dir:
263286
sys.stderr.write("--combine requires --date and a write directory (provide --write-dir or set output_dir in config)\n")
@@ -283,7 +306,11 @@ def _provided(opt: str) -> bool:
283306
)
284307
csv_text = service.export_csv(date=args.date, include_markdown=bool(getattr(args, "include_markdown", False)))
285308
# Determine effective output file: CLI --output > config profile output_dir + default filename; else stdout
286-
cfg_output_dir = prof.get("output_dir") if isinstance(prof.get("output_dir"), str) else None
309+
cfg_output_dir = (
310+
expand_path(prof.get("output_dir"), base_dir=config_base_dir)
311+
if isinstance(prof.get("output_dir"), str)
312+
else None
313+
)
287314
eff_output = getattr(args, "output", None)
288315
if not eff_output and cfg_output_dir:
289316
import os as _os
@@ -327,7 +354,7 @@ def _provided(opt: str) -> bool:
327354
if args.command == "configure":
328355
# Compute target config path and profile
329356
from limitless_tools.config.config import load_config as _load_cfg, save_config as _save_cfg
330-
target_path = config_path or default_config_path()
357+
target_path = resolved_config_path
331358
target_profile = profile_name
332359
# Load existing config
333360
current = _load_cfg(target_path)

limitless_tools/config/paths.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,29 @@ def default_data_dir() -> str:
88
"""Return the default data directory path for lifelogs."""
99
return str(Path(os.path.expanduser("~")) / "limitless_tools" / "data" / "lifelogs")
1010

11+
12+
def expand_path(path: str | None, *, base_dir: str | None = None) -> str | None:
13+
"""Return the provided path expanded, optionally relative to a base directory."""
14+
if not path:
15+
return None
16+
candidate = Path(path).expanduser()
17+
if not candidate.is_absolute():
18+
candidate = _restore_missing_root(candidate)
19+
if not candidate.is_absolute() and base_dir:
20+
base = Path(base_dir).expanduser()
21+
candidate = base / candidate
22+
return str(candidate)
23+
24+
25+
def _restore_missing_root(candidate: Path) -> Path:
26+
"""If the user omitted the leading '/', detect home-relative roots."""
27+
parts = candidate.parts
28+
if not parts:
29+
return candidate
30+
home = Path.home()
31+
home_parts = home.parts
32+
rel_home = home_parts[1:]
33+
if rel_home and len(parts) >= len(rel_home) and tuple(parts[: len(rel_home)]) == tuple(rel_home):
34+
root = home_parts[0] if home_parts else os.sep
35+
return Path(root).joinpath(*parts)
36+
return candidate

limitless_tools/storage/json_repo.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
class JsonFileRepository:
99
def __init__(self, base_dir: str) -> None:
10-
self.base_dir = Path(base_dir)
10+
self.base_dir = Path(base_dir).expanduser()
1111

1212
def path_for_lifelog(self, lifelog: dict[str, Any]) -> str:
1313
start_time = lifelog.get("startTime", "0000-00-00T00:00:00Z")
@@ -21,4 +21,3 @@ def save_lifelog(self, lifelog: dict[str, Any]) -> str:
2121
path.parent.mkdir(parents=True, exist_ok=True)
2222
path.write_text(json.dumps(lifelog, ensure_ascii=False, indent=2))
2323
return str(path)
24-

limitless_tools/storage/state_repo.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class StateRepository:
1212

1313
@property
1414
def _state_path(self) -> Path:
15-
lifelogs_dir = Path(self.base_lifelogs_dir)
15+
lifelogs_dir = Path(self.base_lifelogs_dir).expanduser()
1616
return lifelogs_dir.parent / "state" / "lifelogs_sync.json"
1717

1818
def load(self) -> dict[str, Any]:
@@ -28,4 +28,3 @@ def save(self, state: dict[str, Any]) -> None:
2828
p = self._state_path
2929
p.parent.mkdir(parents=True, exist_ok=True)
3030
p.write_text(json.dumps(state, ensure_ascii=False, indent=2))
31-

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ limitless = "limitless_tools.cli.main:main"
5252
[tool.setuptools.package-data]
5353
limitless_tools = ["py.typed"]
5454

55+
[tool.setuptools]
56+
packages = ["limitless_tools"]
57+
5558
[project.urls]
5659
Homepage = "https://github.com/ScottSucksAtProgramming/limitless_tools"
5760
Issues = "https://github.com/ScottSucksAtProgramming/limitless_tools/issues"

tests/conftest.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
5+
6+
@pytest.fixture(autouse=True)
7+
def isolate_config_path(tmp_path_factory, monkeypatch) -> None:
8+
"""Prevent pytest from loading the user config by pointing it to a temp path."""
9+
cfg_dir = tmp_path_factory.mktemp("config")
10+
cfg_path = cfg_dir / "config.toml"
11+
monkeypatch.setenv("LIMITLESS_CONFIG", str(cfg_path))

0 commit comments

Comments
 (0)