Files
GarminSync/Design.md
2025-08-08 12:33:11 -07:00

205 lines
9.1 KiB
Markdown

Of course. The design has been updated to use the `python-garminconnect` library and now includes a dedicated documentation section with links to all key upstream libraries.
-----
## **GarminSync Application Design (Python Version)**
### **Basic Info**
**App Name:** GarminSync
**What it does:** A CLI application that downloads `.fit` files for every activity in Garmin Connect.
-----
### **Core Features**
1. List all activities (`garminsync list --all`)
2. List activities that have not been downloaded (`garminsync list --missing`)
3. List activities that have been downloaded (`garminsync list --downloaded`)
4. Download all missing activities (`garminsync download --missing`)
-----
### **Tech Stack 🐍**
* **Frontend:** CLI (**Python**)
* **Backend:** **Python**
* **Database:** SQLite (`garmin.db`)
* **Hosting:** Docker container
* **Key Libraries:**
* **`python-garminconnect`**: The library for Garmin Connect API communication.
* **`typer`**: A modern and easy-to-use CLI framework (built on `click`).
* **`python-dotenv`**: For loading credentials from a `.env` file.
* **`sqlalchemy`**: A robust ORM for database interaction and schema management (using Alembic for migrations). The built-in `sqlite3` module can be used for simpler needs.
* **`tqdm`**: For creating user-friendly progress bars.
-----
### **Data Structure**
The core database schema remains the same. It can be defined using raw SQL with the `sqlite3` module or, more robustly, with an `SQLAlchemy` model.
**SQLAlchemy Model Example (`models.py`):**
```python
from sqlalchemy import create_engine, Column, Integer, String, Boolean
from sqlalchemy.orm import declarative_base
Base = declarative_base()
class Activity(Base):
__tablename__ = 'activities'
activity_id = Column(Integer, primary_key=True)
start_time = Column(String, nullable=False)
filename = Column(String, unique=True, nullable=True)
downloaded = Column(Boolean, default=False, nullable=False)
```
-----
### **User Flow**
1. User launches the container with their credentials: `docker run -it --env-file .env -v $(pwd)/data:/app/data garminsync`
2. The application presents a help menu generated by `Typer`.
3. User runs a command, e.g., `garminsync download --missing`.
4. The application executes the task, showing progress indicators for API calls and downloads.
5. Upon completion, the application displays a summary of actions taken.
-----
### **File Structure**
A standard Python project structure is recommended.
```
/garminsync
├── garminsync/ # Main application package
│ ├── __init__.py
│ ├── cli.py # Typer CLI commands and entrypoint
│ ├── config.py # Configuration loading (dotenv)
│ ├── database.py # SQLAlchemy models and sync logic
│ └── garmin.py # Wrapper for the python-garminconnect client
├── data/ # Directory for downloaded .fit files and DB
├── tests/ # Unit and integration tests
├── .env # Stores GARMIN_EMAIL/GARMIN_PASSWORD
├── .gitignore
├── Dockerfile
├── README.md
└── requirements.txt # Pinned Python dependencies
```
-----
### **Technical Implementation Notes**
* **Architecture:** A Python application using **`Typer`** for the CLI. Logic is separated into modules for configuration, database interaction, and Garmin communication.
* **Authentication:** Credentials from `GARMIN_EMAIL` and `GARMIN_PASSWORD` environment variables are loaded via **`python-dotenv`** and passed to **`python-garminconnect`**.
* **File Naming:** Files will be named using Python's f-strings: `f"activity_{activity_id}_{timestamp}.fit"`.
* **Rate Limiting:** A simple `time.sleep(2)` will be inserted between API requests to avoid overloading Garmin's servers.
* **Database:** The schema will be managed by **`SQLAlchemy`**. Versioned migrations can be handled by **Alembic**, which integrates with SQLAlchemy.
* **Database Sync:** Before any operation, a sync function will use **`python-garminconnect`** to fetch the latest activities and reconcile them with the local SQLite database.
* **Package Management:** Dependencies and their versions will be pinned in `requirements.txt` for reproducible builds (`pip freeze > requirements.txt`).
* **Session Management:** **`python-garminconnect`** handles authentication and session management.
* **Docker Permissions:** Mounting a local `./data` directory into the container allows downloaded files and the database to persist. The `sudo` prefix is generally not required if the user is part of the `docker` group.
-----
### **Development Phases (Proposed for Python)**
#### **Phase 1: Core Infrastructure**
* [x] `Dockerfile` creation for a Python environment.
* Uses Python 3.10 slim base image
* Sets PYTHONDONTWRITEBYTECODE and PYTHONUNBUFFERED
* Installs build dependencies
* Copies requirements first for layer caching
* Sets ENV_FILE for configuration
* Entrypoint runs application via `python -m garminsync`
* [x] Setup `python-dotenv` for environment variable handling.
* Added `python-dotenv` to requirements.txt
* Implemented environment loading in config.py:
```python
from dotenv import load_dotenv
load_dotenv() # Load .env file
```
* Updated Dockerfile to copy .env file during build
* Added documentation for environment variable setup
* [x] Initialize the **`Typer`** CLI framework in `cli.py`.
* Created basic Typer app structure
* Implemented skeleton commands for `list` and `download`
* Added proper type hints and docstrings
* Integrated with config.py for environment variable loading
* [x] Implement the **`python-garminconnect`** client wrapper (`garmin.py`).
* Created GarminClient class with authentication
* Implemented get_activities() with rate limiting
* Implemented download_activity_fit() method
* Added error handling for missing credentials
#### **Phase 2: Activity Listing**
* [x] Implement the `SQLAlchemy` schema and database connection.
* Created database.py with Activity model
* Implemented init_db() for database initialization
* Added get_session() for session management
* Implemented sync_database() to fetch activities from Garmin and update database
* [x] Create the `list` commands (`--all`, `--missing`, `--downloaded`).
* Implemented command logic in cli.py
* Added database synchronization before listing
* Added input validation for filter options
* Used tqdm for progress display
#### **Phase 3: Download Pipeline**
* [x] Implement the `.fit` file download function using `python-garminconnect`.
* Added download logic to cli.py
* Used GarminClient to download FIT files
* Created filename-safe timestamps
* Saved files to data directory
* [x] Ensure downloads are idempotent (don't re-download existing files).
* Database tracks downloaded status
* Only missing activities are downloaded
* [x] Update the database record to `downloaded=True` on success.
* Updated activity record after successful download
* Set filename path in database
#### **Phase 4: Polish ✨**
* [x] Add **`tqdm`** progress bars for the download command.
* Implemented in both list and download commands
* [x] Update Dockerfile to:
* Use multi-stage builds for smaller image size
* Add health checks
* Set non-root user for security
* [ ] Implement robust error handling (e.g., API errors, network issues).
* Basic error handling implemented but needs improvement
* [ ] Write comprehensive `README.md` documentation.
* [ ] Add unit tests for database and utility functions.
#### **Current Issues**
1. **Docker Build Warnings**
- Legacy ENV format warnings during build
- Solution: Update Dockerfile to use `ENV key=value` format
2. **Typer Command Parameters**
- Occasional `Parameter.make_metavar()` errors
- Resolved by renaming reserved keywords and fixing decorators
3. **Configuration Loading**
- Initial import issues between modules
- Resolved by restructuring config loading
-----
### **Documentation 📚**
Here are links to the official documentation for the key libraries used in this design.
* **Garmin API:** [`python-garminconnect`](https://www.google.com/search?q=%5Bhttps://github.com/cyberjunky/python-garminconnect%5D\(https://github.com/cyberjunky/python-garminconnect\))
* **CLI Framework:** [`Typer`](https://www.google.com/search?q=%5Bhttps://typer.tiangolo.com/%5D\(https://typer.tiangolo.com/\))
* **Environment Variables:** [`python-dotenv`](https://www.google.com/search?q=%5Bhttps://github.com/theskumar/python-dotenv%5D\(https://github.com/theskumar/python-dotenv\))
* **Database ORM:** [`SQLAlchemy`](https://www.google.com/search?q=%5Bhttps://docs.sqlalchemy.org/en/20/%5D\(https://docs.sqlalchemy.org/en/20/\))
* **Database Migrations:** [`Alembic`](https://www.google.com/search?q=%5Bhttps://alembic.sqlalchemy.org/en/latest/%5D\(https://alembic.sqlalchemy.org/en/latest/\))
* **Progress Bars:** [`tqdm`](https://www.google.com/search?q=%5Bhttps://github.com/tqdm/tqdm%5D\(https://github.com/tqdm/tqdm\))