mirror of
https://github.com/sstent/go-garth.git
synced 2026-03-13 12:35:21 +00:00
sync
This commit is contained in:
280
.clinerules/ContextWindow.md
Normal file
280
.clinerules/ContextWindow.md
Normal file
@@ -0,0 +1,280 @@
|
||||
# You MUST use the `new_task` tool: Task Handoff Strategy Guide
|
||||
|
||||
**⚠️ CRITICAL INSTRUCTIONS - YOU MUST FOLLOW THESE GUIDELINES ⚠️**
|
||||
|
||||
This guide provides **MANDATORY** instructions for effectively breaking down complex tasks and implementing a smooth handoff process between tasks. You **MUST** follow these guidelines to ensure continuity, context preservation, and efficient task completion.
|
||||
|
||||
## ⚠️ CONTEXT WINDOW MONITORING - MANDATORY ACTION REQUIRED ⚠️
|
||||
|
||||
You **MUST** monitor the context window usage displayed in the environment details. When usage exceeds 50% of the available context window, you **MUST** initiate a task handoff using the `new_task` tool.
|
||||
|
||||
Example of context window usage over 50% with a 200K context window:
|
||||
|
||||
\`\`\`text
|
||||
|
||||
# Context Window Usage
|
||||
|
||||
105,000 / 200,000 tokens (53%)
|
||||
Model: anthropic/claude-sonnet-4 (200K context window)
|
||||
\`\`\`
|
||||
|
||||
**IMPORTANT**: When you see context window usage at or above 50%, you MUST:
|
||||
|
||||
1. Complete your current logical step
|
||||
2. Use the `ask_followup_question` tool to offer creating a new task
|
||||
3. If approved, use the `new_task` tool with comprehensive handoff instructions
|
||||
|
||||
## Task Breakdown in Plan Mode - REQUIRED PROCESS
|
||||
|
||||
Plan Mode is specifically designed for analyzing complex tasks and breaking them into manageable subtasks. When in Plan Mode, you **MUST**:
|
||||
|
||||
### 1. Initial Task Analysis - REQUIRED
|
||||
|
||||
- **MUST** begin by thoroughly understanding the full scope of the user's request
|
||||
- **MUST** identify all major components and dependencies of the task
|
||||
- **MUST** consider potential challenges, edge cases, and prerequisites
|
||||
|
||||
### 2. Strategic Task Decomposition - REQUIRED
|
||||
|
||||
- **MUST** break the overall task into logical, discrete subtasks
|
||||
- **MUST** prioritize subtasks based on dependencies (what must be completed first)
|
||||
- **MUST** aim for subtasks that can be completed within a single session (15-30 minutes of work)
|
||||
- **MUST** consider natural breaking points where context switching makes sense
|
||||
|
||||
### 3. Creating a Task Roadmap - REQUIRED
|
||||
|
||||
- **MUST** present a clear, numbered list of subtasks to the user
|
||||
- **MUST** explain dependencies between subtasks
|
||||
- **MUST** provide time estimates for each subtask when possible
|
||||
- **MUST** use Mermaid diagrams to visualize task flow and dependencies when helpful
|
||||
|
||||
\`\`\`mermaid
|
||||
graph TD
|
||||
A[Main Task] --> B[Subtask 1: Setup]
|
||||
A --> C[Subtask 2: Core Implementation]
|
||||
A --> D[Subtask 3: Testing]
|
||||
A --> E[Subtask 4: Documentation]
|
||||
B --> C
|
||||
C --> D
|
||||
\`\`\`
|
||||
|
||||
### 4. Getting User Approval - REQUIRED
|
||||
|
||||
- **MUST** ask for user feedback on the proposed task breakdown
|
||||
- **MUST** adjust the plan based on user priorities or additional requirements
|
||||
- **MUST** confirm which subtask to begin with
|
||||
- **MUST** request the user to toggle to Act Mode when ready to implement
|
||||
|
||||
## Task Implementation and Handoff Process - MANDATORY PROCEDURES
|
||||
|
||||
When implementing tasks in Act Mode, you **MUST** follow these guidelines for effective task handoff:
|
||||
|
||||
### 1. Focused Implementation - REQUIRED
|
||||
|
||||
- **MUST** focus on completing the current subtask fully
|
||||
- **MUST** document progress clearly through comments and commit messages
|
||||
- **MUST** create checkpoints at logical completion points
|
||||
|
||||
### 2. Recognizing Completion Points - CRITICAL
|
||||
|
||||
You **MUST** identify natural handoff points when:
|
||||
|
||||
- The current subtask is fully completed
|
||||
- You've reached a logical stopping point in a larger subtask
|
||||
- The implementation is taking longer than expected and can be continued later
|
||||
- The task scope has expanded beyond the original plan
|
||||
- **CRITICAL**: The context window usage exceeds 50% (e.g., 100,000+ tokens for a 200K context window)
|
||||
|
||||
### 3. Initiating the Handoff Process - MANDATORY ACTION
|
||||
|
||||
When you've reached a completion point, you **MUST**:
|
||||
|
||||
1. Summarize what has been accomplished so far
|
||||
2. Clearly state what remains to be done
|
||||
3. **MANDATORY**: Use the `ask_followup_question` tool to offer creating a new task:
|
||||
|
||||
\`\`\`xml
|
||||
<ask_followup_question>
|
||||
<question>I've completed [specific accomplishment]. Would you like me to create a new task to continue with [remaining work]?</question>
|
||||
<options>["Yes, create a new task", "No, continue in this session", "Let me think about it"]</options>
|
||||
</ask_followup_question>
|
||||
\`\`\`
|
||||
|
||||
### 4. Creating a New Task with Context - REQUIRED ACTION
|
||||
|
||||
If the user agrees to create a new task, you **MUST** use the `new_task` tool with comprehensive handoff instructions:
|
||||
|
||||
\`\`\`xml
|
||||
<new_task>
|
||||
<context>
|
||||
|
||||
# Task Continuation: [Brief Task Title]
|
||||
|
||||
## Completed Work
|
||||
|
||||
- [Detailed list of completed items]
|
||||
- [Include specific files modified/created]
|
||||
- [Note any important decisions made]
|
||||
|
||||
## Current State
|
||||
|
||||
- [Description of the current state of the project]
|
||||
- [Any running processes or environment setup]
|
||||
- [Key files and their current state]
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Detailed list of remaining tasks]
|
||||
- [Specific implementation details to address]
|
||||
- [Any known challenges to be aware of]
|
||||
|
||||
## Reference Information
|
||||
|
||||
- [Links to relevant documentation]
|
||||
- [Important code snippets or patterns to follow]
|
||||
- [Any user preferences noted during the current session]
|
||||
|
||||
Please continue the implementation by [specific next action].
|
||||
</context>
|
||||
</new_task>
|
||||
\`\`\`
|
||||
|
||||
### 5. Detailed Context Transfer - MANDATORY COMPONENTS
|
||||
|
||||
When creating a new task, you **MUST** always include:
|
||||
|
||||
#### Project Context - REQUIRED
|
||||
|
||||
- **MUST** include the overall goal and purpose of the project
|
||||
- **MUST** include key architectural decisions and patterns
|
||||
- **MUST** include technology stack and dependencies
|
||||
|
||||
#### Implementation Details - REQUIRED
|
||||
|
||||
- **MUST** list files created or modified in the current session
|
||||
- **MUST** describe specific functions, classes, or components implemented
|
||||
- **MUST** explain design patterns being followed
|
||||
- **MUST** outline testing approach
|
||||
|
||||
#### Progress Tracking - REQUIRED
|
||||
|
||||
- **MUST** provide checklist of completed items
|
||||
- **MUST** provide checklist of remaining items
|
||||
- **MUST** note any blockers or challenges encountered
|
||||
|
||||
#### User Preferences - REQUIRED
|
||||
|
||||
- **MUST** note coding style preferences mentioned by the user
|
||||
- **MUST** document specific approaches requested by the user
|
||||
- **MUST** highlight priority areas identified by the user
|
||||
|
||||
## Best Practices for Effective Handoffs - MANDATORY GUIDELINES
|
||||
|
||||
### 1. Maintain Continuity - REQUIRED
|
||||
|
||||
- **MUST** use consistent terminology between tasks
|
||||
- **MUST** reference previous decisions and their rationale
|
||||
- **MUST** maintain the same architectural approach unless explicitly changing direction
|
||||
|
||||
### 2. Preserve Context - REQUIRED
|
||||
|
||||
- **MUST** include relevant code snippets in the handoff
|
||||
- **MUST** summarize key discussions from the previous session
|
||||
- **MUST** reference specific files and line numbers when applicable
|
||||
|
||||
### 3. Set Clear Next Actions - REQUIRED
|
||||
|
||||
- **MUST** begin the handoff with a clear, actionable next step
|
||||
- **MUST** prioritize remaining tasks
|
||||
- **MUST** highlight any decisions that need to be made
|
||||
|
||||
### 4. Document Assumptions - REQUIRED
|
||||
|
||||
- **MUST** clearly state any assumptions made during implementation
|
||||
- **MUST** note areas where user input might be needed
|
||||
- **MUST** identify potential alternative approaches
|
||||
|
||||
### 5. Optimize for Resumability - REQUIRED
|
||||
|
||||
- **MUST** structure the handoff so the next session can begin working immediately
|
||||
- **MUST** include setup instructions if environment configuration is needed
|
||||
- **MUST** provide a quick summary at the top for rapid context restoration
|
||||
|
||||
## Example Task Handoff
|
||||
|
||||
### Example #1 of an effective task handoff:
|
||||
|
||||
\`\`\`xml
|
||||
<new_task>
|
||||
<context>
|
||||
|
||||
# Task Continuation: Implement User Authentication System
|
||||
|
||||
## Completed Work
|
||||
|
||||
- Created basic Express.js server structure
|
||||
- Implemented MongoDB connection and user schema
|
||||
- Completed user registration endpoint with password hashing
|
||||
- Added input validation using Joi
|
||||
- Created initial test suite for registration endpoint
|
||||
|
||||
## Current State
|
||||
|
||||
- Server runs successfully on port 3000
|
||||
- MongoDB connection is established
|
||||
- Registration endpoint (/api/users/register) is fully functional
|
||||
- Test suite passes for all registration scenarios
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Implement login endpoint (/api/users/login)
|
||||
- Use bcrypt to compare passwords
|
||||
- Generate JWT token upon successful login
|
||||
- Add proper error handling for invalid credentials
|
||||
2. Create authentication middleware
|
||||
- Verify JWT tokens
|
||||
- Extract user information
|
||||
- Handle expired tokens
|
||||
3. Add protected routes that require authentication
|
||||
4. Implement password reset functionality
|
||||
|
||||
## Reference Information
|
||||
|
||||
- JWT secret should be stored in .env file
|
||||
- Follow the existing error handling pattern in routes/users.js
|
||||
- User schema is defined in models/User.js
|
||||
- Test patterns are established in tests/auth.test.js
|
||||
|
||||
Please continue by implementing the login endpoint following the same patterns established in the registration endpoint.
|
||||
</context>
|
||||
</new_task>
|
||||
\`\`\`
|
||||
|
||||
### Example #2 of an ineffective task handoff:
|
||||
|
||||
_(Note: The example provided in the original rules showing "YOLO MODE Implementation" seems less like a direct handoff context block and more like a general status update with future considerations. A true ineffective handoff might lack detail in 'Current State' or 'Next Steps')._
|
||||
|
||||
## When to Use Task Handoffs - MANDATORY TRIGGERS
|
||||
|
||||
You **MUST** initiate task handoffs in these scenarios:
|
||||
|
||||
1. **CRITICAL**: When context window usage exceeds 50% (e.g., 100,000+ tokens for a 200K context window)
|
||||
2. **Long-running projects** that exceed a single session
|
||||
3. **Complex implementations** with multiple distinct phases
|
||||
4. **When context window limitations** are approaching
|
||||
5. **When switching focus areas** within a larger project
|
||||
6. **When different expertise** might be beneficial for different parts of the task
|
||||
|
||||
**⚠️ FINAL REMINDER - CRITICAL INSTRUCTION ⚠️**
|
||||
|
||||
You **MUST** monitor the context window usage in the environment details section. When it exceeds 50% (e.g., "105,000 / 200,000 tokens (53%)"), you **MUST** proactively initiate the task handoff process using the `ask_followup_question` tool followed by the `new_task` tool. You MUST use the `new_task` tool.
|
||||
|
||||
By strictly following these guidelines, you'll ensure smooth transitions between tasks, maintain project momentum, and provide the best possible experience for users working on complex, multi-session projects.
|
||||
|
||||
```markdown
|
||||
## User Interaction & Workflow Considerations
|
||||
|
||||
- **Linear Flow:** Currently, using `new_task` creates a linear sequence. The old task ends, and the new one begins. The old task history remains accessible for backtracking.
|
||||
- **User Approval:** You always have control, approving the handoff and having the chance to modify the context Cline proposes to carry forward.
|
||||
- **Flexibility:** The core `new_task` tool is a flexible building block. Experiment with `.clinerules` to create workflows that best suit your needs, whether for strict context management, task decomposition, or other creative uses.
|
||||
```
|
||||
137
.clinerules/localrules.md
Normal file
137
.clinerules/localrules.md
Normal file
@@ -0,0 +1,137 @@
|
||||
- when developing go code use the standard go tools
|
||||
- always execute commands from the root of the project
|
||||
- in ACT mode if you hit 5 errors in a row return to plan mode
|
||||
- Both backend and frontend applications must read from the root `.env` file
|
||||
- always prefer small targeted changds over big changes
|
||||
- use the python implementation a core reference before making changes. That version works and will provide insights for solving issues.
|
||||
|
||||
**Testing Requirements**
|
||||
- **MANDATORY**: Run all tests in containerized environments
|
||||
- Follow testing framework conventions (pytest for Python, Jest for React)
|
||||
- Include unit, integration, and end-to-end tests
|
||||
- Test data should be minimal and focused
|
||||
- Separate test types into different directories
|
||||
|
||||
|
||||
**TESTING**
|
||||
- when asked to resolve errors:
|
||||
- iterate until all builds complete with no errors
|
||||
- the commmand should be run from the root of the project: go build ./...
|
||||
|
||||
|
||||
## Go Formatting
|
||||
|
||||
When work is done the following shouldbe run
|
||||
|
||||
- go mod tidy
|
||||
- go fmt ./...
|
||||
- go vet ./...
|
||||
- go test ./...
|
||||
|
||||
|
||||
|
||||
**Rule 13: Database Configuration**
|
||||
- Use environment variables for database configuration
|
||||
- Implement proper connection pooling
|
||||
- Follow database naming conventions
|
||||
- Mount database data as Docker volumes for persistence
|
||||
|
||||
|
||||
**Project Structure**:
|
||||
- **MANDATORY**: All new code must follow the standardized project structure
|
||||
- **MANDATORY**: Core backend logic only in `/src/` directory
|
||||
- **MANDATORY**: Frontend code only in `/frontend/` directory
|
||||
- **MANDATORY**: All Docker files in `/docker/` directory
|
||||
|
||||
**Package Management**:
|
||||
- **MANDATORY**: Use UV for Python package management
|
||||
- **MANDATORY**: Use pnpm for React package management
|
||||
- **MANDATORY**: Dependencies declared in appropriate configuration files
|
||||
|
||||
|
||||
**Code Quality**:
|
||||
- **MANDATORY**: Run linting before building
|
||||
- **MANDATORY**: Resolve all errors before deployment
|
||||
- **MANDATORY**: Follow language-specific coding standards
|
||||
|
||||
**Project Organization**:
|
||||
- **FORBIDDEN**: Data files committed to git
|
||||
- **FORBIDDEN**: Configuration secrets in code
|
||||
- **FORBIDDEN**: Environment variables in subdirectories
|
||||
|
||||
|
||||
**Pre-Deployment Checklist**:
|
||||
1. All tests passing
|
||||
2. Linting and type checking completed
|
||||
3. Environment variables properly configured
|
||||
4. Database migrations applied
|
||||
5. Security scan completed
|
||||
|
||||
|
||||
**Do at every iteration**
|
||||
- check the rules
|
||||
- when you finish a task or sub-task update the todo list
|
||||
|
||||
|
||||
|
||||
|
||||
# GoLang Rules
|
||||
|
||||
## Interface and Mock Management Rules
|
||||
|
||||
- Always implement interfaces with concrete types - Never use anonymous structs with function fields to satisfy interfaces
|
||||
- Create shared mock implementations - Don't duplicate mock types across test files; use a central test_helpers.go file
|
||||
- One mock per interface - Each interface should have exactly one shared mock implementation for consistency
|
||||
- Use factory functions for mocks - Provide NewMockXXX() functions to create mock instances with sensible defaults
|
||||
- Make mocks configurable when needed - Use function fields or configuration structs for tests requiring custom behavior
|
||||
|
||||
## Test Organization Rules
|
||||
|
||||
- Centralize test utilities - Keep all shared test helpers, mocks, and utilities in test_helpers.go or similar
|
||||
- Follow consistent patterns - Use the same approach for creating clients, sessions, and mocks across all test files
|
||||
- Don't pass nil for required interfaces - Always provide a valid mock implementation unless specifically testing nil handling
|
||||
- Use descriptive mock names - Name mocks clearly (e.g., MockAuthenticator, MockHTTPClient) to indicate their purpose
|
||||
|
||||
## Code Change Management Rules
|
||||
|
||||
- Update all dependent code when changing interfaces - If you modify an interface, check and update ALL implementations and tests
|
||||
- Run full test suite after interface changes - Interface changes can break code in unexpected places
|
||||
- Search codebase for interface usage - Use grep or IDE search to find all references to changed interfaces
|
||||
- Update mocks when adding interface methods - New interface methods must be implemented in all mocks
|
||||
|
||||
## Testing Best Practices Rules
|
||||
|
||||
- Test with realistic mock data - Don't use empty strings or zero values unless specifically testing edge cases
|
||||
- Set proper expiration times - Mock sessions should have future expiration times unless testing expiration
|
||||
- Verify mock behavior is called - Add call tracking to mocks when behavior verification is important
|
||||
- Use table-driven tests for multiple scenarios - Test different mock behaviors using test case structs
|
||||
- Mock external dependencies only - Don't mock your own business logic; mock external services, APIs, and I/O
|
||||
|
||||
## Documentation and Review Rules
|
||||
|
||||
- Document shared test utilities - Add comments explaining when and how to use shared mocks
|
||||
- Include interface changes in code reviews - Flag interface modifications for extra review attention
|
||||
- Update README/docs when adding test patterns - Document new testing patterns for other developers
|
||||
- Add examples for complex mock usage - Show how to use configurable mocks in comments or examples
|
||||
|
||||
## Error Prevention Rules
|
||||
|
||||
- Compile-test interface changes immediately - Run go build after any interface modification
|
||||
- Write interface compliance tests - Add var _ InterfaceName = (*MockType)(nil) to verify mock implements interface
|
||||
- Set up pre-commit hooks - Use tools like golangci-lint to catch interface issues before commit
|
||||
- Review test failures carefully - Interface-related test failures often indicate broader architectural issues
|
||||
|
||||
## Refactoring Safety Rules
|
||||
|
||||
- Create shared mocks before removing duplicates - Implement the centralized solution before removing old code
|
||||
- Update one test file at a time - Incremental changes make it easier to identify and fix issues
|
||||
- Keep backup of working tests - Commit working tests before major refactoring
|
||||
- Test each change independently - Verify each file works after updating it
|
||||
- Use version control effectively - Make small, focused commits that are easy to review and revert
|
||||
|
||||
## Learning Points
|
||||
|
||||
- Interface Implementation: Go interfaces must be implemented by concrete types, not anonymous structs with function fields
|
||||
- Test Organization: Shared test helpers reduce duplication and improve maintainability
|
||||
- Dependency Injection: Using interfaces makes code more testable but requires proper mock implementation
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,6 +4,7 @@
|
||||
# IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
.kilocode/
|
||||
|
||||
#code samples
|
||||
garth-python/
|
||||
|
||||
0
.kilocode/rules-orchestrator/rules.md
Normal file
0
.kilocode/rules-orchestrator/rules.md
Normal file
26
.kilocode/rules/architect-mode-rules.md
Normal file
26
.kilocode/rules/architect-mode-rules.md
Normal file
@@ -0,0 +1,26 @@
|
||||
## Brief overview
|
||||
Ensures Architect mode focuses on planning and design without making direct code edits. Guides collaboration between Architect and Code modes.
|
||||
|
||||
## Mode-specific Instructions
|
||||
- Architect mode must only create implementation plans and documentation
|
||||
- All code/file changes must be delegated to Code mode via switch_mode
|
||||
- Use update_todo_list for task tracking instead of direct implementation
|
||||
|
||||
## Development Workflow
|
||||
- Generate detailed technical specifications in Architect mode
|
||||
- Use read_file and list_code_definition_names for context gathering
|
||||
- Create comprehensive todo lists with update_todo_list
|
||||
- Transition to Code mode via switch_mode for implementation
|
||||
|
||||
## Collaboration Guidelines
|
||||
- Architect and Code modes should maintain separate responsibilities
|
||||
- Architectural plans must include:
|
||||
- File structure diagrams
|
||||
- API specifications
|
||||
- Data flow documentation
|
||||
- Code mode implementations must reference architectural docs
|
||||
|
||||
## Enforcement Rules
|
||||
- Block apply_diff/write_to_file usage in Architect mode
|
||||
- Require switch_mode to Code for any code modifications
|
||||
- Automatic todo list validation against architectural specs
|
||||
45
.kilocode/rules/git-workflow.md
Normal file
45
.kilocode/rules/git-workflow.md
Normal file
@@ -0,0 +1,45 @@
|
||||
## Brief overview
|
||||
Git workflow rules for projects with Git repositories, ensuring secure handling of sensitive data and proper version control practices.
|
||||
|
||||
## Git repository setup
|
||||
- Ensure `.gitignore` exists in the project root when the directory is a Git repository
|
||||
- Verify `.gitignore` includes patterns for sensitive files and data directories
|
||||
- Check that `.git` directory exists to confirm Git repository status
|
||||
|
||||
## Sensitive file protection
|
||||
- Always include `.env` files in `.gitignore` to prevent password/token exposure
|
||||
- Ensure `data/` directories are ignored to prevent data file commits
|
||||
- Add patterns for database files: `*.db`, `*.sqlite`, `*.sqlite3`
|
||||
- Include common sensitive file patterns:
|
||||
- `.env.local`
|
||||
- `.env.production`
|
||||
- `config/secrets.json`
|
||||
- `*.key`
|
||||
- `*.pem`
|
||||
|
||||
## Pre-change workflow
|
||||
- Before starting any new changes, ensure current state is committed
|
||||
- Run `git status` to check for uncommitted changes
|
||||
- Commit all pending changes with descriptive messages
|
||||
- Push commits to remote repository before starting new work
|
||||
- Use `git add . && git commit -m "WIP: save state before [feature/change]"` for work-in-progress saves
|
||||
|
||||
## Post-completion workflow
|
||||
- When a task is finished, commit all changes with meaningful commit messages
|
||||
- Use conventional commit format: `type(scope): description`
|
||||
- Examples:
|
||||
- `feat(auth): add Garmin Connect integration`
|
||||
- `fix(data): handle missing activity files`
|
||||
- `docs(readme): update setup instructions`
|
||||
- Push commits immediately after committing: `git push origin [branch-name]`
|
||||
|
||||
## Branch management
|
||||
- Create feature branches for new work: `git checkout -b feature/description`
|
||||
- Keep main/master branch clean and deployable
|
||||
- Use descriptive branch names that reflect the task
|
||||
- Delete merged branches to keep repository clean
|
||||
|
||||
## Verification steps
|
||||
- After each commit, verify with `git log --oneline -5`
|
||||
- Before pushing, run `git status` to confirm clean working directory
|
||||
- Check remote repository to ensure pushes were successful
|
||||
20
.kilocode/rules/go-garth-rules.md
Normal file
20
.kilocode/rules/go-garth-rules.md
Normal file
@@ -0,0 +1,20 @@
|
||||
## Brief overview
|
||||
Project-specific rules for go-garth development. Provides implementation guidelines with Python reference and credential handling requirements.
|
||||
|
||||
## Project Context
|
||||
- Reference Python implementation in 'ReferenceCode' directory when debugging issues:
|
||||
- `garth/` for core functionality patterns
|
||||
- `python-garminconnect/` for API integration examples
|
||||
|
||||
## Development Workflow
|
||||
- Restrict all file modifications to `go-garth/` directory subtree
|
||||
|
||||
## Security Guidelines
|
||||
- Always store credentials in root `.env` file
|
||||
- Never hardcode credentials in source files
|
||||
- Ensure `.gitignore` includes `.env` to prevent accidental commits
|
||||
|
||||
## Code Standards
|
||||
- Follow Go standard naming conventions (camelCase for variables)
|
||||
- Document public methods with GoDoc-style comments
|
||||
- Write table-driven tests for critical functionality
|
||||
27
.kilocode/rules/memory-bank/architecture.md
Normal file
27
.kilocode/rules/memory-bank/architecture.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# System Architecture
|
||||
|
||||
## Overview
|
||||
The Garth client library follows a layered architecture with clear separation between:
|
||||
- API Client Layer
|
||||
- Data Models Layer
|
||||
- Stats Endpoints Layer
|
||||
- Utilities Layer
|
||||
|
||||
## Key Components
|
||||
- **Client**: Handles authentication, session management, and HTTP requests
|
||||
- **Data Models**: Represent Garmin API responses (e.g., DailyBodyBatteryStress)
|
||||
- **Stats**: Implement endpoints for different metrics (steps, stress, etc.)
|
||||
- **Test Utilities**: Mock implementations for testing
|
||||
|
||||
## Data Flow
|
||||
1. User authenticates via `client.Login()`
|
||||
2. Session tokens are stored in `Client` struct
|
||||
3. Stats requests are made through `stats.Stats.List()` method
|
||||
4. Responses are parsed into data models
|
||||
5. Sessions can be saved/loaded using `SaveSession()`/`LoadSession()`
|
||||
|
||||
## Critical Paths
|
||||
- Authentication: `garth/sso/sso.go`
|
||||
- API Requests: `garth/client/client.go`
|
||||
- Stats Implementation: `garth/stats/base.go`
|
||||
- Data Models: `garth/types/types.go`
|
||||
19
.kilocode/rules/memory-bank/brief.md
Normal file
19
.kilocode/rules/memory-bank/brief.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Project Brief
|
||||
|
||||
**Project Name**: Garth - Garmin Connect API Client
|
||||
**Core Functionality**:
|
||||
- Authentication with Garmin Connect
|
||||
- Retrieval of health/fitness data (steps, stress, hydration, sleep, HRV)
|
||||
- Session management
|
||||
- Error handling
|
||||
|
||||
**Scope**:
|
||||
- Go client library for Garmin Connect API
|
||||
- Support for daily and weekly stats
|
||||
- Session persistence
|
||||
- Mock client for testing
|
||||
|
||||
**Out of Scope**:
|
||||
- User interface
|
||||
- Data storage/visualization
|
||||
- Third-party integrations
|
||||
97
.kilocode/rules/memory-bank/coding-style.md
Normal file
97
.kilocode/rules/memory-bank/coding-style.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Go Coding Style Guide
|
||||
|
||||
## Code Organization & Structure
|
||||
|
||||
**Logical Segmentation**
|
||||
- One concept per file (e.g., handlers, models, services).
|
||||
- Keep functions under 20-30 lines when possible.
|
||||
- Use meaningful package names that reflect functionality.
|
||||
- Group related types and functions together.
|
||||
|
||||
**Interface Design**
|
||||
- Prefer small, focused interfaces (1-3 methods).
|
||||
- Define interfaces where they're used, not where they're implemented.
|
||||
- Use composition over inheritance.
|
||||
|
||||
## Conciseness Rules
|
||||
|
||||
**Variable & Function Naming**
|
||||
- Use short names in small scopes (`i`, `err`, `ctx`).
|
||||
- Longer names for broader scopes (`userRepository`, `configManager`).
|
||||
- Omit obvious type information (`users []User` not `userList []User`).
|
||||
|
||||
**Error Handling**
|
||||
```go
|
||||
// Prefer early returns
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// over nested if-else blocks
|
||||
```
|
||||
|
||||
**Type Declarations**
|
||||
- Use type inference: `users := []User{}` not `var users []User = []User{}`.
|
||||
- Combine related variable declarations: `var (name string; age int)`.
|
||||
|
||||
## Go Idioms for Conciseness
|
||||
|
||||
**Zero Values**
|
||||
- Leverage Go's zero values instead of explicit initialization.
|
||||
- Use `var buf bytes.Buffer` instead of `buf := bytes.Buffer{}`.
|
||||
|
||||
**Struct Initialization**
|
||||
```go
|
||||
// Prefer struct literals with field names
|
||||
user := User{Name: name, Email: email}
|
||||
// over multiple assignments
|
||||
```
|
||||
|
||||
**Method Receivers**
|
||||
- Use pointer receivers for modification or large structs.
|
||||
- Use value receivers for small, immutable data.
|
||||
|
||||
**Channel Operations**
|
||||
- Use `select` with `default` for non-blocking operations.
|
||||
- Prefer `range` over explicit channel reads.
|
||||
|
||||
## Context Reduction Strategies
|
||||
|
||||
**Function Signatures**
|
||||
- Group related parameters into structs.
|
||||
- Use functional options pattern for complex configurations.
|
||||
- Return early and often to reduce nesting.
|
||||
|
||||
**Constants and Enums**
|
||||
```go
|
||||
const (
|
||||
StatusPending = iota
|
||||
StatusApproved
|
||||
StatusRejected
|
||||
)
|
||||
```
|
||||
|
||||
**Embed Common Patterns**
|
||||
- Use `sync.Once` for lazy initialization.
|
||||
- Embed `sync.Mutex` in structs needing synchronization.
|
||||
- Use `context.Context` as first parameter in functions.
|
||||
|
||||
## File Organization Rules
|
||||
|
||||
**Package Structure**
|
||||
```
|
||||
/cmd - main applications
|
||||
/internal - private code
|
||||
/pkg - public libraries
|
||||
/api - API definitions
|
||||
```
|
||||
|
||||
**Import Grouping**
|
||||
1. Standard library
|
||||
2. Third-party packages
|
||||
3. Local packages
|
||||
(Separate groups with blank lines)
|
||||
|
||||
**Testing**
|
||||
- Place tests in same package with `_test.go` suffix.
|
||||
- Use table-driven tests for multiple scenarios.
|
||||
- Keep test functions focused and named clearly.
|
||||
18
.kilocode/rules/memory-bank/context.md
Normal file
18
.kilocode/rules/memory-bank/context.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Project Context
|
||||
|
||||
## Current Work Focus
|
||||
- Fixing syntax errors and test failures
|
||||
- Implementing mock client for testing
|
||||
- Refactoring HTTP client interfaces
|
||||
- Improving test coverage
|
||||
|
||||
## Recent Changes
|
||||
- Fixed syntax error in [`garth/integration_test.go`](garth/integration_test.go:168) by removing extra closing brace
|
||||
- Created [`garth/testutils/mock_client.go`](garth/testutils/mock_client.go) for simulating API failures
|
||||
- Added [`garth/client/http_client.go`](garth/client/http_client.go) to define HTTPClient interface
|
||||
|
||||
## Next Steps
|
||||
- Complete mock client implementation
|
||||
- Update stats package to use HTTPClient interface
|
||||
- Fix remaining test failures
|
||||
- Add more integration tests for data endpoints
|
||||
27
.kilocode/rules/memory-bank/product.md
Normal file
27
.kilocode/rules/memory-bank/product.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Product Description
|
||||
|
||||
## Why this project exists
|
||||
Garth provides a Go client library for interacting with Garmin Connect API, enabling developers to:
|
||||
- Access personal fitness data programmatically
|
||||
- Build custom health tracking applications
|
||||
- Integrate Garmin data with other systems
|
||||
|
||||
## Problems it solves
|
||||
- Simplifies authentication with Garmin's complex SSO system
|
||||
- Provides structured access to health metrics (steps, sleep, stress, etc.)
|
||||
- Handles session persistence and token management
|
||||
- Abstracts API complexities behind a clean Go interface
|
||||
|
||||
## How it should work
|
||||
1. Users authenticate with their Garmin credentials
|
||||
2. The client manages session tokens automatically
|
||||
3. Developers can request daily/weekly metrics through simple method calls
|
||||
4. Data is returned as structured Go types
|
||||
5. Sessions can be saved/loaded for persistent access
|
||||
|
||||
## User Experience Goals
|
||||
- Minimal setup required
|
||||
- Intuitive method names matching Garmin's metrics
|
||||
- Clear error handling and documentation
|
||||
- Support for both individual data points and bulk retrieval
|
||||
- Mock client for testing without live API calls
|
||||
27
.kilocode/rules/memory-bank/tech.md
Normal file
27
.kilocode/rules/memory-bank/tech.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Technologies and Development Setup
|
||||
|
||||
## Core Technologies
|
||||
- **Language**: Go (Golang)
|
||||
- **Package Manager**: Go Modules (go.mod)
|
||||
- **HTTP Client**: net/http with custom enhancements
|
||||
- **Testing**: Standard testing package
|
||||
|
||||
## Development Setup
|
||||
1. Install Go (version 1.20+)
|
||||
2. Clone the repository
|
||||
3. Run `go mod tidy` to install dependencies
|
||||
4. Set up Garmin credentials in environment variables or session file
|
||||
|
||||
## Technical Constraints
|
||||
- Requires Garmin Connect account for integration tests
|
||||
- Limited to Garmin's public API endpoints
|
||||
- No official SDK support from Garmin
|
||||
|
||||
## Dependencies
|
||||
- Standard library packages only
|
||||
- No external dependencies beyond Go standard library
|
||||
|
||||
## Tool Usage Patterns
|
||||
- Use `go test` for running unit tests
|
||||
- Use `go test -short` to skip integration tests
|
||||
- Use `go build` to build the package
|
||||
167
.kilocode/rules/memorybank.md
Normal file
167
.kilocode/rules/memorybank.md
Normal file
@@ -0,0 +1,167 @@
|
||||
# Memory Bank
|
||||
|
||||
I am an expert software engineer with a unique characteristic: my memory resets completely between sessions. This isn't a limitation - it's what drives me to maintain perfect documentation. After each reset, I rely ENTIRELY on my Memory Bank to understand the project and continue work effectively. I MUST read ALL memory bank files at the start of EVERY task - this is not optional. The memory bank files are located in `.kilocode/rules/memory-bank` folder.
|
||||
|
||||
When I start a task, I will include `[Memory Bank: Active]` at the beginning of my response if I successfully read the memory bank files, or `[Memory Bank: Missing]` if the folder doesn't exist or is empty. If memory bank is missing, I will warn the user about potential issues and suggest initialization.
|
||||
|
||||
## Memory Bank Structure
|
||||
|
||||
The Memory Bank consists of core files and optional context files, all in Markdown format.
|
||||
|
||||
### Core Files (Required)
|
||||
1. `brief.md`
|
||||
This file is created and maintained manually by the developer. Don't edit this file directly but suggest to user to update it if it can be improved.
|
||||
- Foundation document that shapes all other files
|
||||
- Created at project start if it doesn't exist
|
||||
- Defines core requirements and goals
|
||||
- Source of truth for project scope
|
||||
|
||||
2. `product.md`
|
||||
- Why this project exists
|
||||
- Problems it solves
|
||||
- How it should work
|
||||
- User experience goals
|
||||
|
||||
3. `context.md`
|
||||
This file should be short and factual, not creative or speculative.
|
||||
- Current work focus
|
||||
- Recent changes
|
||||
- Next steps
|
||||
|
||||
4. `architecture.md`
|
||||
- System architecture
|
||||
- Source Code paths
|
||||
- Key technical decisions
|
||||
- Design patterns in use
|
||||
- Component relationships
|
||||
- Critical implementation paths
|
||||
|
||||
5. `tech.md`
|
||||
- Technologies used
|
||||
- Development setup
|
||||
- Technical constraints
|
||||
- Dependencies
|
||||
- Tool usage patterns
|
||||
|
||||
### Additional Files
|
||||
Create additional files/folders within memory-bank/ when they help organize:
|
||||
- `tasks.md` - Documentation of repetitive tasks and their workflows
|
||||
- Complex feature documentation
|
||||
- Integration specifications
|
||||
- API documentation
|
||||
- Testing strategies
|
||||
- Deployment procedures
|
||||
|
||||
## Core workflows
|
||||
|
||||
### Memory Bank Initialization
|
||||
|
||||
The initialization step is CRITICALLY IMPORTANT and must be done with extreme thoroughness as it defines all future effectiveness of the Memory Bank. This is the foundation upon which all future interactions will be built.
|
||||
|
||||
When user requests initialization of the memory bank (command `initialize memory bank`), I'll perform an exhaustive analysis of the project, including:
|
||||
- All source code files and their relationships
|
||||
- Configuration files and build system setup
|
||||
- Project structure and organization patterns
|
||||
- Documentation and comments
|
||||
- Dependencies and external integrations
|
||||
- Testing frameworks and patterns
|
||||
|
||||
I must be extremely thorough during initialization, spending extra time and effort to build a comprehensive understanding of the project. A high-quality initialization will dramatically improve all future interactions, while a rushed or incomplete initialization will permanently limit my effectiveness.
|
||||
|
||||
After initialization, I will ask the user to read through the memory bank files and verify product description, used technologies and other information. I should provide a summary of what I've understood about the project to help the user verify the accuracy of the memory bank files. I should encourage the user to correct any misunderstandings or add missing information, as this will significantly improve future interactions.
|
||||
|
||||
### Memory Bank Update
|
||||
|
||||
Memory Bank updates occur when:
|
||||
1. Discovering new project patterns
|
||||
2. After implementing significant changes
|
||||
3. When user explicitly requests with the phrase **update memory bank** (MUST review ALL files)
|
||||
4. When context needs clarification
|
||||
|
||||
If I notice significant changes that should be preserved but the user hasn't explicitly requested an update, I should suggest: "Would you like me to update the memory bank to reflect these changes?"
|
||||
|
||||
To execute Memory Bank update, I will:
|
||||
|
||||
1. Review ALL project files
|
||||
2. Document current state
|
||||
3. Document Insights & Patterns
|
||||
4. If requested with additional context (e.g., "update memory bank using information from @/Makefile"), focus special attention on that source
|
||||
|
||||
Note: When triggered by **update memory bank**, I MUST review every memory bank file, even if some don't require updates. Focus particularly on context.md as it tracks current state.
|
||||
|
||||
### Add Task
|
||||
|
||||
When user completes a repetitive task (like adding support for a new model version) and wants to document it for future reference, they can request: **add task** or **store this as a task**.
|
||||
|
||||
This workflow is designed for repetitive tasks that follow similar patterns and require editing the same files. Examples include:
|
||||
- Adding support for new AI model versions
|
||||
- Implementing new API endpoints following established patterns
|
||||
- Adding new features that follow existing architecture
|
||||
|
||||
Tasks are stored in the file `tasks.md` in the memory bank folder. The file is optional and can be empty. The file can store many tasks.
|
||||
|
||||
To execute Add Task workflow:
|
||||
|
||||
1. Create or update `tasks.md` in the memory bank folder
|
||||
2. Document the task with:
|
||||
- Task name and description
|
||||
- Files that need to be modified
|
||||
- Step-by-step workflow followed
|
||||
- Important considerations or gotchas
|
||||
- Example of the completed implementation
|
||||
3. Include any context that was discovered during task execution but wasn't previously documented
|
||||
|
||||
Example task entry:
|
||||
```markdown
|
||||
## Add New Model Support
|
||||
**Last performed:** [date]
|
||||
**Files to modify:**
|
||||
- `/providers/gemini.md` - Add model to documentation
|
||||
- `/src/providers/gemini-config.ts` - Add model configuration
|
||||
- `/src/constants/models.ts` - Add to model list
|
||||
- `/tests/providers/gemini.test.ts` - Add test cases
|
||||
|
||||
**Steps:**
|
||||
1. Add model configuration with proper token limits
|
||||
2. Update documentation with model capabilities
|
||||
3. Add to constants file for UI display
|
||||
4. Write tests for new model configuration
|
||||
|
||||
**Important notes:**
|
||||
- Check Google's documentation for exact token limits
|
||||
- Ensure backward compatibility with existing configurations
|
||||
- Test with actual API calls before committing
|
||||
```
|
||||
|
||||
### Regular Task Execution
|
||||
|
||||
In the beginning of EVERY task I MUST read ALL memory bank files - this is not optional.
|
||||
|
||||
The memory bank files are located in `.kilocode/rules/memory-bank` folder. If the folder doesn't exist or is empty, I will warn user about potential issues with the memory bank. I will include `[Memory Bank: Active]` at the beginning of my response if I successfully read the memory bank files, or `[Memory Bank: Missing]` if the folder doesn't exist or is empty. If memory bank is missing, I will warn the user about potential issues and suggest initialization. I should briefly summarize my understanding of the project to confirm alignment with the user's expectations, like:
|
||||
|
||||
"[Memory Bank: Active] I understand we're building a React inventory system with barcode scanning. Currently implementing the scanner component that needs to work with the backend API."
|
||||
|
||||
When starting a task that matches a documented task in `tasks.md`, I should mention this and follow the documented workflow to ensure no steps are missed.
|
||||
|
||||
If the task was repetitive and might be needed again, I should suggest: "Would you like me to add this task to the memory bank for future reference?"
|
||||
|
||||
In the end of the task, when it seems to be completed, I will update `context.md` accordingly. If the change seems significant, I will suggest to the user: "Would you like me to update memory bank to reflect these changes?" I will not suggest updates for minor changes.
|
||||
|
||||
## Context Window Management
|
||||
|
||||
When the context window fills up during an extended session:
|
||||
1. I should suggest updating the memory bank to preserve the current state
|
||||
2. Recommend starting a fresh conversation/task
|
||||
3. In the new conversation, I will automatically load the memory bank files to maintain continuity
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
Memory Bank is built on Kilo Code's Custom Rules feature, with files stored as standard markdown documents that both the user and I can access.
|
||||
|
||||
## Important Notes
|
||||
|
||||
REMEMBER: After every memory reset, I begin completely fresh. The Memory Bank is my only link to previous work. It must be maintained with precision and clarity, as my effectiveness depends entirely on its accuracy.
|
||||
|
||||
If I detect inconsistencies between memory bank files, I should prioritize brief.md and note any discrepancies to the user.
|
||||
|
||||
IMPORTANT: I MUST read ALL memory bank files at the start of EVERY task - this is not optional. The memory bank files are located in `.kilocode/rules/memory-bank` folder.
|
||||
34
.kilocodemodes
Normal file
34
.kilocodemodes
Normal file
@@ -0,0 +1,34 @@
|
||||
customModes:
|
||||
- slug: ochestrator-2
|
||||
name: Ochestrator 2
|
||||
roleDefinition: ggg
|
||||
groups:
|
||||
- read
|
||||
- edit
|
||||
- browser
|
||||
- command
|
||||
- mcp
|
||||
source: project
|
||||
- slug: orchestrator
|
||||
name: Orchestrator
|
||||
roleDefinition: You are Kilo Code, a strategic workflow orchestrator who coordinates complex tasks by delegating them to appropriate specialized modes. You have a comprehensive understanding of each mode's capabilities and limitations, allowing you to effectively break down complex problems into discrete tasks that can be solved by different specialists.
|
||||
whenToUse: Use this mode for complex, multi-step projects that require coordination across different specialties. Ideal when you need to break down large tasks into subtasks, manage workflows, or coordinate work that spans multiple domains or expertise areas.
|
||||
description: Coordinate tasks across multiple modes
|
||||
customInstructions: |-
|
||||
Your role is to coordinate complex workflows by delegating tasks to specialized modes. As an orchestrator, you should:
|
||||
1. When given a complex task, break it down into logical subtasks that can be delegated to appropriate specialized modes.
|
||||
2. For each subtask, use the `new_task` tool to delegate. Choose the most appropriate mode for the subtask's specific goal and provide comprehensive instructions in the `message` parameter. You should use 'code' mode unless you are extremely certain another mode is the right choice. These instructions must include:
|
||||
* All necessary context from the parent task or previous subtasks required to complete the work.
|
||||
* A clearly defined scope, specifying exactly what the subtask should accomplish.
|
||||
* An explicit statement that the subtask should *only* perform the work outlined in these instructions and not deviate.
|
||||
* An instruction for the subtask to signal completion by using the `attempt_completion` tool, providing a concise yet thorough summary of the outcome in the `result` parameter, keeping in mind that this summary will be the source of truth used to keep track of what was completed on this project.
|
||||
* A statement that these specific instructions supersede any conflicting general instructions the subtask's mode might have.
|
||||
3. Track and manage the progress of all subtasks. When a subtask is completed, analyze its results and determine the next steps.
|
||||
5. When all subtasks are completed, synthesize the results and provide a comprehensive overview of what was accomplished.
|
||||
6. Ask clarifying questions often to better understand how to break down complex tasks effectively.
|
||||
7. Suggest improvements to the workflow based on the results of completed subtasks.
|
||||
Extensively and liberally use subtasks to maintain clarity. If a request significantly shifts focus or requires a different expertise (mode), you MUST create a subtask rather than overloading the current one.
|
||||
A plan with fewer than 10 tasks indicates insufficient consideration. Reconsider until you have at least ten, minimally viable tasks.
|
||||
groups: []
|
||||
source: project
|
||||
iconName: codicon-run-all
|
||||
1057
GarminEndpoints.md
1057
GarminEndpoints.md
File diff suppressed because it is too large
Load Diff
116
README.md
116
README.md
@@ -1,116 +0,0 @@
|
||||
# Garmin Connect Go Client
|
||||
|
||||
Go port of the Garth Python library for accessing Garmin Connect data. Provides full API coverage with improved performance and type safety.
|
||||
|
||||
## Installation
|
||||
```bash
|
||||
go get github.com/sstent/garmin-connect/garth
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"garmin-connect/garth"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create client and authenticate
|
||||
client, err := garth.NewClient("garmin.com")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = client.Login("your@email.com", "password")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Get yesterday's body battery data
|
||||
yesterday := time.Now().AddDate(0, 0, -1)
|
||||
bb, err := garth.BodyBatteryData{}.Get(yesterday, client)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if bb != nil {
|
||||
fmt.Printf("Body Battery: %d\n", bb.BodyBatteryValue)
|
||||
}
|
||||
|
||||
// Get weekly steps
|
||||
steps := garth.NewDailySteps()
|
||||
stepData, err := steps.List(time.Now(), 7, client)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, s := range stepData {
|
||||
fmt.Printf("%s: %d steps\n",
|
||||
s.(garth.DailySteps).CalendarDate.Format("2006-01-02"),
|
||||
*s.(garth.DailySteps).TotalSteps)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Data Types
|
||||
Available data types with Get() methods:
|
||||
- `BodyBatteryData`
|
||||
- `HRVData`
|
||||
- `SleepData`
|
||||
- `WeightData`
|
||||
|
||||
## Stats Types
|
||||
Available stats with List() methods:
|
||||
|
||||
### Daily Stats
|
||||
- `DailySteps`
|
||||
- `DailyStress`
|
||||
- `DailyHRV`
|
||||
- `DailyHydration`
|
||||
- `DailyIntensityMinutes`
|
||||
- `DailySleep`
|
||||
|
||||
### Weekly Stats
|
||||
- `WeeklySteps`
|
||||
- `WeeklyStress`
|
||||
- `WeeklyHRV`
|
||||
|
||||
## Error Handling
|
||||
All methods return errors implementing:
|
||||
```go
|
||||
type GarthError interface {
|
||||
error
|
||||
Message() string
|
||||
Cause() error
|
||||
}
|
||||
```
|
||||
|
||||
Specific error types:
|
||||
- `APIError` - HTTP/API failures
|
||||
- `IOError` - File/network issues
|
||||
- `AuthError` - Authentication failures
|
||||
|
||||
## Performance
|
||||
Benchmarks show 3-5x speed improvement over Python implementation for bulk data operations:
|
||||
|
||||
```
|
||||
BenchmarkBodyBatteryGet-8 100000 10452 ns/op
|
||||
BenchmarkSleepList-8 50000 35124 ns/op (7 days)
|
||||
```
|
||||
|
||||
## Documentation
|
||||
Full API docs: [https://pkg.go.dev/garmin-connect/garth](https://pkg.go.dev/garmin-connect/garth)
|
||||
|
||||
## CLI Tool
|
||||
Includes `cmd/garth` CLI for data export. Supports both daily and weekly stats:
|
||||
|
||||
```bash
|
||||
# Daily steps
|
||||
go run cmd/garth/main.go --data steps --period daily --start 2023-01-01 --end 2023-01-07
|
||||
|
||||
# Weekly stress
|
||||
go run cmd/garth/main.go --data stress --period weekly --start 2023-01-01 --end 2023-01-28
|
||||
```
|
||||
15
cmd/garth/cmd/activities/activities.go
Normal file
15
cmd/garth/cmd/activities/activities.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package activities
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var ActivitiesCmd = &cobra.Command{
|
||||
Use: "activities",
|
||||
Short: "Manage activities from Garmin Connect",
|
||||
Long: `Commands for listing, downloading, and managing activities from Garmin Connect`,
|
||||
}
|
||||
|
||||
func init() {
|
||||
ActivitiesCmd.AddCommand(ListCmd)
|
||||
}
|
||||
22
cmd/garth/cmd/activities/client.go
Normal file
22
cmd/garth/cmd/activities/client.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package activities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/sstent/go-garth/garth/client"
|
||||
"github.com/sstent/go-garth/garth/types"
|
||||
)
|
||||
|
||||
type ActivitiesClient interface {
|
||||
GetActivities(start, end time.Time) ([]types.Activity, error)
|
||||
Login(email, password string) error
|
||||
SaveSession(filename string) error
|
||||
}
|
||||
|
||||
var newClient func(domain string) (ActivitiesClient, error) = func(domain string) (ActivitiesClient, error) {
|
||||
return client.NewClient(domain)
|
||||
}
|
||||
|
||||
func SetClient(f func(domain string) (ActivitiesClient, error)) {
|
||||
newClient = f
|
||||
}
|
||||
131
cmd/garth/cmd/activities/list.go
Normal file
131
cmd/garth/cmd/activities/list.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package activities
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/sstent/go-garth/garth/types"
|
||||
)
|
||||
|
||||
var (
|
||||
outputFormat string
|
||||
startDate string
|
||||
endDate string
|
||||
)
|
||||
|
||||
var ListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List activities from Garmin Connect",
|
||||
Long: `List activities with filtering by date range and multiple output formats`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newClient("")
|
||||
if err != nil {
|
||||
return fmt.Errorf("client creation failed: %w", err)
|
||||
}
|
||||
|
||||
start, end, err := getDateFilter()
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid date filter: %w", err)
|
||||
}
|
||||
activities, err := c.GetActivities(start, end)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get activities: %w", err)
|
||||
}
|
||||
|
||||
switch outputFormat {
|
||||
case "table":
|
||||
renderTable(activities)
|
||||
case "json":
|
||||
if err := renderJSON(activities); err != nil {
|
||||
return err
|
||||
}
|
||||
case "csv":
|
||||
if err := renderCSV(activities); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("invalid output format: %s", outputFormat)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
ListCmd.Flags().StringVarP(&outputFormat, "output", "o", "table", "Output format (table|json|csv)")
|
||||
ListCmd.Flags().StringVar(&startDate, "start", "", "Start date (YYYY-MM-DD)")
|
||||
ListCmd.Flags().StringVar(&endDate, "end", "", "End date (YYYY-MM-DD)")
|
||||
}
|
||||
|
||||
func getDateFilter() (time.Time, time.Time, error) {
|
||||
var start, end time.Time
|
||||
var err error
|
||||
|
||||
if startDate != "" {
|
||||
start, err = time.Parse("2006-01-02", startDate)
|
||||
if err != nil {
|
||||
return time.Time{}, time.Time{}, fmt.Errorf("invalid start date format: %w", err)
|
||||
}
|
||||
}
|
||||
if endDate != "" {
|
||||
end, err = time.Parse("2006-01-02", endDate)
|
||||
if err != nil {
|
||||
return time.Time{}, time.Time{}, fmt.Errorf("invalid end date format: %w", err)
|
||||
}
|
||||
}
|
||||
return start, end, nil
|
||||
}
|
||||
|
||||
func renderTable(activities []types.Activity) {
|
||||
table := tablewriter.NewWriter(os.Stdout)
|
||||
table.Header("ID", "Name", "Type", "Date", "Distance (km)", "Duration")
|
||||
|
||||
for _, a := range activities {
|
||||
table.Append(
|
||||
fmt.Sprint(a.ActivityID),
|
||||
a.ActivityName,
|
||||
a.ActivityType.TypeKey,
|
||||
a.StartTimeLocal,
|
||||
fmt.Sprintf("%.2f", a.Distance/1000),
|
||||
time.Duration(a.Duration*float64(time.Second)).String(),
|
||||
)
|
||||
}
|
||||
|
||||
table.Render()
|
||||
}
|
||||
|
||||
func renderJSON(activities []types.Activity) error {
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(activities)
|
||||
}
|
||||
|
||||
func renderCSV(activities []types.Activity) error {
|
||||
w := csv.NewWriter(os.Stdout)
|
||||
defer w.Flush()
|
||||
|
||||
if err := w.Write([]string{"ID", "Name", "Type", "Date", "Distance (km)", "Duration"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, a := range activities {
|
||||
record := []string{
|
||||
fmt.Sprint(a.ActivityID),
|
||||
a.ActivityName,
|
||||
a.ActivityType.TypeKey,
|
||||
a.StartTimeLocal,
|
||||
fmt.Sprintf("%.2f", a.Distance/1000),
|
||||
time.Duration(a.Duration * float64(time.Second)).String(),
|
||||
}
|
||||
if err := w.Write(record); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
193
cmd/garth/cmd/activities/list_test.go
Normal file
193
cmd/garth/cmd/activities/list_test.go
Normal file
@@ -0,0 +1,193 @@
|
||||
package activities
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sstent/go-garth/garth/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type mockClient struct{}
|
||||
|
||||
func (m *mockClient) GetActivities(start, end time.Time) ([]types.Activity, error) {
|
||||
return []types.Activity{
|
||||
{
|
||||
ActivityID: 123,
|
||||
ActivityName: "Morning Run",
|
||||
ActivityType: types.ActivityType{
|
||||
TypeKey: "running",
|
||||
},
|
||||
StartTimeLocal: "2025-09-18T08:30:00",
|
||||
Distance: 5000,
|
||||
Duration: 1800,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *mockClient) Login(email, password string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockClient) SaveSession(filename string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestRenderTable(t *testing.T) {
|
||||
activities := []types.Activity{
|
||||
{
|
||||
ActivityID: 123,
|
||||
ActivityName: "Morning Run",
|
||||
ActivityType: types.ActivityType{
|
||||
TypeKey: "running",
|
||||
},
|
||||
StartTimeLocal: "2025-09-18T08:30:00",
|
||||
Distance: 5000,
|
||||
Duration: 1800,
|
||||
},
|
||||
}
|
||||
|
||||
// Capture stdout
|
||||
old := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
renderTable(activities)
|
||||
|
||||
w.Close()
|
||||
os.Stdout = old
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.ReadFrom(r)
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "123")
|
||||
assert.Contains(t, output, "Morning Run")
|
||||
assert.Contains(t, output, "running")
|
||||
}
|
||||
|
||||
func TestRenderJSON(t *testing.T) {
|
||||
activities := []types.Activity{
|
||||
{
|
||||
ActivityID: 123,
|
||||
ActivityName: "Morning Run",
|
||||
ActivityType: types.ActivityType{
|
||||
TypeKey: "running",
|
||||
},
|
||||
StartTimeLocal: "2025-09-18T08:30:00",
|
||||
Distance: 5000,
|
||||
Duration: 1800,
|
||||
},
|
||||
}
|
||||
|
||||
// Capture stdout
|
||||
old := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
err := renderJSON(activities)
|
||||
|
||||
w.Close()
|
||||
os.Stdout = old
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.ReadFrom(r)
|
||||
output := buf.String()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, output, `"activityId": 123`)
|
||||
assert.Contains(t, output, `"activityName": "Morning Run"`)
|
||||
}
|
||||
|
||||
func TestRenderCSV(t *testing.T) {
|
||||
activities := []types.Activity{
|
||||
{
|
||||
ActivityID: 123,
|
||||
ActivityName: "Morning Run",
|
||||
ActivityType: types.ActivityType{
|
||||
TypeKey: "running",
|
||||
},
|
||||
StartTimeLocal: "2025-09-18T08:30:00",
|
||||
Distance: 5000,
|
||||
Duration: 1800,
|
||||
},
|
||||
}
|
||||
|
||||
// Capture stdout
|
||||
old := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
err := renderCSV(activities)
|
||||
|
||||
w.Close()
|
||||
os.Stdout = old
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.ReadFrom(r)
|
||||
output := buf.String()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, output, "123,Morning Run,running")
|
||||
}
|
||||
|
||||
func TestGetDateFilter(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
startDate string
|
||||
endDate string
|
||||
wantStart bool
|
||||
wantEnd bool
|
||||
}{
|
||||
{
|
||||
name: "no dates",
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
wantStart: false,
|
||||
wantEnd: false,
|
||||
},
|
||||
{
|
||||
name: "start date only",
|
||||
startDate: "2025-09-18",
|
||||
endDate: "",
|
||||
wantStart: true,
|
||||
wantEnd: false,
|
||||
},
|
||||
{
|
||||
name: "both dates",
|
||||
startDate: "2025-09-18",
|
||||
endDate: "2025-09-20",
|
||||
wantStart: true,
|
||||
wantEnd: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Save original values
|
||||
origStart, origEnd := startDate, endDate
|
||||
defer func() {
|
||||
startDate, endDate = origStart, origEnd
|
||||
}()
|
||||
|
||||
startDate = tt.startDate
|
||||
endDate = tt.endDate
|
||||
|
||||
start, end := getDateFilter()
|
||||
|
||||
if tt.wantStart {
|
||||
assert.False(t, start.IsZero(), "expected non-zero start time")
|
||||
} else {
|
||||
assert.True(t, start.IsZero(), "expected zero start time")
|
||||
}
|
||||
|
||||
if tt.wantEnd {
|
||||
assert.False(t, end.IsZero(), "expected non-zero end time")
|
||||
} else {
|
||||
assert.True(t, end.IsZero(), "expected zero end time")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
57
cmd/garth/cmd/auth/login.go
Normal file
57
cmd/garth/cmd/auth/login.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"syscall"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/term"
|
||||
|
||||
"github.com/sstent/go-garth/garth/client"
|
||||
)
|
||||
|
||||
var (
|
||||
email string
|
||||
password string
|
||||
)
|
||||
|
||||
var LoginCmd = &cobra.Command{
|
||||
Use: "login",
|
||||
Short: "Authenticate with Garmin Connect",
|
||||
Long: `Authenticate using Garmin Connect credentials and save session tokens`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if password == "" {
|
||||
fmt.Print("Enter password: ")
|
||||
bytePassword, err := term.ReadPassword(int(syscall.Stdin))
|
||||
if err != nil {
|
||||
return fmt.Errorf("password input failed: %w", err)
|
||||
}
|
||||
password = string(bytePassword)
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
c, err := client.NewClient("")
|
||||
if err != nil {
|
||||
return fmt.Errorf("client creation failed: %w", err)
|
||||
}
|
||||
|
||||
if err := c.Login(email, password); err != nil {
|
||||
return fmt.Errorf("login failed: %w", err)
|
||||
}
|
||||
|
||||
if err := c.SaveSession("session.json"); err != nil {
|
||||
return fmt.Errorf("session save failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Logged in successfully. Session saved to session.json")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
LoginCmd.Flags().StringVarP(&email, "email", "e", "", "Garmin login email")
|
||||
LoginCmd.Flags().StringVarP(&password, "password", "p", "", "Garmin login password (optional, will prompt if empty)")
|
||||
|
||||
// Mark required flags
|
||||
LoginCmd.MarkFlagRequired("email")
|
||||
}
|
||||
38
cmd/garth/cmd/auth/logout.go
Normal file
38
cmd/garth/cmd/auth/logout.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var LogoutCmd = &cobra.Command{
|
||||
Use: "logout",
|
||||
Short: "Logout and remove saved session",
|
||||
Long: `Remove the saved session file and clear authentication tokens`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
sessionPath, _ := cmd.Flags().GetString("session")
|
||||
if sessionPath == "" {
|
||||
sessionPath = "session.json"
|
||||
}
|
||||
|
||||
// Check if session file exists
|
||||
if _, err := os.Stat(sessionPath); os.IsNotExist(err) {
|
||||
fmt.Printf("No session file found at %s\n", sessionPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove session file
|
||||
if err := os.Remove(sessionPath); err != nil {
|
||||
return fmt.Errorf("failed to remove session file: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Logged out successfully. Session file removed: %s\n", sessionPath)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
LogoutCmd.Flags().StringP("session", "s", "session.json", "Session file path to remove")
|
||||
}
|
||||
59
cmd/garth/cmd/auth/status.go
Normal file
59
cmd/garth/cmd/auth/status.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/sstent/go-garth/garth/client"
|
||||
)
|
||||
|
||||
var StatusCmd = &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Check authentication status",
|
||||
Long: `Check if you are currently authenticated with Garmin Connect`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
sessionPath, _ := cmd.Flags().GetString("session")
|
||||
if sessionPath == "" {
|
||||
sessionPath = "session.json"
|
||||
}
|
||||
|
||||
// Check if session file exists
|
||||
if _, err := os.Stat(sessionPath); os.IsNotExist(err) {
|
||||
fmt.Printf("❌ Not authenticated\n")
|
||||
fmt.Printf("Session file not found: %s\n", sessionPath)
|
||||
fmt.Printf("Run 'garth auth login' to authenticate\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try to create client and load session
|
||||
c, err := client.NewClient("")
|
||||
if err != nil {
|
||||
return fmt.Errorf("client creation failed: %w", err)
|
||||
}
|
||||
|
||||
if err := c.LoadSession(sessionPath); err != nil {
|
||||
fmt.Printf("❌ Session file exists but is invalid\n")
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
fmt.Printf("Run 'garth auth login' to re-authenticate\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try to make a simple authenticated request to verify session
|
||||
if _, err := c.GetUserProfile(); err != nil {
|
||||
fmt.Printf("❌ Session exists but authentication failed\n")
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
fmt.Printf("Run 'garth auth login' to re-authenticate\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("✅ Authenticated\n")
|
||||
fmt.Printf("Session file: %s\n", sessionPath)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
StatusCmd.Flags().StringP("session", "s", "session.json", "Session file path to check")
|
||||
}
|
||||
111
cmd/garth/cmd/root.go
Normal file
111
cmd/garth/cmd/root.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/sstent/go-garth/cmd/garth/cmd/activities"
|
||||
"github.com/sstent/go-garth/cmd/garth/cmd/auth"
|
||||
garthclient "github.com/sstent/go-garth/garth/client"
|
||||
)
|
||||
|
||||
var (
|
||||
cfgFile string
|
||||
garthClient *garthclient.Client
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "garth",
|
||||
Short: "Garmin Connect CLI client",
|
||||
Long: `A comprehensive CLI client for Garmin Connect.
|
||||
Access your activities, health data, and statistics from the command line.`,
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
initClient()
|
||||
},
|
||||
}
|
||||
|
||||
func Execute() error {
|
||||
return rootCmd.Execute()
|
||||
}
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(initConfig)
|
||||
|
||||
// Add subcommands
|
||||
rootCmd.AddCommand(auth.LoginCmd)
|
||||
rootCmd.AddCommand(auth.LogoutCmd)
|
||||
rootCmd.AddCommand(auth.StatusCmd)
|
||||
rootCmd.AddCommand(activities.ActivitiesCmd)
|
||||
|
||||
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "",
|
||||
"config file (default is $HOME/.garth/config.yaml)")
|
||||
rootCmd.PersistentFlags().StringP("format", "f", "table",
|
||||
"output format (table, json, csv)")
|
||||
rootCmd.PersistentFlags().BoolP("verbose", "v", false,
|
||||
"verbose output")
|
||||
rootCmd.PersistentFlags().StringP("session", "s", "",
|
||||
"session file path")
|
||||
|
||||
viper.BindPFlag("format", rootCmd.PersistentFlags().Lookup("format"))
|
||||
viper.BindPFlag("verbose", rootCmd.PersistentFlags().Lookup("verbose"))
|
||||
viper.BindPFlag("session", rootCmd.PersistentFlags().Lookup("session"))
|
||||
}
|
||||
|
||||
func initConfig() {
|
||||
if cfgFile != "" {
|
||||
viper.SetConfigFile(cfgFile)
|
||||
} else {
|
||||
home, err := os.UserHomeDir()
|
||||
cobra.CheckErr(err)
|
||||
|
||||
configDir := filepath.Join(home, ".garth")
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error creating config directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
viper.AddConfigPath(configDir)
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("yaml")
|
||||
}
|
||||
|
||||
viper.AutomaticEnv()
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
||||
fmt.Fprintf(os.Stderr, "Error reading config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func initClient() {
|
||||
var err error
|
||||
domain := viper.GetString("domain")
|
||||
if domain == "" {
|
||||
domain = "garmin.com"
|
||||
}
|
||||
|
||||
garthClient, err = garthclient.NewClient(domain)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error creating client: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
sessionPath := viper.GetString("session")
|
||||
if sessionPath == "" {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error getting home directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
sessionPath = filepath.Join(home, ".garth", "session.json")
|
||||
}
|
||||
|
||||
if err := garthClient.LoadSession(sessionPath); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: could not load session: %v\n", err)
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"garmin-connect/garth"
|
||||
"garmin-connect/garth/credentials"
|
||||
auth "github.com/sstent/go-garth/internal/auth"
|
||||
garmin "github.com/sstent/go-garth/pkg/garmin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -23,13 +23,13 @@ func main() {
|
||||
flag.Parse()
|
||||
|
||||
// Load credentials from .env file
|
||||
email, password, domain, err := credentials.LoadEnvCredentials()
|
||||
email, password, domain, err := auth.LoadEnvCredentials()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load credentials: %v", err)
|
||||
}
|
||||
|
||||
// Create client
|
||||
garminClient, err := garth.NewClient(domain)
|
||||
garminClient, err := garmin.NewClient(domain)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
@@ -77,7 +77,7 @@ func main() {
|
||||
displayActivities(activities)
|
||||
}
|
||||
|
||||
func outputTokensJSON(c *garth.Client) {
|
||||
func outputTokensJSON(c *garmin.Client) {
|
||||
tokens := struct {
|
||||
OAuth1 *garth.OAuth1Token `json:"oauth1"`
|
||||
OAuth2 *garth.OAuth2Token `json:"oauth2"`
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package garth
|
||||
|
||||
import (
|
||||
"garmin-connect/garth/client"
|
||||
"garmin-connect/garth/data"
|
||||
"garmin-connect/garth/errors"
|
||||
"garmin-connect/garth/stats"
|
||||
"garmin-connect/garth/types"
|
||||
"github.com/sstent/go-garth/garth/client"
|
||||
"github.com/sstent/go-garth/garth/data"
|
||||
"github.com/sstent/go-garth/garth/errors"
|
||||
"github.com/sstent/go-garth/garth/stats"
|
||||
"github.com/sstent/go-garth/garth/types"
|
||||
)
|
||||
|
||||
// Re-export main types for convenience
|
||||
|
||||
@@ -2,11 +2,12 @@ package garth_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"garmin-connect/garth/client"
|
||||
"garmin-connect/garth/data"
|
||||
"garmin-connect/garth/testutils"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sstent/go-garth/garth/client"
|
||||
"github.com/sstent/go-garth/garth/data"
|
||||
"github.com/sstent/go-garth/garth/testutils"
|
||||
)
|
||||
|
||||
func BenchmarkBodyBatteryGet(b *testing.B) {
|
||||
|
||||
@@ -5,13 +5,13 @@ import (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"garmin-connect/garth/testutils"
|
||||
"github.com/sstent/go-garth/garth/testutils"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"garmin-connect/garth/client"
|
||||
"garmin-connect/garth/errors"
|
||||
"github.com/sstent/go-garth/garth/client"
|
||||
"github.com/sstent/go-garth/garth/errors"
|
||||
)
|
||||
|
||||
func TestClient_Login_Success(t *testing.T) {
|
||||
|
||||
@@ -5,12 +5,12 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"garmin-connect/garth/testutils"
|
||||
"github.com/sstent/go-garth/garth/testutils"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"garmin-connect/garth/client"
|
||||
"github.com/sstent/go-garth/garth/client"
|
||||
)
|
||||
|
||||
func TestClient_GetUserProfile(t *testing.T) {
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"garmin-connect/garth/client"
|
||||
"garmin-connect/garth/utils"
|
||||
"github.com/sstent/go-garth/garth/client"
|
||||
"github.com/sstent/go-garth/garth/utils"
|
||||
)
|
||||
|
||||
// Data defines the interface for Garmin Connect data types.
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"garmin-connect/garth/client"
|
||||
"github.com/sstent/go-garth/garth/client"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"garmin-connect/garth/client"
|
||||
"github.com/sstent/go-garth/garth/client"
|
||||
)
|
||||
|
||||
// DailyBodyBatteryStress represents complete daily Body Battery and stress data
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"garmin-connect/garth/client"
|
||||
"garmin-connect/garth/utils"
|
||||
"github.com/sstent/go-garth/garth/client"
|
||||
"github.com/sstent/go-garth/garth/utils"
|
||||
)
|
||||
|
||||
// HRVSummary represents Heart Rate Variability summary data
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"garmin-connect/garth/client"
|
||||
"github.com/sstent/go-garth/garth/client"
|
||||
)
|
||||
|
||||
// SleepScores represents sleep scoring data
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"garmin-connect/garth/client"
|
||||
"github.com/sstent/go-garth/garth/client"
|
||||
)
|
||||
|
||||
// WeightData represents weight measurement data
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package garth
|
||||
|
||||
import (
|
||||
"garmin-connect/garth/client"
|
||||
"garmin-connect/garth/data"
|
||||
"garmin-connect/garth/errors"
|
||||
"garmin-connect/garth/stats"
|
||||
"garmin-connect/garth/types"
|
||||
"github.com/sstent/go-garth/garth/client"
|
||||
"github.com/sstent/go-garth/garth/data"
|
||||
"github.com/sstent/go-garth/garth/errors"
|
||||
"github.com/sstent/go-garth/garth/stats"
|
||||
"github.com/sstent/go-garth/garth/types"
|
||||
)
|
||||
|
||||
// Client is the main Garmin Connect client type
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"garmin-connect/garth/client"
|
||||
"garmin-connect/garth/data"
|
||||
"garmin-connect/garth/stats"
|
||||
"github.com/sstent/go-garth/garth/client"
|
||||
"github.com/sstent/go-garth/garth/data"
|
||||
"github.com/sstent/go-garth/garth/stats"
|
||||
)
|
||||
|
||||
func TestBodyBatteryIntegration(t *testing.T) {
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"garmin-connect/garth/types"
|
||||
"garmin-connect/garth/utils"
|
||||
"github.com/sstent/go-garth/garth/types"
|
||||
"github.com/sstent/go-garth/garth/utils"
|
||||
)
|
||||
|
||||
// GetOAuth1Token retrieves an OAuth1 token using the provided ticket
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"garmin-connect/garth/client"
|
||||
"garmin-connect/garth/utils"
|
||||
"github.com/sstent/go-garth/garth/client"
|
||||
"github.com/sstent/go-garth/garth/utils"
|
||||
)
|
||||
|
||||
type Stats interface {
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"io"
|
||||
"net/url"
|
||||
|
||||
"garmin-connect/garth/client"
|
||||
"github.com/sstent/go-garth/garth/client"
|
||||
)
|
||||
|
||||
// MockClient simulates API client for tests
|
||||
|
||||
@@ -3,7 +3,7 @@ package users
|
||||
import (
|
||||
"time"
|
||||
|
||||
"garmin-connect/garth/client"
|
||||
"github.com/sstent/go-garth/garth/client"
|
||||
)
|
||||
|
||||
type PowerFormat struct {
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"garmin-connect/garth/types"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
@@ -14,6 +13,8 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sstent/go-garth/garth/types"
|
||||
)
|
||||
|
||||
var oauthConsumer *types.OAuthConsumer
|
||||
|
||||
39
go.mod
39
go.mod
@@ -1,8 +1,41 @@
|
||||
module garmin-connect
|
||||
module github.com/sstent/go-garth
|
||||
|
||||
go 1.24.2
|
||||
|
||||
require github.com/joho/godotenv v1.5.1
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.2
|
||||
|
||||
require (
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/olekukonko/tablewriter v1.0.9
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/spf13/viper v1.21.0
|
||||
golang.org/x/term v0.35.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
|
||||
github.com/olekukonko/errors v1.1.0 // indirect
|
||||
github.com/olekukonko/ll v0.1.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
|
||||
68
go.sum
68
go.sum
@@ -1,12 +1,78 @@
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
|
||||
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
|
||||
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||
github.com/olekukonko/ll v0.1.1 h1:9Dfeed5/Mgaxb9lHRAftLK9pVfYETvHn+If6lywVhJc=
|
||||
github.com/olekukonko/ll v0.1.1/go.mod h1:2dJo+hYZcJMLMbKwHEWvxCUbAOLc/CXWS9noET22Mdo=
|
||||
github.com/olekukonko/tablewriter v1.0.9 h1:XGwRsYLC2bY7bNd93Dk51bcPZksWZmLYuaTHR0FqfL8=
|
||||
github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
||||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -1,550 +0,0 @@
|
||||
# Implementation Plan for Steps 1 & 2: Project Structure and Client Refactoring
|
||||
|
||||
## Overview
|
||||
This document provides a detailed implementation plan for refactoring the existing Go code from `main.go` into a proper modular structure as outlined in the porting plan.
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
### Existing Code in main.go (Lines 1-761)
|
||||
The current `main.go` contains:
|
||||
- **Client struct** (lines 24-30) with domain, httpClient, username, authToken
|
||||
- **Data models**: SessionData, ActivityType, EventType, Activity, OAuth1Token, OAuth2Token, OAuthConsumer
|
||||
- **OAuth functions**: loadOAuthConsumer, generateNonce, generateTimestamp, percentEncode, createSignatureBaseString, createSigningKey, signRequest, createOAuth1AuthorizationHeader
|
||||
- **SSO functions**: getCSRFToken, extractTicket, exchangeOAuth1ForOAuth2, Login, loadEnvCredentials
|
||||
- **Client methods**: NewClient, getUserProfile, GetActivities, SaveSession, LoadSession
|
||||
- **Main function** with authentication flow and activity retrieval
|
||||
|
||||
## Step 1: Project Structure Setup
|
||||
|
||||
### Directory Structure to Create
|
||||
```
|
||||
garmin-connect/
|
||||
├── client/
|
||||
│ ├── client.go # Core client logic
|
||||
│ ├── auth.go # Authentication handling
|
||||
│ └── sso.go # SSO authentication
|
||||
├── data/
|
||||
│ └── base.go # Base data models and interfaces
|
||||
├── types/
|
||||
│ └── tokens.go # Token structures
|
||||
├── utils/
|
||||
│ └── utils.go # Utility functions
|
||||
├── errors/
|
||||
│ └── errors.go # Custom error types
|
||||
├── cmd/
|
||||
│ └── garth/
|
||||
│ └── main.go # CLI tool (refactored from current main.go)
|
||||
└── main.go # Keep original temporarily for testing
|
||||
```
|
||||
|
||||
## Step 2: Core Client Refactoring - Detailed Implementation
|
||||
|
||||
### 2.1 Create `types/tokens.go`
|
||||
**Purpose**: Centralize all token-related structures
|
||||
|
||||
```go
|
||||
package types
|
||||
|
||||
import "time"
|
||||
|
||||
// OAuth1Token represents OAuth1 token response
|
||||
type OAuth1Token struct {
|
||||
OAuthToken string `json:"oauth_token"`
|
||||
OAuthTokenSecret string `json:"oauth_token_secret"`
|
||||
MFAToken string `json:"mfa_token,omitempty"`
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
// OAuth2Token represents OAuth2 token response
|
||||
type OAuth2Token struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
Scope string `json:"scope"`
|
||||
CreatedAt time.Time // Added for expiration tracking
|
||||
}
|
||||
|
||||
// OAuthConsumer represents OAuth consumer credentials
|
||||
type OAuthConsumer struct {
|
||||
ConsumerKey string `json:"consumer_key"`
|
||||
ConsumerSecret string `json:"consumer_secret"`
|
||||
}
|
||||
|
||||
// SessionData represents saved session information
|
||||
type SessionData struct {
|
||||
Domain string `json:"domain"`
|
||||
Username string `json:"username"`
|
||||
AuthToken string `json:"auth_token"`
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 Create `client/client.go`
|
||||
**Purpose**: Core client functionality and HTTP operations
|
||||
|
||||
```go
|
||||
package client
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"garmin-connect/types"
|
||||
)
|
||||
|
||||
// Client represents the Garmin Connect client
|
||||
type Client struct {
|
||||
domain string
|
||||
httpClient *http.Client
|
||||
username string
|
||||
authToken string
|
||||
oauth1Token *types.OAuth1Token
|
||||
oauth2Token *types.OAuth2Token
|
||||
}
|
||||
|
||||
// ConfigOption represents a client configuration option
|
||||
type ConfigOption func(*Client)
|
||||
|
||||
// NewClient creates a new Garmin Connect client
|
||||
func NewClient(domain string) (*Client, error) {
|
||||
jar, err := cookiejar.New(nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create cookie jar: %w", err)
|
||||
}
|
||||
|
||||
return &Client{
|
||||
domain: domain,
|
||||
httpClient: &http.Client{
|
||||
Jar: jar,
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Configure applies configuration options to the client
|
||||
func (c *Client) Configure(opts ...ConfigOption) error {
|
||||
for _, opt := range opts {
|
||||
opt(c)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConnectAPI makes authenticated API calls to Garmin Connect
|
||||
func (c *Client) ConnectAPI(path, method string, data interface{}) (interface{}, error) {
|
||||
// Implementation based on Python http.py Client.connectapi()
|
||||
// Should handle authentication, retries, and error responses
|
||||
}
|
||||
|
||||
// Download downloads data from Garmin Connect
|
||||
func (c *Client) Download(path string) ([]byte, error) {
|
||||
// Implementation for downloading files/data
|
||||
}
|
||||
|
||||
// Upload uploads data to Garmin Connect
|
||||
func (c *Client) Upload(filePath, uploadPath string) (map[string]interface{}, error) {
|
||||
// Implementation for uploading files/data
|
||||
}
|
||||
|
||||
// GetUserProfile retrieves the current user's profile
|
||||
func (c *Client) GetUserProfile() error {
|
||||
// Extracted from main.go getUserProfile method
|
||||
}
|
||||
|
||||
// GetActivities retrieves recent activities
|
||||
func (c *Client) GetActivities(limit int) ([]Activity, error) {
|
||||
// Extracted from main.go GetActivities method
|
||||
}
|
||||
|
||||
// SaveSession saves the current session to a file
|
||||
func (c *Client) SaveSession(filename string) error {
|
||||
// Extracted from main.go SaveSession method
|
||||
}
|
||||
|
||||
// LoadSession loads a session from a file
|
||||
func (c *Client) LoadSession(filename string) error {
|
||||
// Extracted from main.go LoadSession method
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 Create `client/auth.go`
|
||||
**Purpose**: Authentication and token management
|
||||
|
||||
```go
|
||||
package client
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"garmin-connect/types"
|
||||
)
|
||||
|
||||
var oauthConsumer *types.OAuthConsumer
|
||||
|
||||
// loadOAuthConsumer loads OAuth consumer credentials
|
||||
func loadOAuthConsumer() (*types.OAuthConsumer, error) {
|
||||
// Extracted from main.go loadOAuthConsumer function
|
||||
}
|
||||
|
||||
// OAuth1 signing functions (extract from main.go)
|
||||
func generateNonce() string
|
||||
func generateTimestamp() string
|
||||
func percentEncode(s string) string
|
||||
func createSignatureBaseString(method, baseURL string, params map[string]string) string
|
||||
func createSigningKey(consumerSecret, tokenSecret string) string
|
||||
func signRequest(consumerSecret, tokenSecret, baseString string) string
|
||||
func createOAuth1AuthorizationHeader(method, requestURL string, params map[string]string, consumerKey, consumerSecret, token, tokenSecret string) string
|
||||
|
||||
// Token expiration checking
|
||||
func (t *types.OAuth2Token) IsExpired() bool {
|
||||
return time.Since(t.CreatedAt) > time.Duration(t.ExpiresIn)*time.Second
|
||||
}
|
||||
|
||||
// MFA support placeholder
|
||||
func (c *Client) HandleMFA(mfaToken string) error {
|
||||
// Placeholder for MFA handling
|
||||
return fmt.Errorf("MFA not yet implemented")
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 Create `client/sso.go`
|
||||
**Purpose**: SSO authentication flow
|
||||
|
||||
```go
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"garmin-connect/types"
|
||||
)
|
||||
|
||||
var (
|
||||
csrfRegex = regexp.MustCompile(`name="_csrf"\s+value="(.+?)"`)
|
||||
titleRegex = regexp.MustCompile(`<title>(.+?)</title>`)
|
||||
ticketRegex = regexp.MustCompile(`embed\?ticket=([^"]+)"`)
|
||||
)
|
||||
|
||||
// Login performs SSO login with email and password
|
||||
func (c *Client) Login(email, password string) error {
|
||||
// Extracted from main.go Login method
|
||||
}
|
||||
|
||||
// ResumeLogin resumes login after MFA
|
||||
func (c *Client) ResumeLogin(mfaToken string) error {
|
||||
// New method for MFA completion
|
||||
}
|
||||
|
||||
// SSO helper functions (extract from main.go)
|
||||
func getCSRFToken(respBody string) string
|
||||
func extractTicket(respBody string) string
|
||||
func exchangeOAuth1ForOAuth2(oauth1Token *types.OAuth1Token, domain string) (*types.OAuth2Token, error)
|
||||
func loadEnvCredentials() (email, password, domain string, err error)
|
||||
```
|
||||
|
||||
### 2.5 Create `data/base.go`
|
||||
**Purpose**: Base data models and interfaces
|
||||
|
||||
```go
|
||||
package data
|
||||
|
||||
import (
|
||||
"time"
|
||||
"garmin-connect/client"
|
||||
)
|
||||
|
||||
// ActivityType represents the type of activity
|
||||
type ActivityType struct {
|
||||
TypeID int `json:"typeId"`
|
||||
TypeKey string `json:"typeKey"`
|
||||
ParentTypeID *int `json:"parentTypeId,omitempty"`
|
||||
}
|
||||
|
||||
// EventType represents the event type of an activity
|
||||
type EventType struct {
|
||||
TypeID int `json:"typeId"`
|
||||
TypeKey string `json:"typeKey"`
|
||||
}
|
||||
|
||||
// Activity represents a Garmin Connect activity
|
||||
type Activity struct {
|
||||
ActivityID int64 `json:"activityId"`
|
||||
ActivityName string `json:"activityName"`
|
||||
Description string `json:"description"`
|
||||
StartTimeLocal string `json:"startTimeLocal"`
|
||||
StartTimeGMT string `json:"startTimeGMT"`
|
||||
ActivityType ActivityType `json:"activityType"`
|
||||
EventType EventType `json:"eventType"`
|
||||
Distance float64 `json:"distance"`
|
||||
Duration float64 `json:"duration"`
|
||||
ElapsedDuration float64 `json:"elapsedDuration"`
|
||||
MovingDuration float64 `json:"movingDuration"`
|
||||
ElevationGain float64 `json:"elevationGain"`
|
||||
ElevationLoss float64 `json:"elevationLoss"`
|
||||
AverageSpeed float64 `json:"averageSpeed"`
|
||||
MaxSpeed float64 `json:"maxSpeed"`
|
||||
Calories float64 `json:"calories"`
|
||||
AverageHR float64 `json:"averageHR"`
|
||||
MaxHR float64 `json:"maxHR"`
|
||||
}
|
||||
|
||||
// Data interface for all data models
|
||||
type Data interface {
|
||||
Get(day time.Time, client *client.Client) (interface{}, error)
|
||||
List(end time.Time, days int, client *client.Client, maxWorkers int) ([]interface{}, error)
|
||||
}
|
||||
```
|
||||
|
||||
### 2.6 Create `errors/errors.go`
|
||||
**Purpose**: Custom error types for better error handling
|
||||
|
||||
```go
|
||||
package errors
|
||||
|
||||
import "fmt"
|
||||
|
||||
// GarthError represents a general Garth error
|
||||
type GarthError struct {
|
||||
Message string
|
||||
Cause error
|
||||
}
|
||||
|
||||
func (e *GarthError) Error() string {
|
||||
if e.Cause != nil {
|
||||
return fmt.Sprintf("%s: %v", e.Message, e.Cause)
|
||||
}
|
||||
return e.Message
|
||||
}
|
||||
|
||||
// GarthHTTPError represents an HTTP-related error
|
||||
type GarthHTTPError struct {
|
||||
GarthError
|
||||
StatusCode int
|
||||
Response string
|
||||
}
|
||||
|
||||
func (e *GarthHTTPError) Error() string {
|
||||
return fmt.Sprintf("HTTP %d: %s", e.StatusCode, e.GarthError.Error())
|
||||
}
|
||||
```
|
||||
|
||||
### 2.7 Create `utils/utils.go`
|
||||
**Purpose**: Utility functions
|
||||
|
||||
```go
|
||||
package utils
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// CamelToSnake converts CamelCase to snake_case
|
||||
func CamelToSnake(s string) string {
|
||||
var result []rune
|
||||
for i, r := range s {
|
||||
if unicode.IsUpper(r) && i > 0 {
|
||||
result = append(result, '_')
|
||||
}
|
||||
result = append(result, unicode.ToLower(r))
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
|
||||
// CamelToSnakeDict converts map keys from camelCase to snake_case
|
||||
func CamelToSnakeDict(m map[string]interface{}) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
for k, v := range m {
|
||||
result[CamelToSnake(k)] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// FormatEndDate formats an end date interface to time.Time
|
||||
func FormatEndDate(end interface{}) time.Time {
|
||||
switch v := end.(type) {
|
||||
case time.Time:
|
||||
return v
|
||||
case string:
|
||||
if t, err := time.Parse("2006-01-02", v); err == nil {
|
||||
return t
|
||||
}
|
||||
}
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
// DateRange generates a range of dates
|
||||
func DateRange(end time.Time, days int) []time.Time {
|
||||
var dates []time.Time
|
||||
for i := 0; i < days; i++ {
|
||||
dates = append(dates, end.AddDate(0, 0, -i))
|
||||
}
|
||||
return dates
|
||||
}
|
||||
|
||||
// GetLocalizedDateTime converts timestamps to localized time
|
||||
func GetLocalizedDateTime(gmtTimestamp, localTimestamp int64) time.Time {
|
||||
// Implementation based on timezone offset
|
||||
return time.Unix(localTimestamp, 0)
|
||||
}
|
||||
```
|
||||
|
||||
### 2.8 Refactor `main.go`
|
||||
**Purpose**: Simplified main function using the new client package
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"garmin-connect/client"
|
||||
"garmin-connect/data"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Load credentials from .env file
|
||||
email, password, domain, err := loadEnvCredentials()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load credentials: %v", err)
|
||||
}
|
||||
|
||||
// Create client
|
||||
garminClient, err := client.NewClient(domain)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
// Try to load existing session first
|
||||
sessionFile := "garmin_session.json"
|
||||
if err := garminClient.LoadSession(sessionFile); err != nil {
|
||||
fmt.Println("No existing session found, logging in with credentials from .env...")
|
||||
|
||||
if err := garminClient.Login(email, password); err != nil {
|
||||
log.Fatalf("Login failed: %v", err)
|
||||
}
|
||||
|
||||
// Save session for future use
|
||||
if err := garminClient.SaveSession(sessionFile); err != nil {
|
||||
fmt.Printf("Failed to save session: %v\n", err)
|
||||
}
|
||||
} else {
|
||||
fmt.Println("Loaded existing session")
|
||||
}
|
||||
|
||||
// Test getting activities
|
||||
activities, err := garminClient.GetActivities(5)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get activities: %v", err)
|
||||
}
|
||||
|
||||
// Display activities
|
||||
displayActivities(activities)
|
||||
}
|
||||
|
||||
func displayActivities(activities []data.Activity) {
|
||||
fmt.Printf("\n=== Recent Activities ===\n")
|
||||
for i, activity := range activities {
|
||||
fmt.Printf("%d. %s\n", i+1, activity.ActivityName)
|
||||
fmt.Printf(" Type: %s\n", activity.ActivityType.TypeKey)
|
||||
fmt.Printf(" Date: %s\n", activity.StartTimeLocal)
|
||||
if activity.Distance > 0 {
|
||||
fmt.Printf(" Distance: %.2f km\n", activity.Distance/1000)
|
||||
}
|
||||
if activity.Duration > 0 {
|
||||
duration := time.Duration(activity.Duration) * time.Second
|
||||
fmt.Printf(" Duration: %v\n", duration.Round(time.Second))
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
func loadEnvCredentials() (email, password, domain string, err error) {
|
||||
// This function should be moved to client package eventually
|
||||
// For now, keep it here to maintain functionality
|
||||
if err := godotenv.Load(); err != nil {
|
||||
return "", "", "", fmt.Errorf("failed to load .env file: %w", err)
|
||||
}
|
||||
|
||||
email = os.Getenv("GARMIN_EMAIL")
|
||||
password = os.Getenv("GARMIN_PASSWORD")
|
||||
domain = os.Getenv("GARMIN_DOMAIN")
|
||||
|
||||
if domain == "" {
|
||||
domain = "garmin.com"
|
||||
}
|
||||
|
||||
if email == "" || password == "" {
|
||||
return "", "", "", fmt.Errorf("GARMIN_EMAIL and GARMIN_PASSWORD must be set in .env file")
|
||||
}
|
||||
|
||||
return email, password, domain, nil
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. **Create directory structure** first
|
||||
2. **Create types/tokens.go** - Move all token structures
|
||||
3. **Create errors/errors.go** - Define custom error types
|
||||
4. **Create utils/utils.go** - Add utility functions
|
||||
5. **Create client/auth.go** - Extract authentication logic
|
||||
6. **Create client/sso.go** - Extract SSO logic
|
||||
7. **Create data/base.go** - Extract data models
|
||||
8. **Create client/client.go** - Extract client logic
|
||||
9. **Refactor main.go** - Update to use new packages
|
||||
10. **Test the refactored code** - Ensure functionality is preserved
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
After each major step:
|
||||
1. Run `go build` to check for compilation errors
|
||||
2. Test authentication flow if SSO logic was modified
|
||||
3. Test activity retrieval if client methods were changed
|
||||
4. Verify session save/load functionality
|
||||
|
||||
## Key Considerations
|
||||
|
||||
1. **Maintain backward compatibility** - Ensure existing functionality works
|
||||
2. **Error handling** - Use new custom error types appropriately
|
||||
3. **Package imports** - Update import paths correctly
|
||||
4. **Visibility** - Export only necessary functions/types (capitalize appropriately)
|
||||
5. **Documentation** - Add package and function documentation
|
||||
|
||||
This plan provides a systematic approach to refactoring the existing code while maintaining functionality and preparing for the addition of new features from the Python library.
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"garmin-connect/garth/oauth"
|
||||
"garmin-connect/garth/types"
|
||||
"github.com/sstent/go-garth/garth/oauth"
|
||||
"github.com/sstent/go-garth/garth/types"
|
||||
)
|
||||
|
||||
var (
|
||||
6
main.go
6
main.go
@@ -5,9 +5,9 @@ import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"garmin-connect/garth/client"
|
||||
"garmin-connect/garth/credentials"
|
||||
"garmin-connect/garth/types"
|
||||
"github.com/sstent/go-garth/internal/auth"
|
||||
"github.com/sstent/go-garth/pkg/garmin"
|
||||
"github.com/sstent/go-garth/pkg/garmin/types"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
58
orchestrator-export.yaml
Normal file
58
orchestrator-export.yaml
Normal file
@@ -0,0 +1,58 @@
|
||||
customModes:
|
||||
- slug: orchestrator
|
||||
name: Orchestrator
|
||||
iconName: codicon-run-all
|
||||
roleDefinition: You are Kilo Code, a strategic workflow orchestrator who
|
||||
coordinates complex tasks by delegating them to appropriate specialized
|
||||
modes. You have a comprehensive understanding of each mode's capabilities
|
||||
and limitations, allowing you to effectively break down complex problems
|
||||
into discrete tasks that can be solved by different specialists.
|
||||
whenToUse: Use this mode for complex, multi-step projects that require
|
||||
coordination across different specialties. Ideal when you need to break
|
||||
down large tasks into subtasks, manage workflows, or coordinate work that
|
||||
spans multiple domains or expertise areas.
|
||||
description: Coordinate tasks across multiple modes
|
||||
groups: []
|
||||
customInstructions: >-
|
||||
Your role is to coordinate complex workflows by delegating tasks to
|
||||
specialized modes. As an orchestrator, you should:
|
||||
|
||||
|
||||
1. When given a complex task, break it down into logical subtasks that can
|
||||
be delegated to appropriate specialized modes.
|
||||
|
||||
|
||||
2. For each subtask, use the `new_task` tool to delegate. Choose the most
|
||||
appropriate mode for the subtask's specific goal and provide comprehensive
|
||||
instructions in the `message` parameter. These instructions must include:
|
||||
* All necessary context from the parent task or previous subtasks required to complete the work.
|
||||
* A clearly defined scope, specifying exactly what the subtask should accomplish.
|
||||
* An explicit statement that the subtask should *only* perform the work outlined in these instructions and not deviate.
|
||||
* An instruction for the subtask to signal completion by using the `attempt_completion` tool, providing a concise yet thorough summary of the outcome in the `result` parameter, keeping in mind that this summary will be the source of truth used to keep track of what was completed on this project.
|
||||
* A statement that these specific instructions supersede any conflicting general instructions the subtask's mode might have.
|
||||
|
||||
3. Track and manage the progress of all subtasks. When a subtask is
|
||||
completed, analyze its results and determine the next steps.
|
||||
|
||||
|
||||
4. Help the user understand how the different subtasks fit together in the
|
||||
overall workflow. Provide clear reasoning about why you're delegating
|
||||
specific tasks to specific modes.
|
||||
|
||||
|
||||
5. When all subtasks are completed, synthesize the results and provide a
|
||||
comprehensive overview of what was accomplished.
|
||||
|
||||
|
||||
6. Ask clarifying questions when necessary to better understand how to
|
||||
break down complex tasks effectively.
|
||||
|
||||
|
||||
7. Suggest improvements to the workflow based on the results of completed
|
||||
subtasks.
|
||||
|
||||
|
||||
Use subtasks to maintain clarity. If a request significantly shifts focus
|
||||
or requires a different expertise (mode), consider creating a subtask
|
||||
rather than overloading the current one.
|
||||
source: project
|
||||
529
phase1.md
Normal file
529
phase1.md
Normal file
@@ -0,0 +1,529 @@
|
||||
# Phase 1: Core Functionality Implementation Plan
|
||||
**Duration: 2-3 weeks**
|
||||
**Goal: Establish solid foundation with enhanced CLI and core missing features**
|
||||
|
||||
## Overview
|
||||
Phase 1 focuses on building the essential functionality that users need immediately while establishing the foundation for future enhancements. This phase prioritizes user-facing features and basic API improvements.
|
||||
|
||||
---
|
||||
|
||||
## Subphase 1A: Package Reorganization & CLI Foundation (Days 1-3)
|
||||
|
||||
### Objectives
|
||||
- Restructure packages for better maintainability
|
||||
- Set up cobra-based CLI framework
|
||||
- Establish consistent naming conventions
|
||||
|
||||
### Tasks
|
||||
|
||||
#### 1A.1: Package Structure Refactoring
|
||||
**Duration: 1 day**
|
||||
|
||||
```
|
||||
Current Structure → New Structure
|
||||
garth/ pkg/garmin/
|
||||
├── client/ ├── client.go # Main client interface
|
||||
├── data/ ├── activities.go # Activity operations
|
||||
├── stats/ ├── health.go # Health data operations
|
||||
├── sso/ ├── stats.go # Statistics operations
|
||||
├── oauth/ ├── auth.go # Authentication
|
||||
└── ... └── types.go # Public types
|
||||
|
||||
internal/
|
||||
├── api/ # Low-level API client
|
||||
├── auth/ # Auth implementation
|
||||
├── data/ # Data processing
|
||||
└── utils/ # Internal utilities
|
||||
|
||||
cmd/garth/
|
||||
├── main.go # CLI entry point
|
||||
├── root.go # Root command
|
||||
├── auth.go # Auth commands
|
||||
├── activities.go # Activity commands
|
||||
├── health.go # Health commands
|
||||
└── stats.go # Stats commands
|
||||
```
|
||||
|
||||
**Deliverables:**
|
||||
- [ ] New package structure implemented
|
||||
- [ ] All imports updated
|
||||
- [ ] No breaking changes to existing functionality
|
||||
- [ ] Package documentation updated
|
||||
|
||||
#### 1A.2: CLI Framework Setup
|
||||
**Duration: 1 day**
|
||||
|
||||
```go
|
||||
// cmd/garth/root.go
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "garth",
|
||||
Short: "Garmin Connect CLI tool",
|
||||
Long: `A comprehensive CLI tool for interacting with Garmin Connect`,
|
||||
}
|
||||
|
||||
// Global flags
|
||||
var (
|
||||
configFile string
|
||||
outputFormat string // json, table, csv
|
||||
verbose bool
|
||||
dateFrom string
|
||||
dateTo string
|
||||
)
|
||||
```
|
||||
|
||||
**Tasks:**
|
||||
- [ ] Install and configure cobra
|
||||
- [ ] Create root command with global flags
|
||||
- [ ] Implement configuration file loading
|
||||
- [ ] Add output formatting infrastructure
|
||||
- [ ] Create help text and usage examples
|
||||
|
||||
**Deliverables:**
|
||||
- [ ] Working CLI framework with `garth --help`
|
||||
- [ ] Configuration file support
|
||||
- [ ] Output formatting (JSON, table, CSV)
|
||||
|
||||
#### 1A.3: Configuration Management
|
||||
**Duration: 1 day**
|
||||
|
||||
```go
|
||||
// internal/config/config.go
|
||||
type Config struct {
|
||||
Auth struct {
|
||||
Email string `yaml:"email"`
|
||||
Domain string `yaml:"domain"`
|
||||
Session string `yaml:"session_file"`
|
||||
} `yaml:"auth"`
|
||||
|
||||
Output struct {
|
||||
Format string `yaml:"format"`
|
||||
File string `yaml:"file"`
|
||||
} `yaml:"output"`
|
||||
|
||||
Cache struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
TTL string `yaml:"ttl"`
|
||||
Dir string `yaml:"dir"`
|
||||
} `yaml:"cache"`
|
||||
}
|
||||
```
|
||||
|
||||
**Tasks:**
|
||||
- [ ] Design configuration schema
|
||||
- [ ] Implement config file loading/saving
|
||||
- [ ] Add environment variable support
|
||||
- [ ] Create config validation
|
||||
- [ ] Add config commands (`garth config init`, `garth config show`)
|
||||
|
||||
**Deliverables:**
|
||||
- [ ] Configuration system working
|
||||
- [ ] Default config file created
|
||||
- [ ] Config commands implemented
|
||||
|
||||
---
|
||||
|
||||
## Subphase 1B: Enhanced CLI Commands (Days 4-7)
|
||||
|
||||
### Objectives
|
||||
- Implement all major CLI commands
|
||||
- Add interactive features
|
||||
- Ensure consistent user experience
|
||||
|
||||
### Tasks
|
||||
|
||||
#### 1B.1: Authentication Commands
|
||||
**Duration: 1 day**
|
||||
|
||||
```bash
|
||||
# Target CLI interface
|
||||
garth auth login # Interactive login
|
||||
garth auth login --email user@example.com --password-stdin
|
||||
garth auth logout # Clear session
|
||||
garth auth status # Show auth status
|
||||
garth auth refresh # Refresh tokens
|
||||
```
|
||||
|
||||
```go
|
||||
// cmd/garth/auth.go
|
||||
var authCmd = &cobra.Command{
|
||||
Use: "auth",
|
||||
Short: "Authentication management",
|
||||
}
|
||||
|
||||
var loginCmd = &cobra.Command{
|
||||
Use: "login",
|
||||
Short: "Login to Garmin Connect",
|
||||
RunE: runLogin,
|
||||
}
|
||||
```
|
||||
|
||||
**Tasks:**
|
||||
- [ ] Implement `auth login` with interactive prompts
|
||||
- [ ] Add `auth logout` functionality
|
||||
- [ ] Create `auth status` command
|
||||
- [ ] Implement secure password input
|
||||
- [ ] Add MFA support (prepare for future)
|
||||
- [ ] Session validation and refresh
|
||||
|
||||
**Deliverables:**
|
||||
- [ ] All auth commands working
|
||||
- [ ] Secure credential handling
|
||||
- [ ] Session persistence working
|
||||
|
||||
#### 1B.2: Activity Commands
|
||||
**Duration: 2 days**
|
||||
|
||||
```bash
|
||||
# Target CLI interface
|
||||
garth activities list # Recent activities
|
||||
garth activities list --limit 50 --type running
|
||||
garth activities get 12345678 # Activity details
|
||||
garth activities download 12345678 --format gpx
|
||||
garth activities search --query "morning run"
|
||||
```
|
||||
|
||||
```go
|
||||
// pkg/garmin/activities.go
|
||||
type ActivityOptions struct {
|
||||
Limit int
|
||||
Offset int
|
||||
ActivityType string
|
||||
DateFrom time.Time
|
||||
DateTo time.Time
|
||||
}
|
||||
|
||||
type ActivityDetail struct {
|
||||
BasicInfo Activity
|
||||
Summary ActivitySummary
|
||||
Laps []Lap
|
||||
Metrics []Metric
|
||||
}
|
||||
```
|
||||
|
||||
**Tasks:**
|
||||
- [ ] Enhanced activity listing with filters
|
||||
- [ ] Activity detail fetching
|
||||
- [ ] Search functionality
|
||||
- [ ] Table formatting for activity lists
|
||||
- [ ] Activity download preparation (basic structure)
|
||||
- [ ] Date range filtering
|
||||
- [ ] Activity type filtering
|
||||
|
||||
**Deliverables:**
|
||||
- [ ] `activities list` with all filtering options
|
||||
- [ ] `activities get` showing detailed info
|
||||
- [ ] `activities search` functionality
|
||||
- [ ] Proper error handling and user feedback
|
||||
|
||||
#### 1B.3: Health Data Commands
|
||||
**Duration: 2 days**
|
||||
|
||||
```bash
|
||||
# Target CLI interface
|
||||
garth health sleep --from 2024-01-01 --to 2024-01-07
|
||||
garth health hrv --days 30
|
||||
garth health stress --week
|
||||
garth health bodybattery --yesterday
|
||||
```
|
||||
|
||||
**Tasks:**
|
||||
- [ ] Implement all health data commands
|
||||
- [ ] Add date range parsing utilities
|
||||
- [ ] Create consistent output formatting
|
||||
- [ ] Add data aggregation options
|
||||
- [ ] Implement caching for expensive operations
|
||||
- [ ] Error handling for missing data
|
||||
|
||||
**Deliverables:**
|
||||
- [ ] All health commands working
|
||||
- [ ] Consistent date filtering across commands
|
||||
- [ ] Proper data formatting and display
|
||||
|
||||
#### 1B.4: Statistics Commands
|
||||
**Duration: 1 day**
|
||||
|
||||
```bash
|
||||
# Target CLI interface
|
||||
garth stats steps --month
|
||||
garth stats distance --year
|
||||
garth stats calories --from 2024-01-01
|
||||
```
|
||||
|
||||
**Tasks:**
|
||||
- [ ] Implement statistics commands
|
||||
- [ ] Add aggregation periods (day, week, month, year)
|
||||
- [ ] Create summary statistics
|
||||
- [ ] Add trend analysis
|
||||
- [ ] Implement data export options
|
||||
|
||||
**Deliverables:**
|
||||
- [ ] All stats commands working
|
||||
- [ ] Multiple aggregation options
|
||||
- [ ] Export functionality
|
||||
|
||||
---
|
||||
|
||||
## Subphase 1C: Activity Download Implementation (Days 8-12)
|
||||
|
||||
### Objectives
|
||||
- Implement activity file downloading
|
||||
- Support multiple formats (GPX, TCX, FIT)
|
||||
- Add batch download capabilities
|
||||
|
||||
### Tasks
|
||||
|
||||
#### 1C.1: Core Download Infrastructure
|
||||
**Duration: 2 days**
|
||||
|
||||
```go
|
||||
// pkg/garmin/activities.go
|
||||
type DownloadOptions struct {
|
||||
Format string // "gpx", "tcx", "fit", "csv"
|
||||
Original bool // Download original uploaded file
|
||||
OutputDir string
|
||||
Filename string
|
||||
}
|
||||
|
||||
func (c *Client) DownloadActivity(id string, opts *DownloadOptions) error {
|
||||
// Implementation
|
||||
}
|
||||
```
|
||||
|
||||
**Tasks:**
|
||||
- [ ] Research Garmin's download endpoints
|
||||
- [ ] Implement format detection and conversion
|
||||
- [ ] Add file writing with proper naming
|
||||
- [ ] Implement progress indication
|
||||
- [ ] Add download validation
|
||||
- [ ] Error handling for failed downloads
|
||||
|
||||
**Deliverables:**
|
||||
- [ ] Working download for at least GPX format
|
||||
- [ ] Progress indication during download
|
||||
- [ ] Proper error handling
|
||||
|
||||
#### 1C.2: Multi-Format Support
|
||||
**Duration: 2 days**
|
||||
|
||||
**Tasks:**
|
||||
- [ ] Implement TCX format download
|
||||
- [ ] Implement FIT format download (if available)
|
||||
- [ ] Add CSV export for activity summaries
|
||||
- [ ] Format validation and conversion
|
||||
- [ ] Add format-specific options
|
||||
|
||||
**Deliverables:**
|
||||
- [ ] Support for GPX, TCX, and CSV formats
|
||||
- [ ] Format auto-detection
|
||||
- [ ] Format-specific download options
|
||||
|
||||
#### 1C.3: Batch Download Features
|
||||
**Duration: 1 day**
|
||||
|
||||
```bash
|
||||
# Target functionality
|
||||
garth activities download --all --type running --format gpx
|
||||
garth activities download --from 2024-01-01 --to 2024-01-31
|
||||
```
|
||||
|
||||
**Tasks:**
|
||||
- [ ] Implement batch download with filtering
|
||||
- [ ] Add parallel download support
|
||||
- [ ] Progress bars for multiple downloads
|
||||
- [ ] Resume interrupted downloads
|
||||
- [ ] Duplicate detection and handling
|
||||
|
||||
**Deliverables:**
|
||||
- [ ] Batch download working
|
||||
- [ ] Parallel processing implemented
|
||||
- [ ] Resume capability
|
||||
|
||||
---
|
||||
|
||||
## Subphase 1D: Missing Health Data Types (Days 13-15)
|
||||
|
||||
### Objectives
|
||||
- Implement VO2 max data fetching
|
||||
- Add heart rate zones
|
||||
- Complete missing health metrics
|
||||
|
||||
### Tasks
|
||||
|
||||
#### 1D.1: VO2 Max Implementation
|
||||
**Duration: 1 day**
|
||||
|
||||
```go
|
||||
// pkg/garmin/health.go
|
||||
type VO2MaxData struct {
|
||||
Running *VO2MaxReading `json:"running"`
|
||||
Cycling *VO2MaxReading `json:"cycling"`
|
||||
Updated time.Time `json:"updated"`
|
||||
History []VO2MaxHistory `json:"history"`
|
||||
}
|
||||
|
||||
type VO2MaxReading struct {
|
||||
Value float64 `json:"value"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Source string `json:"source"`
|
||||
Confidence string `json:"confidence"`
|
||||
}
|
||||
```
|
||||
|
||||
**Tasks:**
|
||||
- [ ] Research VO2 max API endpoints
|
||||
- [ ] Implement data fetching
|
||||
- [ ] Add historical data support
|
||||
- [ ] Create CLI command
|
||||
- [ ] Add data validation
|
||||
- [ ] Format output appropriately
|
||||
|
||||
**Deliverables:**
|
||||
- [ ] `garth health vo2max` command working
|
||||
- [ ] Historical data support
|
||||
- [ ] Both running and cycling metrics
|
||||
|
||||
#### 1D.2: Heart Rate Zones
|
||||
**Duration: 1 day**
|
||||
|
||||
```go
|
||||
type HeartRateZones struct {
|
||||
RestingHR int `json:"resting_hr"`
|
||||
MaxHR int `json:"max_hr"`
|
||||
LactateThreshold int `json:"lactate_threshold"`
|
||||
Zones []HRZone `json:"zones"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type HRZone struct {
|
||||
Zone int `json:"zone"`
|
||||
MinBPM int `json:"min_bpm"`
|
||||
MaxBPM int `json:"max_bpm"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
```
|
||||
|
||||
**Tasks:**
|
||||
- [ ] Implement HR zones API calls
|
||||
- [ ] Add zone calculation logic
|
||||
- [ ] Create CLI command
|
||||
- [ ] Add zone analysis features
|
||||
- [ ] Implement zone updates (if possible)
|
||||
|
||||
**Deliverables:**
|
||||
- [ ] `garth health hr-zones` command
|
||||
- [ ] Zone calculation and display
|
||||
- [ ] Integration with other health metrics
|
||||
|
||||
#### 1D.3: Additional Health Metrics
|
||||
**Duration: 1 day**
|
||||
|
||||
```go
|
||||
type WellnessData struct {
|
||||
Date time.Time `json:"date"`
|
||||
RestingHR *int `json:"resting_hr"`
|
||||
Weight *float64 `json:"weight"`
|
||||
BodyFat *float64 `json:"body_fat"`
|
||||
BMI *float64 `json:"bmi"`
|
||||
BodyWater *float64 `json:"body_water"`
|
||||
BoneMass *float64 `json:"bone_mass"`
|
||||
MuscleMass *float64 `json:"muscle_mass"`
|
||||
}
|
||||
```
|
||||
|
||||
**Tasks:**
|
||||
- [ ] Research additional wellness endpoints
|
||||
- [ ] Implement body composition data
|
||||
- [ ] Add resting heart rate trends
|
||||
- [ ] Create comprehensive wellness command
|
||||
- [ ] Add data correlation features
|
||||
|
||||
**Deliverables:**
|
||||
- [ ] Additional health metrics available
|
||||
- [ ] Wellness overview command
|
||||
- [ ] Data trend analysis
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 Testing & Quality Assurance (Days 14-15)
|
||||
|
||||
### Tasks
|
||||
|
||||
#### Integration Testing
|
||||
- [ ] End-to-end CLI testing
|
||||
- [ ] Authentication flow testing
|
||||
- [ ] Data fetching validation
|
||||
- [ ] Error handling verification
|
||||
|
||||
#### Documentation
|
||||
- [ ] Update README with new CLI commands
|
||||
- [ ] Add usage examples
|
||||
- [ ] Document configuration options
|
||||
- [ ] Create troubleshooting guide
|
||||
|
||||
#### Performance Testing
|
||||
- [ ] Concurrent operation testing
|
||||
- [ ] Memory usage validation
|
||||
- [ ] Download performance testing
|
||||
- [ ] Large dataset handling
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 Deliverables Checklist
|
||||
|
||||
### CLI Tool
|
||||
- [ ] Complete CLI with all major commands
|
||||
- [ ] Configuration file support
|
||||
- [ ] Multiple output formats (JSON, table, CSV)
|
||||
- [ ] Interactive authentication
|
||||
- [ ] Progress indicators for long operations
|
||||
|
||||
### Core Functionality
|
||||
- [ ] Activity listing with filtering
|
||||
- [ ] Activity detail fetching
|
||||
- [ ] Activity downloading (GPX, TCX, CSV)
|
||||
- [ ] All existing health data accessible via CLI
|
||||
- [ ] VO2 max and heart rate zone data
|
||||
|
||||
### Code Quality
|
||||
- [ ] Reorganized package structure
|
||||
- [ ] Consistent error handling
|
||||
- [ ] Comprehensive logging
|
||||
- [ ] Basic test coverage (>60%)
|
||||
- [ ] Documentation updated
|
||||
|
||||
### User Experience
|
||||
- [ ] Intuitive command structure
|
||||
- [ ] Helpful error messages
|
||||
- [ ] Progress feedback
|
||||
- [ ] Consistent data formatting
|
||||
- [ ] Working examples and documentation
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. **CLI Completeness**: All major Garmin data types accessible via CLI
|
||||
2. **Usability**: New users can get started within 5 minutes
|
||||
3. **Reliability**: Commands work consistently without errors
|
||||
4. **Performance**: Downloads and data fetching perform well
|
||||
5. **Documentation**: Clear examples and troubleshooting available
|
||||
|
||||
## Risks & Mitigations
|
||||
|
||||
| Risk | Impact | Mitigation |
|
||||
|------|--------|------------|
|
||||
| API endpoint changes | High | Create abstraction layer, add endpoint validation |
|
||||
| Authentication issues | High | Implement robust error handling and retry logic |
|
||||
| Download format limitations | Medium | Start with GPX, add others incrementally |
|
||||
| Performance with large datasets | Medium | Implement pagination and caching |
|
||||
| Package reorganization complexity | Medium | Do incrementally with thorough testing |
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Cobra CLI framework
|
||||
- Garmin Connect API stability
|
||||
- OAuth flow reliability
|
||||
- File system permissions for downloads
|
||||
- Network connectivity for API calls
|
||||
|
||||
This phase establishes the foundation for all subsequent development while delivering immediate value to users through a comprehensive CLI tool.
|
||||
@@ -14,9 +14,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"garmin-connect/garth/errors"
|
||||
"garmin-connect/garth/sso"
|
||||
"garmin-connect/garth/types"
|
||||
"github.com/sstent/go-garth/garth/errors"
|
||||
"github.com/sstent/go-garth/garth/sso"
|
||||
"github.com/sstent/go-garth/garth/types"
|
||||
)
|
||||
|
||||
// Client represents the Garmin Connect API client
|
||||
@@ -326,13 +326,18 @@ func (c *Client) Download(activityID string, filePath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetActivities retrieves recent activities
|
||||
func (c *Client) GetActivities(limit int) ([]types.Activity, error) {
|
||||
if limit <= 0 {
|
||||
limit = 10
|
||||
}
|
||||
// GetActivities retrieves activities filtered by date range
|
||||
func (c *Client) GetActivities(start, end time.Time) ([]types.Activity, error) {
|
||||
activitiesURL := fmt.Sprintf("https://connectapi.%s/activitylist-service/activities/search/activities", c.Domain)
|
||||
|
||||
activitiesURL := fmt.Sprintf("https://connectapi.%s/activitylist-service/activities/search/activities?limit=%d&start=0", c.Domain, limit)
|
||||
params := url.Values{}
|
||||
if !start.IsZero() {
|
||||
params.Add("startDate", start.Format("2006-01-02"))
|
||||
}
|
||||
if !end.IsZero() {
|
||||
params.Add("endDate", end.Format("2006-01-02"))
|
||||
}
|
||||
activitiesURL += "?" + params.Encode()
|
||||
|
||||
req, err := http.NewRequest("GET", activitiesURL, nil)
|
||||
if err != nil {
|
||||
18
pkg/garmin/types/types.go
Normal file
18
pkg/garmin/types/types.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package types
|
||||
|
||||
// DailyBodyBatteryStress represents Garmin's daily body battery and stress data
|
||||
type DailyBodyBatteryStress struct {
|
||||
CalendarDate string `json:"calendarDate"`
|
||||
BodyBattery int `json:"bodyBattery"`
|
||||
Stress int `json:"stress"`
|
||||
RestStress float64 `json:"restStress"`
|
||||
}
|
||||
|
||||
// Activity represents a Garmin activity
|
||||
type Activity struct {
|
||||
ActivityID int64 `json:"activityId"`
|
||||
StartTime string `json:"startTime"`
|
||||
ActivityType string `json:"activityType"`
|
||||
Duration float64 `json:"duration"`
|
||||
Distance float64 `json:"distance"`
|
||||
}
|
||||
252
portingplan.md
252
portingplan.md
@@ -1,252 +0,0 @@
|
||||
# Garth Python to Go Port Plan
|
||||
|
||||
## Overview
|
||||
Port the Python `garth` library to Go with feature parity. The existing Go code provides basic authentication and activity retrieval. This plan outlines the systematic porting of all Python modules.
|
||||
|
||||
## Current State Analysis
|
||||
**Existing Go code has:**
|
||||
- Basic SSO authentication flow (`main.go`)
|
||||
- OAuth1/OAuth2 token handling
|
||||
- Activity retrieval
|
||||
- Session persistence
|
||||
|
||||
**Missing (needs porting):**
|
||||
- All data models and retrieval methods
|
||||
- Stats modules
|
||||
- User profile/settings
|
||||
- Structured error handling
|
||||
- Client configuration options
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### 1. Project Structure Setup
|
||||
```
|
||||
garth/
|
||||
├── main.go (keep existing)
|
||||
├── client/
|
||||
│ ├── client.go (refactor from main.go)
|
||||
│ ├── auth.go (OAuth flows)
|
||||
│ └── sso.go (SSO authentication)
|
||||
├── data/
|
||||
│ ├── base.go
|
||||
│ ├── body_battery.go
|
||||
│ ├── hrv.go
|
||||
│ ├── sleep.go
|
||||
│ └── weight.go
|
||||
├── stats/
|
||||
│ ├── base.go
|
||||
│ ├── hrv.go
|
||||
│ ├── steps.go
|
||||
│ ├── stress.go
|
||||
│ └── [other stats].go
|
||||
├── users/
|
||||
│ ├── profile.go
|
||||
│ └── settings.go
|
||||
├── utils/
|
||||
│ └── utils.go
|
||||
└── types/
|
||||
└── tokens.go
|
||||
```
|
||||
|
||||
### 2. Core Client Refactoring (Priority 1)
|
||||
|
||||
**File: `client/client.go`**
|
||||
- Extract client logic from `main.go`
|
||||
- Port `src/garth/http.py` Client class
|
||||
- Key methods to implement:
|
||||
```go
|
||||
type Client struct {
|
||||
Domain string
|
||||
HTTPClient *http.Client
|
||||
OAuth1Token *OAuth1Token
|
||||
OAuth2Token *OAuth2Token
|
||||
// ... other fields from Python Client
|
||||
}
|
||||
|
||||
func (c *Client) Configure(opts ...ConfigOption) error
|
||||
func (c *Client) ConnectAPI(path, method string, data interface{}) (interface{}, error)
|
||||
func (c *Client) Download(path string) ([]byte, error)
|
||||
func (c *Client) Upload(filePath, uploadPath string) (map[string]interface{}, error)
|
||||
```
|
||||
|
||||
**Reference:** `src/garth/http.py` lines 23-280
|
||||
|
||||
### 3. Authentication Module (Priority 1)
|
||||
|
||||
**File: `client/auth.go`**
|
||||
- Port `src/garth/auth_tokens.py` token structures
|
||||
- Implement token expiration checking
|
||||
- Add MFA support placeholder
|
||||
|
||||
**File: `client/sso.go`**
|
||||
- Port SSO functions from `src/garth/sso.py`
|
||||
- Extract login logic from current `main.go`
|
||||
- Implement `ResumeLogin()` for MFA completion
|
||||
|
||||
**Reference:** `src/garth/sso.py` and `src/garth/auth_tokens.py`
|
||||
|
||||
### 4. Data Models Base (Priority 2)
|
||||
|
||||
**File: `data/base.go`**
|
||||
- Port `src/garth/data/_base.py` Data interface and base functionality
|
||||
- Implement concurrent data fetching pattern:
|
||||
```go
|
||||
type Data interface {
|
||||
Get(day time.Time, client *Client) (interface{}, error)
|
||||
List(end time.Time, days int, client *Client, maxWorkers int) ([]interface{}, error)
|
||||
}
|
||||
```
|
||||
|
||||
**Reference:** `src/garth/data/_base.py` lines 8-40
|
||||
|
||||
### 5. Body Battery Data (Priority 2)
|
||||
|
||||
**File: `data/body_battery.go`**
|
||||
- Port all structs from `src/garth/data/body_battery/` directory
|
||||
- Key structures to implement:
|
||||
```go
|
||||
type DailyBodyBatteryStress struct {
|
||||
UserProfilePK int `json:"userProfilePk"`
|
||||
CalendarDate time.Time `json:"calendarDate"`
|
||||
// ... all fields from Python class
|
||||
}
|
||||
|
||||
type BodyBatteryData struct {
|
||||
Event *BodyBatteryEvent `json:"event"`
|
||||
// ... other fields
|
||||
}
|
||||
```
|
||||
|
||||
**Reference:**
|
||||
- `src/garth/data/body_battery/daily_stress.py`
|
||||
- `src/garth/data/body_battery/events.py`
|
||||
- `src/garth/data/body_battery/readings.py`
|
||||
|
||||
### 6. Other Data Models (Priority 2)
|
||||
|
||||
**Files: `data/hrv.go`, `data/sleep.go`, `data/weight.go`**
|
||||
|
||||
For each file, port the corresponding Python module:
|
||||
|
||||
**HRV Data (`data/hrv.go`):**
|
||||
```go
|
||||
type HRVData struct {
|
||||
UserProfilePK int `json:"userProfilePk"`
|
||||
HRVSummary HRVSummary `json:"hrvSummary"`
|
||||
HRVReadings []HRVReading `json:"hrvReadings"`
|
||||
// ... rest of fields
|
||||
}
|
||||
```
|
||||
**Reference:** `src/garth/data/hrv.py`
|
||||
|
||||
**Sleep Data (`data/sleep.go`):**
|
||||
- Port `DailySleepDTO`, `SleepScores`, `SleepMovement` structs
|
||||
- Implement property methods as getter functions
|
||||
**Reference:** `src/garth/data/sleep.py`
|
||||
|
||||
**Weight Data (`data/weight.go`):**
|
||||
- Port `WeightData` struct with field validation
|
||||
- Implement date range fetching logic
|
||||
**Reference:** `src/garth/data/weight.py`
|
||||
|
||||
### 7. Stats Modules (Priority 3)
|
||||
|
||||
**File: `stats/base.go`**
|
||||
- Port `src/garth/stats/_base.py` Stats base class
|
||||
- Implement pagination logic for large date ranges
|
||||
|
||||
**Individual Stats Files:**
|
||||
Create separate files for each stat type, porting from corresponding Python files:
|
||||
- `stats/hrv.go` ← `src/garth/stats/hrv.py`
|
||||
- `stats/steps.go` ← `src/garth/stats/steps.py`
|
||||
- `stats/stress.go` ← `src/garth/stats/stress.py`
|
||||
- `stats/sleep.go` ← `src/garth/stats/sleep.py`
|
||||
- `stats/hydration.go` ← `src/garth/stats/hydration.py`
|
||||
- `stats/intensity_minutes.go` ← `src/garth/stats/intensity_minutes.py`
|
||||
|
||||
**Reference:** All files in `src/garth/stats/`
|
||||
|
||||
### 8. User Profile and Settings (Priority 3)
|
||||
|
||||
**File: `users/profile.go`**
|
||||
```go
|
||||
type UserProfile struct {
|
||||
ID int `json:"id"`
|
||||
ProfileID int `json:"profileId"`
|
||||
DisplayName string `json:"displayName"`
|
||||
// ... all other fields from Python UserProfile
|
||||
}
|
||||
|
||||
func (up *UserProfile) Get(client *Client) error
|
||||
```
|
||||
|
||||
**File: `users/settings.go`**
|
||||
- Port all nested structs: `PowerFormat`, `FirstDayOfWeek`, `WeatherLocation`, etc.
|
||||
- Implement `UserSettings.Get()` method
|
||||
|
||||
**Reference:** `src/garth/users/profile.py` and `src/garth/users/settings.py`
|
||||
|
||||
### 9. Utilities (Priority 3)
|
||||
|
||||
**File: `utils/utils.go`**
|
||||
```go
|
||||
func CamelToSnake(s string) string
|
||||
func CamelToSnakeDict(m map[string]interface{}) map[string]interface{}
|
||||
func FormatEndDate(end interface{}) time.Time
|
||||
func DateRange(end time.Time, days int) []time.Time
|
||||
func GetLocalizedDateTime(gmtTimestamp, localTimestamp int64) time.Time
|
||||
```
|
||||
|
||||
**Reference:** `src/garth/utils.py`
|
||||
|
||||
### 10. Error Handling (Priority 4)
|
||||
|
||||
**File: `errors/errors.go`**
|
||||
```go
|
||||
type GarthError struct {
|
||||
Message string
|
||||
Cause error
|
||||
}
|
||||
|
||||
type GarthHTTPError struct {
|
||||
GarthError
|
||||
StatusCode int
|
||||
Response string
|
||||
}
|
||||
```
|
||||
|
||||
**Reference:** `src/garth/exc.py`
|
||||
|
||||
### 11. CLI Tool (Priority 4)
|
||||
|
||||
**File: `cmd/garth/main.go`**
|
||||
- Port `src/garth/cli.py` functionality
|
||||
- Support login and token output
|
||||
|
||||
### 12. Testing Strategy
|
||||
|
||||
For each module:
|
||||
1. Create `*_test.go` files with unit tests
|
||||
2. Mock HTTP responses using Python examples as expected data
|
||||
3. Test error handling paths
|
||||
4. Add integration tests with real API calls (optional)
|
||||
|
||||
### 13. Key Implementation Notes
|
||||
|
||||
1. **JSON Handling:** Use struct tags for proper JSON marshaling/unmarshaling
|
||||
2. **Time Handling:** Convert Python datetime objects to Go `time.Time`
|
||||
3. **Error Handling:** Wrap errors with context using `fmt.Errorf`
|
||||
4. **Concurrency:** Use goroutines and channels for the concurrent data fetching in `List()` methods
|
||||
5. **HTTP Client:** Reuse the existing HTTP client setup with proper timeout and retry logic
|
||||
|
||||
### 14. Development Order
|
||||
|
||||
1. Start with client refactoring and authentication
|
||||
2. Implement base data structures and one data model (body battery)
|
||||
3. Add remaining data models
|
||||
4. Implement stats modules
|
||||
5. Add user profile/settings
|
||||
6. Complete utilities and error handling
|
||||
7. Add CLI tool and tests
|
||||
|
||||
This plan provides a systematic approach to achieving feature parity with the Python library while maintaining Go idioms and best practices.
|
||||
187
portingplan_3.md
187
portingplan_3.md
@@ -1,187 +0,0 @@
|
||||
# Implementation Plan for Garmin Connect Go Client - Feature Parity
|
||||
|
||||
## Phase 1: Complete Core Data Types (Priority: High)
|
||||
|
||||
### 1.1 Complete HRV Data Implementation
|
||||
**File**: `garth/data/hrv.go`
|
||||
**Reference**: Python `garth/hrv.py` and API examples in README
|
||||
|
||||
**Tasks**:
|
||||
- Implement `Get()` method calling `/wellness-service/wellness/dailyHrvData/{username}?date={date}`
|
||||
- Complete `ParseHRVReadings()` function based on Python parsing logic
|
||||
- Add missing fields to `HRVSummary` struct (reference Python HRVSummary dataclass)
|
||||
- Implement `List()` method using BaseData pattern
|
||||
|
||||
### 1.2 Complete Weight Data Implementation
|
||||
**File**: `garth/data/weight.go`
|
||||
**Reference**: Python `garth/weight.py`
|
||||
|
||||
**Tasks**:
|
||||
- Implement `Get()` method calling `/weight-service/weight/dateRange?startDate={date}&endDate={date}`
|
||||
- Add all missing fields from Python WeightData dataclass
|
||||
- Implement proper unit conversions (grams vs kg)
|
||||
- Add `List()` method for date ranges
|
||||
|
||||
### 1.3 Complete Sleep Data Implementation
|
||||
**File**: `garth/data/sleep.go`
|
||||
**Reference**: Python `garth/sleep.py`
|
||||
|
||||
**Tasks**:
|
||||
- Fix `Get()` method to properly parse nested sleep data structures
|
||||
- Add missing `SleepScores` fields from Python implementation
|
||||
- Implement sleep quality calculations and derived properties
|
||||
- Add proper timezone handling for sleep timestamps
|
||||
|
||||
## Phase 2: Add Missing Core API Methods (Priority: High)
|
||||
|
||||
### 2.1 Add ConnectAPI Method
|
||||
**File**: `garth/client/client.go`
|
||||
**Reference**: Python `garth/client.py` `connectapi()` method
|
||||
|
||||
**Tasks**:
|
||||
- Add `ConnectAPI(path, params, method)` method to Client struct
|
||||
- Support GET/POST with query parameters and JSON body
|
||||
- Return raw JSON response for flexible endpoint access
|
||||
- Add proper error handling and authentication headers
|
||||
|
||||
### 2.2 Add File Operations
|
||||
**File**: `garth/client/client.go`
|
||||
**Reference**: Python `garth/client.py` upload/download methods
|
||||
|
||||
**Tasks**:
|
||||
- Complete `Upload()` method for FIT file uploads to `/upload-service/upload`
|
||||
- Add `Download()` method for activity exports
|
||||
- Handle multipart form uploads properly
|
||||
- Add progress callbacks for large files
|
||||
|
||||
## Phase 3: Complete Stats Implementation (Priority: Medium)
|
||||
|
||||
### 3.1 Fix Stats Pagination
|
||||
**File**: `garth/stats/base.go`
|
||||
**Reference**: Python `garth/stats.py` pagination logic
|
||||
|
||||
**Tasks**:
|
||||
- Fix recursive pagination in `BaseStats.List()` method
|
||||
- Ensure proper date range handling for >28 day requests
|
||||
- Add proper error handling for missing data pages
|
||||
- Test with large date ranges (>365 days)
|
||||
|
||||
### 3.2 Add Missing Stats Types
|
||||
**Files**: `garth/stats/` directory
|
||||
**Reference**: Python `garth/stats/` directory
|
||||
|
||||
**Tasks**:
|
||||
- Add `WeeklySteps`, `WeeklyStress`, `WeeklyHRV` types
|
||||
- Implement monthly and yearly aggregation types if present in Python
|
||||
- Add any missing daily stats types by comparing Python vs Go stats files
|
||||
|
||||
## Phase 4: Add Advanced Features (Priority: Medium)
|
||||
|
||||
### 4.1 Add Data Validation
|
||||
**Files**: All data types
|
||||
**Reference**: Python Pydantic dataclass validators
|
||||
|
||||
**Tasks**:
|
||||
- Add `Validate()` methods to all data structures
|
||||
- Implement field validation rules from Python Pydantic models
|
||||
- Add data sanitization for API responses
|
||||
- Handle missing/null fields gracefully
|
||||
|
||||
### 4.2 Add Derived Properties
|
||||
**Files**: `garth/data/` directory
|
||||
**Reference**: Python dataclass `@property` methods
|
||||
|
||||
**Tasks**:
|
||||
- Add calculated fields to BodyBattery (current_level, max_level, min_level, battery_change)
|
||||
- Add sleep duration calculations and sleep efficiency
|
||||
- Add stress level aggregations and summaries
|
||||
- Implement timezone-aware timestamp helpers
|
||||
|
||||
## Phase 5: Enhanced Error Handling & Logging (Priority: Low)
|
||||
|
||||
### 5.1 Improve Error Types
|
||||
**File**: `garth/errors/errors.go`
|
||||
**Reference**: Python `garth/exc.py`
|
||||
|
||||
**Tasks**:
|
||||
- Add specific error types for rate limiting, MFA required, etc.
|
||||
- Implement error retry logic with exponential backoff
|
||||
- Add request/response logging for debugging
|
||||
- Handle partial failures in List() operations
|
||||
|
||||
### 5.2 Add Configuration Options
|
||||
**File**: `garth/client/client.go`
|
||||
**Reference**: Python `garth/configure.py`
|
||||
|
||||
**Tasks**:
|
||||
- Add proxy support configuration
|
||||
- Add custom timeout settings
|
||||
- Add SSL verification options
|
||||
- Add custom user agent configuration
|
||||
|
||||
## Phase 6: Testing & Documentation (Priority: Medium)
|
||||
|
||||
### 6.1 Add Integration Tests
|
||||
**File**: `garth/integration_test.go`
|
||||
**Reference**: Python test files
|
||||
|
||||
**Tasks**:
|
||||
- Add real API tests with saved session files
|
||||
- Test all data types with real Garmin data
|
||||
- Add benchmark comparisons with Python timings
|
||||
- Test error scenarios and edge cases
|
||||
|
||||
### 6.2 Add Usage Examples
|
||||
**Files**: `examples/` directory (create new)
|
||||
**Reference**: Python README examples
|
||||
|
||||
**Tasks**:
|
||||
- Port all Python README examples to Go
|
||||
- Add Jupyter notebook equivalent examples
|
||||
- Create data export utilities matching Python functionality
|
||||
- Add data visualization examples using Go libraries
|
||||
|
||||
## Implementation Guidelines
|
||||
|
||||
### Code Standards
|
||||
- Follow existing Go package structure
|
||||
- Use existing error handling patterns
|
||||
- Maintain interface compatibility where possible
|
||||
- Add comprehensive godoc comments
|
||||
|
||||
### Testing Strategy
|
||||
- Add unit tests for each new method
|
||||
- Use table-driven tests for data parsing
|
||||
- Mock HTTP responses for reliable testing
|
||||
- Test timezone handling thoroughly
|
||||
|
||||
### Data Structure Mapping
|
||||
- Compare Python dataclass fields to Go struct fields
|
||||
- Ensure JSON tag mapping matches API responses
|
||||
- Handle optional fields with pointers (`*int`, `*string`)
|
||||
- Use proper Go time.Time for timestamps
|
||||
|
||||
### API Endpoint Discovery
|
||||
- Check Python source for endpoint URLs
|
||||
- Verify parameter names and formats
|
||||
- Test with actual API calls using saved sessions
|
||||
- Document any API differences found
|
||||
|
||||
## Completion Criteria
|
||||
|
||||
Each phase is complete when:
|
||||
1. All methods have working implementations (no `return nil, nil`)
|
||||
2. Unit tests pass with >80% coverage
|
||||
3. Integration tests pass with real API data
|
||||
4. Documentation includes usage examples
|
||||
5. Benchmarks show performance is maintained or improved
|
||||
|
||||
## Estimated Timeline
|
||||
- Phase 1: 2-3 weeks
|
||||
- Phase 2: 1-2 weeks
|
||||
- Phase 3: 1 week
|
||||
- Phase 4: 2 weeks
|
||||
- Phase 5: 1 week
|
||||
- Phase 6: 1 week
|
||||
|
||||
**Total**: 8-10 weeks for complete feature parity
|
||||
@@ -1,670 +0,0 @@
|
||||
# Complete Garth Python to Go Port - Implementation Plan
|
||||
|
||||
## Current Status
|
||||
The Go port has excellent architecture (85% complete) but needs implementation of core API methods and data models. All structure, error handling, and utilities are in place.
|
||||
|
||||
## Phase 1: Core API Implementation (Priority 1 - Week 1)
|
||||
|
||||
### Task 1.1: Implement Client.ConnectAPI Method
|
||||
**File:** `garth/client/client.go`
|
||||
**Reference:** `src/garth/http.py` lines 206-217
|
||||
|
||||
Add this method to the Client struct:
|
||||
|
||||
```go
|
||||
func (c *Client) ConnectAPI(path, method string, data interface{}) (interface{}, error) {
|
||||
url := fmt.Sprintf("https://connectapi.%s%s", c.Domain, path)
|
||||
|
||||
var body io.Reader
|
||||
if data != nil && (method == "POST" || method == "PUT") {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, &errors.APIError{GarthHTTPError: errors.GarthHTTPError{
|
||||
GarthError: errors.GarthError{Message: "Failed to marshal request data", Cause: err}}}
|
||||
}
|
||||
body = bytes.NewReader(jsonData)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, url, body)
|
||||
if err != nil {
|
||||
return nil, &errors.APIError{GarthHTTPError: errors.GarthHTTPError{
|
||||
GarthError: errors.GarthError{Message: "Failed to create request", Cause: err}}}
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", c.AuthToken)
|
||||
req.Header.Set("User-Agent", "GCM-iOS-5.7.2.1")
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, &errors.APIError{GarthHTTPError: errors.GarthHTTPError{
|
||||
GarthError: errors.GarthError{Message: "API request failed", Cause: err}}}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == 204 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
return nil, &errors.APIError{GarthHTTPError: errors.GarthHTTPError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Response: string(bodyBytes),
|
||||
GarthError: errors.GarthError{Message: "API error"}}}
|
||||
}
|
||||
|
||||
var result interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, &errors.IOError{GarthError: errors.GarthError{
|
||||
Message: "Failed to parse response", Cause: err}}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
```
|
||||
|
||||
### Task 1.2: Add File Download/Upload Methods
|
||||
**File:** `garth/client/client.go`
|
||||
**Reference:** `src/garth/http.py` lines 219-230, 232-244
|
||||
|
||||
```go
|
||||
func (c *Client) Download(path string) ([]byte, error) {
|
||||
resp, err := c.ConnectAPI(path, "GET", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("https://connectapi.%s%s", c.Domain, path)
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", c.AuthToken)
|
||||
req.Header.Set("User-Agent", "GCM-iOS-5.7.2.1")
|
||||
|
||||
httpResp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer httpResp.Body.Close()
|
||||
|
||||
return io.ReadAll(httpResp.Body)
|
||||
}
|
||||
|
||||
func (c *Client) Upload(filePath, uploadPath string) (map[string]interface{}, error) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, &errors.IOError{GarthError: errors.GarthError{
|
||||
Message: "Failed to open file", Cause: err}}
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var b bytes.Buffer
|
||||
writer := multipart.NewWriter(&b)
|
||||
part, err := writer.CreateFormFile("file", filepath.Base(filePath))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = io.Copy(part, file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
writer.Close()
|
||||
|
||||
url := fmt.Sprintf("https://connectapi.%s%s", c.Domain, uploadPath)
|
||||
req, err := http.NewRequest("POST", url, &b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", c.AuthToken)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
```
|
||||
|
||||
## Phase 2: Data Model Implementation (Week 1-2)
|
||||
|
||||
### Task 2.1: Complete Body Battery Implementation
|
||||
**File:** `garth/data/body_battery.go`
|
||||
**Reference:** `src/garth/data/body_battery/daily_stress.py` lines 55-77
|
||||
|
||||
Replace the stub `Get()` method:
|
||||
|
||||
```go
|
||||
func (d *DailyBodyBatteryStress) Get(day time.Time, client *client.Client) (interface{}, error) {
|
||||
dateStr := day.Format("2006-01-02")
|
||||
path := fmt.Sprintf("/wellness-service/wellness/dailyStress/%s", dateStr)
|
||||
|
||||
response, err := client.ConnectAPI(path, "GET", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if response == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
responseMap, ok := response.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, &errors.IOError{GarthError: errors.GarthError{
|
||||
Message: "Invalid response format"}}
|
||||
}
|
||||
|
||||
snakeResponse := utils.CamelToSnakeDict(responseMap)
|
||||
|
||||
jsonBytes, err := json.Marshal(snakeResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result DailyBodyBatteryStress
|
||||
if err := json.Unmarshal(jsonBytes, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
```
|
||||
|
||||
### Task 2.2: Complete Sleep Data Implementation
|
||||
**File:** `garth/data/sleep.go`
|
||||
**Reference:** `src/garth/data/sleep.py` lines 91-107
|
||||
|
||||
```go
|
||||
func (d *DailySleepDTO) Get(day time.Time, client *client.Client) (interface{}, error) {
|
||||
dateStr := day.Format("2006-01-02")
|
||||
path := fmt.Sprintf("/wellness-service/wellness/dailySleepData/%s?nonSleepBufferMinutes=60&date=%s",
|
||||
client.Username, dateStr)
|
||||
|
||||
response, err := client.ConnectAPI(path, "GET", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if response == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
responseMap := response.(map[string]interface{})
|
||||
snakeResponse := utils.CamelToSnakeDict(responseMap)
|
||||
|
||||
dailySleepDto, exists := snakeResponse["daily_sleep_dto"].(map[string]interface{})
|
||||
if !exists || dailySleepDto["id"] == nil {
|
||||
return nil, nil // No sleep data
|
||||
}
|
||||
|
||||
jsonBytes, err := json.Marshal(snakeResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result struct {
|
||||
DailySleepDTO *DailySleepDTO `json:"daily_sleep_dto"`
|
||||
SleepMovement []SleepMovement `json:"sleep_movement"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(jsonBytes, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
```
|
||||
|
||||
### Task 2.3: Complete HRV Implementation
|
||||
**File:** `garth/data/hrv.go`
|
||||
**Reference:** `src/garth/data/hrv.py` lines 68-78
|
||||
|
||||
```go
|
||||
func (h *HRVData) Get(day time.Time, client *client.Client) (interface{}, error) {
|
||||
dateStr := day.Format("2006-01-02")
|
||||
path := fmt.Sprintf("/hrv-service/hrv/%s", dateStr)
|
||||
|
||||
response, err := client.ConnectAPI(path, "GET", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if response == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
responseMap := response.(map[string]interface{})
|
||||
snakeResponse := utils.CamelToSnakeDict(responseMap)
|
||||
|
||||
jsonBytes, err := json.Marshal(snakeResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result HRVData
|
||||
if err := json.Unmarshal(jsonBytes, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
```
|
||||
|
||||
### Task 2.4: Complete Weight Implementation
|
||||
**File:** `garth/data/weight.go`
|
||||
**Reference:** `src/garth/data/weight.py` lines 39-52 and 54-74
|
||||
|
||||
```go
|
||||
func (w *WeightData) Get(day time.Time, client *client.Client) (interface{}, error) {
|
||||
dateStr := day.Format("2006-01-02")
|
||||
path := fmt.Sprintf("/weight-service/weight/dayview/%s", dateStr)
|
||||
|
||||
response, err := client.ConnectAPI(path, "GET", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if response == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
responseMap := response.(map[string]interface{})
|
||||
dayWeightList, exists := responseMap["dateWeightList"].([]interface{})
|
||||
if !exists || len(dayWeightList) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Get first weight entry
|
||||
firstEntry := dayWeightList[0].(map[string]interface{})
|
||||
snakeResponse := utils.CamelToSnakeDict(firstEntry)
|
||||
|
||||
jsonBytes, err := json.Marshal(snakeResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result WeightData
|
||||
if err := json.Unmarshal(jsonBytes, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
```
|
||||
|
||||
## Phase 3: Stats Module Implementation (Week 2)
|
||||
|
||||
### Task 3.1: Create Stats Base
|
||||
**File:** `garth/stats/base.go` (new file)
|
||||
**Reference:** `src/garth/stats/_base.py`
|
||||
|
||||
```go
|
||||
package stats
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"garmin-connect/garth/client"
|
||||
"garmin-connect/garth/utils"
|
||||
)
|
||||
|
||||
type Stats interface {
|
||||
List(end time.Time, period int, client *client.Client) ([]interface{}, error)
|
||||
}
|
||||
|
||||
type BaseStats struct {
|
||||
Path string
|
||||
PageSize int
|
||||
}
|
||||
|
||||
func (b *BaseStats) List(end time.Time, period int, client *client.Client) ([]interface{}, error) {
|
||||
endDate := utils.FormatEndDate(end)
|
||||
|
||||
if period > b.PageSize {
|
||||
// Handle pagination - get first page
|
||||
page, err := b.fetchPage(endDate, b.PageSize, client)
|
||||
if err != nil || len(page) == 0 {
|
||||
return page, err
|
||||
}
|
||||
|
||||
// Get remaining pages recursively
|
||||
remainingStart := endDate.AddDate(0, 0, -b.PageSize)
|
||||
remainingPeriod := period - b.PageSize
|
||||
remainingData, err := b.List(remainingStart, remainingPeriod, client)
|
||||
if err != nil {
|
||||
return page, err
|
||||
}
|
||||
|
||||
return append(remainingData, page...), nil
|
||||
}
|
||||
|
||||
return b.fetchPage(endDate, period, client)
|
||||
}
|
||||
|
||||
func (b *BaseStats) fetchPage(end time.Time, period int, client *client.Client) ([]interface{}, error) {
|
||||
var start time.Time
|
||||
var path string
|
||||
|
||||
if strings.Contains(b.Path, "daily") {
|
||||
start = end.AddDate(0, 0, -(period - 1))
|
||||
path = strings.Replace(b.Path, "{start}", start.Format("2006-01-02"), 1)
|
||||
path = strings.Replace(path, "{end}", end.Format("2006-01-02"), 1)
|
||||
} else {
|
||||
path = strings.Replace(b.Path, "{end}", end.Format("2006-01-02"), 1)
|
||||
path = strings.Replace(path, "{period}", fmt.Sprintf("%d", period), 1)
|
||||
}
|
||||
|
||||
response, err := client.ConnectAPI(path, "GET", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if response == nil {
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
|
||||
responseSlice, ok := response.([]interface{})
|
||||
if !ok || len(responseSlice) == 0 {
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
|
||||
var results []interface{}
|
||||
for _, item := range responseSlice {
|
||||
itemMap := item.(map[string]interface{})
|
||||
|
||||
// Handle nested "values" structure
|
||||
if values, exists := itemMap["values"]; exists {
|
||||
valuesMap := values.(map[string]interface{})
|
||||
for k, v := range valuesMap {
|
||||
itemMap[k] = v
|
||||
}
|
||||
delete(itemMap, "values")
|
||||
}
|
||||
|
||||
snakeItem := utils.CamelToSnakeDict(itemMap)
|
||||
results = append(results, snakeItem)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
```
|
||||
|
||||
### Task 3.2: Create Individual Stats Types
|
||||
**Files:** Create these files in `garth/stats/`
|
||||
**Reference:** All files in `src/garth/stats/`
|
||||
|
||||
**`steps.go`** (Reference: `src/garth/stats/steps.py`):
|
||||
```go
|
||||
package stats
|
||||
|
||||
import "time"
|
||||
|
||||
const BASE_STEPS_PATH = "/usersummary-service/stats/steps"
|
||||
|
||||
type DailySteps struct {
|
||||
CalendarDate time.Time `json:"calendar_date"`
|
||||
TotalSteps *int `json:"total_steps"`
|
||||
TotalDistance *int `json:"total_distance"`
|
||||
StepGoal int `json:"step_goal"`
|
||||
BaseStats
|
||||
}
|
||||
|
||||
func NewDailySteps() *DailySteps {
|
||||
return &DailySteps{
|
||||
BaseStats: BaseStats{
|
||||
Path: BASE_STEPS_PATH + "/daily/{start}/{end}",
|
||||
PageSize: 28,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type WeeklySteps struct {
|
||||
CalendarDate time.Time `json:"calendar_date"`
|
||||
TotalSteps int `json:"total_steps"`
|
||||
AverageSteps float64 `json:"average_steps"`
|
||||
AverageDistance float64 `json:"average_distance"`
|
||||
TotalDistance float64 `json:"total_distance"`
|
||||
WellnessDataDaysCount int `json:"wellness_data_days_count"`
|
||||
BaseStats
|
||||
}
|
||||
|
||||
func NewWeeklySteps() *WeeklySteps {
|
||||
return &WeeklySteps{
|
||||
BaseStats: BaseStats{
|
||||
Path: BASE_STEPS_PATH + "/weekly/{end}/{period}",
|
||||
PageSize: 52,
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**`stress.go`** (Reference: `src/garth/stats/stress.py`):
|
||||
```go
|
||||
package stats
|
||||
|
||||
import "time"
|
||||
|
||||
const BASE_STRESS_PATH = "/usersummary-service/stats/stress"
|
||||
|
||||
type DailyStress struct {
|
||||
CalendarDate time.Time `json:"calendar_date"`
|
||||
OverallStressLevel int `json:"overall_stress_level"`
|
||||
RestStressDuration *int `json:"rest_stress_duration"`
|
||||
LowStressDuration *int `json:"low_stress_duration"`
|
||||
MediumStressDuration *int `json:"medium_stress_duration"`
|
||||
HighStressDuration *int `json:"high_stress_duration"`
|
||||
BaseStats
|
||||
}
|
||||
|
||||
func NewDailyStress() *DailyStress {
|
||||
return &DailyStress{
|
||||
BaseStats: BaseStats{
|
||||
Path: BASE_STRESS_PATH + "/daily/{start}/{end}",
|
||||
PageSize: 28,
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Create similar files for:
|
||||
- `hydration.go` → Reference `src/garth/stats/hydration.py`
|
||||
- `intensity_minutes.go` → Reference `src/garth/stats/intensity_minutes.py`
|
||||
- `sleep.go` → Reference `src/garth/stats/sleep.py`
|
||||
- `hrv.go` → Reference `src/garth/stats/hrv.py`
|
||||
|
||||
## Phase 4: Complete Data Interface Implementation (Week 2)
|
||||
|
||||
### Task 4.1: Fix BaseData List Implementation
|
||||
**File:** `garth/data/base.go`
|
||||
|
||||
Update the List method to properly use the BaseData pattern:
|
||||
|
||||
```go
|
||||
func (b *BaseData) List(end time.Time, days int, c *client.Client, maxWorkers int) ([]interface{}, []error) {
|
||||
if maxWorkers < 1 {
|
||||
maxWorkers = 10 // Match Python's MAX_WORKERS
|
||||
}
|
||||
|
||||
dates := utils.DateRange(end, days)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
workCh := make(chan time.Time, days)
|
||||
resultsCh := make(chan result, days)
|
||||
|
||||
type result struct {
|
||||
data interface{}
|
||||
err error
|
||||
}
|
||||
|
||||
// Worker function
|
||||
worker := func() {
|
||||
defer wg.Done()
|
||||
for date := range workCh {
|
||||
data, err := b.Get(date, c)
|
||||
resultsCh <- result{data: data, err: err}
|
||||
}
|
||||
}
|
||||
|
||||
// Start workers
|
||||
wg.Add(maxWorkers)
|
||||
for i := 0; i < maxWorkers; i++ {
|
||||
go worker()
|
||||
}
|
||||
|
||||
// Send work
|
||||
go func() {
|
||||
for _, date := range dates {
|
||||
workCh <- date
|
||||
}
|
||||
close(workCh)
|
||||
}()
|
||||
|
||||
// Close results channel when workers are done
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultsCh)
|
||||
}()
|
||||
|
||||
var results []interface{}
|
||||
var errs []error
|
||||
|
||||
for r := range resultsCh {
|
||||
if r.err != nil {
|
||||
errs = append(errs, r.err)
|
||||
} else if r.data != nil {
|
||||
results = append(results, r.data)
|
||||
}
|
||||
}
|
||||
|
||||
return results, errs
|
||||
}
|
||||
```
|
||||
|
||||
## Phase 5: Testing and Documentation (Week 3)
|
||||
|
||||
### Task 5.1: Create Integration Tests
|
||||
**File:** `garth/integration_test.go` (new file)
|
||||
|
||||
```go
|
||||
package garth_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
"garmin-connect/garth/client"
|
||||
"garmin-connect/garth/data"
|
||||
)
|
||||
|
||||
func TestBodyBatteryIntegration(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
c, err := client.NewClient("garmin.com")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Load test session
|
||||
err = c.LoadSession("test_session.json")
|
||||
if err != nil {
|
||||
t.Skip("No test session available")
|
||||
}
|
||||
|
||||
bb := &data.DailyBodyBatteryStress{}
|
||||
result, err := bb.Get(time.Now().AddDate(0, 0, -1), c)
|
||||
|
||||
assert.NoError(t, err)
|
||||
if result != nil {
|
||||
bbData := result.(*data.DailyBodyBatteryStress)
|
||||
assert.NotZero(t, bbData.UserProfilePK)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Task 5.2: Update Package Exports
|
||||
**File:** `garth/__init__.go` (new file)
|
||||
|
||||
Create a package-level API that matches Python's `__init__.py`:
|
||||
|
||||
```go
|
||||
package garth
|
||||
|
||||
import (
|
||||
"garmin-connect/garth/client"
|
||||
"garmin-connect/garth/data"
|
||||
"garmin-connect/garth/stats"
|
||||
)
|
||||
|
||||
// Re-export main types for convenience
|
||||
type Client = client.Client
|
||||
|
||||
// Data types
|
||||
type BodyBatteryData = data.DailyBodyBatteryStress
|
||||
type HRVData = data.HRVData
|
||||
type SleepData = data.DailySleepDTO
|
||||
type WeightData = data.WeightData
|
||||
|
||||
// Stats types
|
||||
type DailySteps = stats.DailySteps
|
||||
type DailyStress = stats.DailyStress
|
||||
type DailyHRV = stats.DailyHRV
|
||||
|
||||
// Main functions
|
||||
var (
|
||||
NewClient = client.NewClient
|
||||
Login = client.Login
|
||||
)
|
||||
```
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
### Week 1 (Core Implementation):
|
||||
- [ ] Client.ConnectAPI method
|
||||
- [ ] Download/Upload methods
|
||||
- [ ] Body Battery Get() implementation
|
||||
- [ ] Sleep Data Get() implementation
|
||||
- [ ] End-to-end test with real API
|
||||
|
||||
### Week 2 (Complete Feature Set):
|
||||
- [ ] HRV and Weight Get() implementations
|
||||
- [ ] Complete stats module (all 7 types)
|
||||
- [ ] BaseData List() method fix
|
||||
- [ ] Integration tests
|
||||
|
||||
### Week 3 (Polish and Documentation):
|
||||
- [ ] Package-level exports
|
||||
- [ ] README with examples
|
||||
- [ ] Performance testing vs Python
|
||||
- [ ] CLI tool verification
|
||||
|
||||
## Key Implementation Notes
|
||||
|
||||
1. **Error Handling**: Use the existing comprehensive error types
|
||||
2. **Date Formats**: Always use `time.Time` and convert to "2006-01-02" for API calls
|
||||
3. **Response Parsing**: Always use `utils.CamelToSnakeDict` before unmarshaling
|
||||
4. **Concurrency**: The existing BaseData.List() handles worker pools correctly
|
||||
5. **Testing**: Use `testutils.MockJSONResponse` for unit tests
|
||||
|
||||
## Success Criteria
|
||||
|
||||
Port is complete when:
|
||||
- All Python data models have working Get() methods
|
||||
- All Python stats types are implemented
|
||||
- CLI tool outputs same format as Python
|
||||
- Integration tests pass against real API
|
||||
- Performance is equal or better than Python
|
||||
|
||||
**Estimated Effort:** 2-3 weeks for junior developer with this detailed plan.
|
||||
Reference in New Issue
Block a user