feat(config): add TOML config file support
- Add config.py with load_config(), find_default_config(), get_token(), and get_map_path() - Auto-discover timesheets.toml in cwd; override with --config flag - Priority: CLI flag > config file > env var / cwd default - Add timesheets.example.toml as a committed reference template - Add timesheets.toml to .gitignore to prevent accidental secret leakage - Document config file format in AGENTS.md
This commit is contained in:
parent
267ad5b1b5
commit
29698b1241
6 changed files with 183 additions and 5 deletions
|
|
@ -3,6 +3,7 @@ import os
|
|||
import sys
|
||||
from datetime import date
|
||||
|
||||
from .config import find_default_config, get_map_path, get_token, load_config
|
||||
from .output import print_summary, write_csv
|
||||
from .parser import aggregate_rows, filter_rows_by_date, parse_document
|
||||
from .projects import load_project_map
|
||||
|
|
@ -32,6 +33,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
),
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--config",
|
||||
help="Path to a TOML config file. Defaults to timesheets.toml in the current working directory if it exists.",
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--token",
|
||||
help="Joplin API token. Falls back to the JOPLIN_TOKEN environment variable.",
|
||||
|
|
@ -68,8 +74,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
return parser
|
||||
|
||||
|
||||
def _resolve_token(args: argparse.Namespace) -> str:
|
||||
token = args.token or os.environ.get("JOPLIN_TOKEN")
|
||||
def _resolve_token(args: argparse.Namespace, config: dict) -> str:
|
||||
token = args.token or get_token(config) or os.environ.get("JOPLIN_TOKEN")
|
||||
if not token:
|
||||
print(
|
||||
"Error: Joplin API token required. "
|
||||
|
|
@ -83,6 +89,10 @@ def _resolve_token(args: argparse.Namespace) -> str:
|
|||
def main() -> None:
|
||||
args = build_parser().parse_args()
|
||||
|
||||
# Load config file: explicit --config flag, else auto-discover timesheets.toml
|
||||
config_path = args.config if args.config is not None else find_default_config()
|
||||
config = load_config(config_path)
|
||||
|
||||
if args.day is None:
|
||||
target_date = date.today()
|
||||
else:
|
||||
|
|
@ -101,7 +111,7 @@ def main() -> None:
|
|||
# Late import so joppy is only required when --joplin is used
|
||||
from .joplin import fetch_week_note
|
||||
|
||||
token = _resolve_token(args)
|
||||
token = _resolve_token(args, config)
|
||||
try:
|
||||
content = fetch_week_note(token, target_date)
|
||||
except RuntimeError as e:
|
||||
|
|
@ -129,8 +139,8 @@ def main() -> None:
|
|||
|
||||
aggregated = aggregate_rows(rows)
|
||||
|
||||
# Resolve project map: explicit --map flag, else project_map.json in cwd
|
||||
map_path = args.map
|
||||
# Resolve project map: CLI flag > config file > project_map.json in cwd
|
||||
map_path = args.map or get_map_path(config)
|
||||
if map_path is None:
|
||||
default_map = os.path.join(os.getcwd(), "project_map.json")
|
||||
if os.path.exists(default_map):
|
||||
|
|
|
|||
54
src/timesheets/config.py
Normal file
54
src/timesheets/config.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
"""
|
||||
TOML config file support.
|
||||
|
||||
Default filename: timesheets.toml in the current working directory.
|
||||
|
||||
Supported keys:
|
||||
[joplin]
|
||||
token = "..."
|
||||
|
||||
[projects]
|
||||
map = "/path/to/project_map.json"
|
||||
"""
|
||||
|
||||
import sys
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
DEFAULT_CONFIG_FILENAME = "timesheets.toml"
|
||||
|
||||
|
||||
def find_default_config() -> Path | None:
|
||||
"""Return the default config path if it exists, else None."""
|
||||
p = Path.cwd() / DEFAULT_CONFIG_FILENAME
|
||||
return p if p.exists() else None
|
||||
|
||||
|
||||
def load_config(path: Path | str | None) -> dict:
|
||||
"""
|
||||
Load a TOML config file and return its contents as a dict.
|
||||
Returns an empty dict if path is None.
|
||||
Exits with an error message if the file is missing or malformed.
|
||||
"""
|
||||
if path is None:
|
||||
return {}
|
||||
p = Path(path)
|
||||
try:
|
||||
with p.open("rb") as f:
|
||||
return tomllib.load(f)
|
||||
except FileNotFoundError:
|
||||
print(f"Error: config file not found: {p}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except tomllib.TOMLDecodeError as e:
|
||||
print(f"Error: could not parse config file {p}: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def get_token(config: dict) -> str | None:
|
||||
"""Extract joplin.token from a loaded config dict, or None."""
|
||||
return config.get("joplin", {}).get("token")
|
||||
|
||||
|
||||
def get_map_path(config: dict) -> str | None:
|
||||
"""Extract projects.map from a loaded config dict, or None."""
|
||||
return config.get("projects", {}).get("map")
|
||||
Loading…
Add table
Add a link
Reference in a new issue