feat: Add --debug option to CLI for verbose output

This commit introduces a global  option to the GarminSync CLI, providing verbose logging and diagnostic information for troubleshooting.

Key changes include:

- Implemented a  to manage and propagate the debug flag across CLI commands.

- Refactored  in  to accept and utilize the debug flag, enabling detailed logging of HTTP requests and responses.

- Updated CLI commands (, ) to access the  from the .

- Resolved circular import by extracting  into a dedicated  module.

- Configured  for Poetry-based dependency management.

- Addressed various  type hinting issues and  linting warnings across the CLI codebase to maintain code quality.
This commit is contained in:
2025-12-22 06:39:40 -08:00
parent 9e096e6f6e
commit 02fa8aa1eb
13 changed files with 1018 additions and 325 deletions

View File

@@ -1,13 +1,11 @@
import os
import yaml # type: ignore[import-untyped]
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
@@ -16,18 +14,18 @@ class ConfigManager:
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:
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",
"api_base_url": "http://localhost:8001", # Default to local GarminSync service
"default_timeout": 30,
"output_format": "table", # Options: table, json, csv
"remember_login": True,
@@ -37,7 +35,7 @@ class ConfigManager:
def _save_config(self, config: Dict[str, Any]) -> None:
"""Save configuration to YAML file"""
with open(self.config_path, "w") as f:
with open(self.config_path, 'w') as f:
yaml.dump(config, f)
def get(self, key: str, default: Any = None) -> Any:
@@ -52,4 +50,4 @@ class ConfigManager:
def update(self, updates: Dict[str, Any]) -> None:
"""Update multiple configuration values"""
self.config.update(updates)
self._save_config(self.config)
self._save_config(self.config)

View File

@@ -1,27 +1,28 @@
import csv
import json
import csv
from io import StringIO
from typing import Any, Dict, List, Mapping, Set, Union
from typing import List, Dict, Any, Union, Set, Mapping, cast # Import Set, Mapping, and cast
from csv import DictWriter # Removed CsvWriter from import
def format_output(data: Any, format_type: str = "table") -> str:
def format_output(data: Union[Dict, List, Any], output_format: str = "table") -> str:
"""Format output in multiple formats (JSON, table, CSV)"""
if format_type.lower() == "json":
if output_format.lower() == "json":
return json.dumps(data, indent=2, default=str)
elif format_type.lower() == "csv":
elif output_format.lower() == "csv":
return _format_as_csv(data)
elif format_type.lower() == "table":
elif output_format.lower() == "table":
return _format_as_table(data)
else:
# Default to table format
return _format_as_table(data)
def _format_as_table(data: Any) -> str:
def _format_as_table(data: Union[Dict, List, Any]) -> str:
"""Format data as a human-readable table"""
if isinstance(data, dict):
# Format dictionary as key-value pairs
@@ -29,70 +30,71 @@ def _format_as_table(data: Any) -> str:
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:
def _format_as_csv(data: Union[Dict, List, 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()
fieldnames: List[str] = [] # Initialize as List[str]
unique_fieldnames: Set[str] = set() # Use Set for uniqueness
for row in data:
fieldnames.update(row.keys())
fieldnames = sorted(list(fieldnames))
writer_csv: csv.DictWriter = csv.DictWriter(output, fieldnames=fieldnames)
writer_csv.writeheader()
unique_fieldnames.update(row.keys())
fieldnames = sorted(list(unique_fieldnames)) # Convert to list and sort
writer: DictWriter[Any] = csv.DictWriter(output, fieldnames=fieldnames) # Explicitly type writer
writer.writeheader()
for row in data:
writer_csv.writerow({k: v for k, v in row.items() if k in fieldnames})
writer.writerow(cast(Mapping[str, Any], {k: v for k, v in row.items() if k in fieldnames})) # Cast to Mapping[str, Any]
return output.getvalue()
elif isinstance(data, list):
# Format simple list as CSV with one column
output = StringIO()
writer_csv: csv.writer = csv.writer(output)
simple_writer = csv.writer(output) # Removed type hint CsvWriter
for item in data:
writer_csv.writerow([item])
simple_writer.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()
simple_writer = csv.writer(output) # Removed type hint CsvWriter
simple_writer.writerow([str(data)])
return output.getvalue()