mirror of
https://github.com/sstent/FitTrack_GarminSync.git
synced 2026-01-26 09:01:53 +00:00
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:
0
cli/src/utils/__init__.py
Normal file
0
cli/src/utils/__init__.py
Normal file
55
cli/src/utils/config.py
Normal file
55
cli/src/utils/config.py
Normal 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
98
cli/src/utils/output.py
Normal 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()
|
||||
Reference in New Issue
Block a user