Implement CLI app for API interaction with MFA

- Create full CLI application with authentication, sync triggering, and status checking
- Implement MFA support for secure authentication
- Add token management with secure local storage
- Create API client for backend communication
- Implement data models for User Session, Sync Job, and Authentication Token
- Add command-line interface with auth and sync commands
- Include unit and integration tests
- Follow project constitution standards for Python 3.13, type hints, and code quality
- Support multiple output formats (table, JSON, CSV)
This commit is contained in:
2025-12-18 15:23:56 -08:00
parent 31f96660c7
commit fb6417b1a3
27 changed files with 1502 additions and 44 deletions

View File

55
cli/src/utils/config.py Normal file
View File

@@ -0,0 +1,55 @@
import os
from pathlib import Path
from typing import Any, Dict, Optional
import yaml
class ConfigManager:
"""Configuration management utilities for YAML config"""
def __init__(self, config_path: Optional[Path] = None):
if config_path is None:
# Use default location in user's home directory
self.config_path = Path.home() / ".garmin-sync" / "config.yaml"
self.config_path.parent.mkdir(exist_ok=True)
else:
self.config_path = config_path
self.config_path.parent.mkdir(parents=True, exist_ok=True)
self.config = self._load_config()
def _load_config(self) -> Dict[str, Any]:
"""Load configuration from YAML file"""
if self.config_path.exists():
with open(self.config_path, "r") as f:
return yaml.safe_load(f) or {}
else:
# Return default configuration
default_config = {
"api_base_url": "https://api.garmin.com",
"default_timeout": 30,
"output_format": "table", # Options: table, json, csv
"remember_login": True,
}
self._save_config(default_config)
return default_config
def _save_config(self, config: Dict[str, Any]) -> None:
"""Save configuration to YAML file"""
with open(self.config_path, "w") as f:
yaml.dump(config, f)
def get(self, key: str, default: Any = None) -> Any:
"""Get a configuration value"""
return self.config.get(key, default)
def set(self, key: str, value: Any) -> None:
"""Set a configuration value"""
self.config[key] = value
self._save_config(self.config)
def update(self, updates: Dict[str, Any]) -> None:
"""Update multiple configuration values"""
self.config.update(updates)
self._save_config(self.config)

98
cli/src/utils/output.py Normal file
View File

@@ -0,0 +1,98 @@
import csv
import json
from io import StringIO
from typing import Any, Dict, List, Mapping, Set, Union
def format_output(data: Any, format_type: str = "table") -> str:
"""Format output in multiple formats (JSON, table, CSV)"""
if format_type.lower() == "json":
return json.dumps(data, indent=2, default=str)
elif format_type.lower() == "csv":
return _format_as_csv(data)
elif format_type.lower() == "table":
return _format_as_table(data)
else:
# Default to table format
return _format_as_table(data)
def _format_as_table(data: Any) -> str:
"""Format data as a human-readable table"""
if isinstance(data, dict):
# Format dictionary as key-value pairs
lines = []
for key, value in data.items():
lines.append(f"{key:<20} | {value}")
return "\n".join(lines)
elif isinstance(data, list):
if not data:
return "No data to display"
if isinstance(data[0], dict):
# Format list of dictionaries as a table
if not data[0]:
return "No data to display"
headers = list(data[0].keys())
# Create header row
header_line = " | ".join(f"{h:<15}" for h in headers)
separator = "-+-".join("-" * 15 for _ in headers)
# Create data rows
rows = [header_line, separator]
for item in data:
row = " | ".join(f"{str(item.get(h, '')):<15}" for h in headers)
rows.append(row)
return "\n".join(rows)
else:
# Format simple list
return "\n".join(str(item) for item in data)
else:
# For other types, just convert to string
return str(data)
def _format_as_csv(data: Any) -> str:
"""Format data as CSV"""
if isinstance(data, dict):
# Convert single dict to list with one item for CSV processing
data = [data]
if isinstance(data, list) and data and isinstance(data[0], dict):
# Format list of dictionaries as CSV
output = StringIO()
if data:
fieldnames: Set[str] = set()
for row in data:
fieldnames.update(row.keys())
fieldnames = sorted(list(fieldnames))
writer_csv: csv.DictWriter = csv.DictWriter(output, fieldnames=fieldnames)
writer_csv.writeheader()
for row in data:
writer_csv.writerow({k: v for k, v in row.items() if k in fieldnames})
return output.getvalue()
elif isinstance(data, list):
# Format simple list as CSV with one column
output = StringIO()
writer_csv: csv.writer = csv.writer(output)
for item in data:
writer_csv.writerow([item])
return output.getvalue()
else:
# For other types, just convert to string and put in one cell
output = StringIO()
writer_csv: csv.writer = csv.writer(output)
writer_csv.writerow([str(data)])
return output.getvalue()