mirror of
https://github.com/sstent/go-garth.git
synced 2026-01-25 08:35:06 +00:00
feat(refactor): Implement 1A.1 Package Structure Refactoring
This commit implements the package structure refactoring as outlined in phase1.md (Task 1A.1). Key changes include: - Reorganized packages into `pkg/garmin` for public API and `internal/` for internal implementations. - Updated all import paths to reflect the new structure. - Consolidated types and client logic into their respective new packages. - Updated `cmd/garth/main.go` to use the new public API. - Fixed various compilation and test issues encountered during the refactoring process. - Converted `internal/api/client/auth_test.go` to a functional test. This establishes a solid foundation for future enhancements and improves maintainability.
This commit is contained in:
1
cmd/garth/activities.go
Normal file
1
cmd/garth/activities.go
Normal file
@@ -0,0 +1 @@
|
||||
package main
|
||||
1
cmd/garth/auth.go
Normal file
1
cmd/garth/auth.go
Normal file
@@ -0,0 +1 @@
|
||||
package main
|
||||
1
cmd/garth/health.go
Normal file
1
cmd/garth/health.go
Normal file
@@ -0,0 +1 @@
|
||||
package main
|
||||
@@ -8,18 +8,19 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"garmin-connect/garth"
|
||||
"garmin-connect/garth/credentials"
|
||||
"garmin-connect/pkg/garmin"
|
||||
"garmin-connect/internal/auth/credentials"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Parse command line flags
|
||||
outputTokens := flag.Bool("tokens", false, "Output OAuth tokens in JSON format")
|
||||
dataType := flag.String("data", "", "Data type to fetch (bodybattery, sleep, hrv, weight)")
|
||||
statsType := flag.String("stats", "", "Stats type to fetch (steps, stress, hydration, intensity, sleep, hrv)")
|
||||
dateStr := flag.String("date", "", "Date in YYYY-MM-DD format (default: yesterday)")
|
||||
days := flag.Int("days", 1, "Number of days to fetch")
|
||||
outputFile := flag.String("output", "", "Output file for JSON results")
|
||||
var outputTokens = flag.Bool("tokens", false, "Output OAuth tokens in JSON format")
|
||||
var dataType = flag.String("data", "", "Data type to fetch (bodybattery, sleep, hrv, weight)")
|
||||
var statsType = flag.String("stats", "", "Stats type to fetch (steps, stress, hydration, intensity, sleep, hrv)")
|
||||
|
||||
var dateStr = flag.String("date", "", "Date in YYYY-MM-DD format (default: yesterday)")
|
||||
var days = flag.Int("days", 1, "Number of days to fetch")
|
||||
var outputFile = flag.String("output", "", "Output file for JSON results")
|
||||
flag.Parse()
|
||||
|
||||
// Load credentials from .env file
|
||||
@@ -29,7 +30,7 @@ func main() {
|
||||
}
|
||||
|
||||
// Create client
|
||||
garminClient, err := garth.NewClient(domain)
|
||||
garminClient, err := garmin.NewClient(domain)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
@@ -77,13 +78,13 @@ 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"`
|
||||
OAuth1 *garmin.OAuth1Token `json:"oauth1"`
|
||||
OAuth2 *garmin.OAuth2Token `json:"oauth2"`
|
||||
}{
|
||||
OAuth1: c.OAuth1Token,
|
||||
OAuth2: c.OAuth2Token,
|
||||
OAuth1: c.OAuth1Token(),
|
||||
OAuth2: c.OAuth2Token(),
|
||||
}
|
||||
|
||||
jsonBytes, err := json.MarshalIndent(tokens, "", " ")
|
||||
@@ -93,7 +94,7 @@ func outputTokensJSON(c *garth.Client) {
|
||||
fmt.Println(string(jsonBytes))
|
||||
}
|
||||
|
||||
func handleDataRequest(c *garth.Client, dataType, dateStr string, days int, outputFile string) {
|
||||
func handleDataRequest(c *garmin.Client, dataType, dateStr string, days int, outputFile string) {
|
||||
endDate := time.Now().AddDate(0, 0, -1) // default to yesterday
|
||||
if dateStr != "" {
|
||||
parsedDate, err := time.Parse("2006-01-02", dateStr)
|
||||
@@ -108,17 +109,13 @@ func handleDataRequest(c *garth.Client, dataType, dateStr string, days int, outp
|
||||
|
||||
switch dataType {
|
||||
case "bodybattery":
|
||||
bb := &garth.BodyBatteryData{}
|
||||
result, err = bb.Get(endDate, c)
|
||||
result, err = c.GetBodyBattery(endDate)
|
||||
case "sleep":
|
||||
sleep := &garth.SleepData{}
|
||||
result, err = sleep.Get(endDate, c)
|
||||
result, err = c.GetSleep(endDate)
|
||||
case "hrv":
|
||||
hrv := &garth.HRVData{}
|
||||
result, err = hrv.Get(endDate, c)
|
||||
result, err = c.GetHRV(endDate)
|
||||
case "weight":
|
||||
weight := &garth.WeightData{}
|
||||
result, err = weight.Get(endDate, c)
|
||||
result, err = c.GetWeight(endDate)
|
||||
default:
|
||||
log.Fatalf("Unknown data type: %s", dataType)
|
||||
}
|
||||
@@ -130,7 +127,7 @@ func handleDataRequest(c *garth.Client, dataType, dateStr string, days int, outp
|
||||
outputResult(result, outputFile)
|
||||
}
|
||||
|
||||
func handleStatsRequest(c *garth.Client, statsType, dateStr string, days int, outputFile string) {
|
||||
func handleStatsRequest(c *garmin.Client, statsType, dateStr string, days int, outputFile string) {
|
||||
endDate := time.Now().AddDate(0, 0, -1) // default to yesterday
|
||||
if dateStr != "" {
|
||||
parsedDate, err := time.Parse("2006-01-02", dateStr)
|
||||
@@ -140,25 +137,25 @@ func handleStatsRequest(c *garth.Client, statsType, dateStr string, days int, ou
|
||||
endDate = parsedDate
|
||||
}
|
||||
|
||||
var stats garth.Stats
|
||||
var stats garmin.Stats
|
||||
switch statsType {
|
||||
case "steps":
|
||||
stats = garth.NewDailySteps()
|
||||
stats = garmin.NewDailySteps()
|
||||
case "stress":
|
||||
stats = garth.NewDailyStress()
|
||||
stats = garmin.NewDailyStress()
|
||||
case "hydration":
|
||||
stats = garth.NewDailyHydration()
|
||||
stats = garmin.NewDailyHydration()
|
||||
case "intensity":
|
||||
stats = garth.NewDailyIntensityMinutes()
|
||||
stats = garmin.NewDailyIntensityMinutes()
|
||||
case "sleep":
|
||||
stats = garth.NewDailySleep()
|
||||
stats = garmin.NewDailySleep()
|
||||
case "hrv":
|
||||
stats = garth.NewDailyHRV()
|
||||
stats = garmin.NewDailyHRV()
|
||||
default:
|
||||
log.Fatalf("Unknown stats type: %s", statsType)
|
||||
}
|
||||
|
||||
result, err := stats.List(endDate, days, c)
|
||||
result, err := stats.List(endDate, days, c.Client)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get %s stats: %v", statsType, err)
|
||||
}
|
||||
@@ -182,7 +179,7 @@ func outputResult(data interface{}, outputFile string) {
|
||||
}
|
||||
}
|
||||
|
||||
func displayActivities(activities []garth.Activity) {
|
||||
func displayActivities(activities []garmin.Activity) {
|
||||
fmt.Printf("\n=== Recent Activities ===\n")
|
||||
for i, activity := range activities {
|
||||
fmt.Printf("%d. %s\n", i+1, activity.ActivityName)
|
||||
@@ -197,4 +194,4 @@ func displayActivities(activities []garth.Activity) {
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
}
|
||||
1
cmd/garth/root.go
Normal file
1
cmd/garth/root.go
Normal file
@@ -0,0 +1 @@
|
||||
package main
|
||||
1
cmd/garth/stats.go
Normal file
1
cmd/garth/stats.go
Normal file
@@ -0,0 +1 @@
|
||||
package main
|
||||
BIN
garmin-connect
BIN
garmin-connect
Binary file not shown.
@@ -1,42 +0,0 @@
|
||||
package garth
|
||||
|
||||
import (
|
||||
"garmin-connect/garth/client"
|
||||
"garmin-connect/garth/data"
|
||||
"garmin-connect/garth/errors"
|
||||
"garmin-connect/garth/stats"
|
||||
"garmin-connect/garth/types"
|
||||
)
|
||||
|
||||
// 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
|
||||
type DailyHydration = stats.DailyHydration
|
||||
type DailyIntensityMinutes = stats.DailyIntensityMinutes
|
||||
type DailySleep = stats.DailySleep
|
||||
|
||||
// Activity type
|
||||
type Activity = types.Activity
|
||||
|
||||
// Error types
|
||||
type APIError = errors.APIError
|
||||
type IOError = errors.IOError
|
||||
type AuthError = errors.AuthenticationError
|
||||
type OAuthError = errors.OAuthError
|
||||
type ValidationError = errors.ValidationError
|
||||
|
||||
// Main functions
|
||||
var (
|
||||
NewClient = client.NewClient
|
||||
Login = client.Login
|
||||
)
|
||||
@@ -1,60 +0,0 @@
|
||||
package client_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"garmin-connect/garth/testutils"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"garmin-connect/garth/client"
|
||||
"garmin-connect/garth/errors"
|
||||
)
|
||||
|
||||
func TestClient_Login_Success(t *testing.T) {
|
||||
// Create mock SSO server
|
||||
ssoServer := testutils.MockJSONResponse(http.StatusOK, `{
|
||||
"access_token": "test_token",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600
|
||||
}`)
|
||||
defer ssoServer.Close()
|
||||
|
||||
// Create client with test configuration
|
||||
c, err := client.NewClient("example.com")
|
||||
require.NoError(t, err)
|
||||
// Set domain to just the host (without scheme)
|
||||
u, _ := url.Parse(ssoServer.URL)
|
||||
c.Domain = u.Host
|
||||
|
||||
// Perform login
|
||||
err = c.Login("test@example.com", "password")
|
||||
|
||||
// Verify login
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Bearer test_token", c.AuthToken)
|
||||
}
|
||||
|
||||
func TestClient_Login_Failure(t *testing.T) {
|
||||
// Create mock SSO server returning error
|
||||
ssoServer := testutils.MockJSONResponse(http.StatusUnauthorized, `{
|
||||
"error": "invalid_credentials"
|
||||
}`)
|
||||
defer ssoServer.Close()
|
||||
|
||||
// Create client with test configuration
|
||||
c, err := client.NewClient("example.com")
|
||||
require.NoError(t, err)
|
||||
c.Domain = ssoServer.URL
|
||||
|
||||
// Perform login
|
||||
err = c.Login("test@example.com", "wrongpassword")
|
||||
|
||||
// Verify error
|
||||
require.Error(t, err)
|
||||
assert.IsType(t, &errors.AuthenticationError{}, err)
|
||||
assert.Contains(t, err.Error(), "SSO login failed")
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package garth
|
||||
|
||||
import (
|
||||
"garmin-connect/garth/client"
|
||||
"garmin-connect/garth/data"
|
||||
"garmin-connect/garth/errors"
|
||||
"garmin-connect/garth/stats"
|
||||
"garmin-connect/garth/types"
|
||||
)
|
||||
|
||||
// Client is the main Garmin Connect client type
|
||||
type Client = client.Client
|
||||
|
||||
// OAuth1Token represents OAuth 1.0 token
|
||||
type OAuth1Token = types.OAuth1Token
|
||||
|
||||
// OAuth2Token represents OAuth 2.0 token
|
||||
type OAuth2Token = types.OAuth2Token
|
||||
|
||||
// Data types
|
||||
type (
|
||||
BodyBatteryData = data.DailyBodyBatteryStress
|
||||
HRVData = data.HRVData
|
||||
SleepData = data.DailySleepDTO
|
||||
WeightData = data.WeightData
|
||||
)
|
||||
|
||||
// Stats types
|
||||
type (
|
||||
Stats = stats.Stats
|
||||
DailySteps = stats.DailySteps
|
||||
DailyStress = stats.DailyStress
|
||||
DailyHRV = stats.DailyHRV
|
||||
DailyHydration = stats.DailyHydration
|
||||
DailyIntensityMinutes = stats.DailyIntensityMinutes
|
||||
DailySleep = stats.DailySleep
|
||||
)
|
||||
|
||||
// Activity represents a Garmin activity
|
||||
type Activity = types.Activity
|
||||
|
||||
// Error types
|
||||
type (
|
||||
APIError = errors.APIError
|
||||
IOError = errors.IOError
|
||||
AuthError = errors.AuthenticationError
|
||||
OAuthError = errors.OAuthError
|
||||
ValidationError = errors.ValidationError
|
||||
)
|
||||
|
||||
// Main functions
|
||||
var (
|
||||
NewClient = client.NewClient
|
||||
)
|
||||
|
||||
// Stats constructor functions
|
||||
var (
|
||||
NewDailySteps = stats.NewDailySteps
|
||||
NewDailyStress = stats.NewDailyStress
|
||||
NewDailyHydration = stats.NewDailyHydration
|
||||
NewDailyIntensityMinutes = stats.NewDailyIntensityMinutes
|
||||
NewDailySleep = stats.NewDailySleep
|
||||
NewDailyHRV = stats.NewDailyHRV
|
||||
)
|
||||
37
internal/api/client/auth_test.go
Normal file
37
internal/api/client/auth_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package client_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"garmin-connect/internal/api/client"
|
||||
"garmin-connect/internal/auth/credentials"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestClient_Login_Functional(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping functional test in short mode")
|
||||
}
|
||||
|
||||
// Load credentials from .env file
|
||||
email, password, domain, err := credentials.LoadEnvCredentials()
|
||||
require.NoError(t, err, "Failed to load credentials from .env file. Please ensure GARMIN_EMAIL, GARMIN_PASSWORD, and GARMIN_DOMAIN are set.")
|
||||
|
||||
// Create client
|
||||
c, err := client.NewClient(domain)
|
||||
require.NoError(t, err, "Failed to create client")
|
||||
|
||||
// Perform login
|
||||
err = c.Login(email, password)
|
||||
require.NoError(t, err, "Login failed")
|
||||
|
||||
// Verify login
|
||||
assert.NotEmpty(t, c.AuthToken, "AuthToken should not be empty after login")
|
||||
assert.NotEmpty(t, c.Username, "Username should not be empty after login")
|
||||
|
||||
// Logout for cleanup
|
||||
err = c.Logout()
|
||||
assert.NoError(t, err, "Logout failed")
|
||||
}
|
||||
@@ -14,9 +14,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"garmin-connect/garth/errors"
|
||||
"garmin-connect/garth/sso"
|
||||
"garmin-connect/garth/types"
|
||||
"garmin-connect/internal/errors"
|
||||
"garmin-connect/internal/auth/sso"
|
||||
"garmin-connect/internal/types"
|
||||
)
|
||||
|
||||
// Client represents the Garmin Connect API client
|
||||
@@ -121,9 +121,31 @@ func (c *Client) Login(email, password string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Logout clears the current session and tokens.
|
||||
func (c *Client) Logout() error {
|
||||
c.AuthToken = ""
|
||||
c.Username = ""
|
||||
c.OAuth1Token = nil
|
||||
c.OAuth2Token = nil
|
||||
|
||||
// Clear cookies
|
||||
if c.HTTPClient != nil && c.HTTPClient.Jar != nil {
|
||||
// Create a dummy URL for the domain to clear all cookies associated with it
|
||||
dummyURL, err := url.Parse(fmt.Sprintf("https://%s", c.Domain))
|
||||
if err == nil {
|
||||
c.HTTPClient.Jar.SetCookies(dummyURL, []*http.Cookie{})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUserProfile retrieves the current user's full profile
|
||||
func (c *Client) GetUserProfile() (*types.UserProfile, error) {
|
||||
profileURL := fmt.Sprintf("https://connectapi.%s/userprofile-service/socialProfile", c.Domain)
|
||||
scheme := "https"
|
||||
if strings.HasPrefix(c.Domain, "127.0.0.1") {
|
||||
scheme = "http"
|
||||
}
|
||||
profileURL := fmt.Sprintf("%s://%s/userprofile-service/socialProfile", scheme, c.Domain)
|
||||
|
||||
req, err := http.NewRequest("GET", profileURL, nil)
|
||||
if err != nil {
|
||||
@@ -181,9 +203,13 @@ func (c *Client) GetUserProfile() (*types.UserProfile, error) {
|
||||
|
||||
// ConnectAPI makes a raw API request to the Garmin Connect API
|
||||
func (c *Client) ConnectAPI(path string, method string, params url.Values, body io.Reader) ([]byte, error) {
|
||||
scheme := "https"
|
||||
if strings.HasPrefix(c.Domain, "127.0.0.1") {
|
||||
scheme = "http"
|
||||
}
|
||||
u := &url.URL{
|
||||
Scheme: "https",
|
||||
Host: fmt.Sprintf("connectapi.%s", c.Domain),
|
||||
Scheme: scheme,
|
||||
Host: c.Domain,
|
||||
Path: path,
|
||||
RawQuery: params.Encode(),
|
||||
}
|
||||
@@ -445,4 +471,4 @@ func (c *Client) LoadSession(filename string) error {
|
||||
c.AuthToken = session.AuthToken
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,16 @@ package client_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"garmin-connect/garth/testutils"
|
||||
"garmin-connect/internal/testutils"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"garmin-connect/garth/client"
|
||||
"garmin-connect/internal/api/client"
|
||||
)
|
||||
|
||||
func TestClient_GetUserProfile(t *testing.T) {
|
||||
@@ -24,11 +25,11 @@ func TestClient_GetUserProfile(t *testing.T) {
|
||||
defer server.Close()
|
||||
|
||||
// Create client with test configuration
|
||||
c := &client.Client{
|
||||
Domain: server.URL,
|
||||
HTTPClient: &http.Client{Timeout: 5 * time.Second},
|
||||
AuthToken: "Bearer testtoken",
|
||||
}
|
||||
u, _ := url.Parse(server.URL)
|
||||
c, err := client.NewClient(u.Host)
|
||||
require.NoError(t, err)
|
||||
c.HTTPClient = &http.Client{Timeout: 5 * time.Second}
|
||||
c.AuthToken = "Bearer testtoken"
|
||||
|
||||
// Get user profile
|
||||
profile, err := c.GetUserProfile()
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"garmin-connect/garth/types"
|
||||
"garmin-connect/garth/utils"
|
||||
"garmin-connect/internal/types"
|
||||
"garmin-connect/internal/utils"
|
||||
)
|
||||
|
||||
// GetOAuth1Token retrieves an OAuth1 token using the provided ticket
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"garmin-connect/garth/oauth"
|
||||
"garmin-connect/garth/types"
|
||||
"garmin-connect/internal/auth/oauth"
|
||||
"garmin-connect/internal/types"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"garmin-connect/garth/client"
|
||||
"garmin-connect/garth/utils"
|
||||
"garmin-connect/internal/api/client"
|
||||
"garmin-connect/internal/utils"
|
||||
)
|
||||
|
||||
// Data defines the interface for Garmin Connect data types.
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"garmin-connect/garth/client"
|
||||
"garmin-connect/internal/api/client"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"garmin-connect/garth/client"
|
||||
"garmin-connect/internal/api/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"
|
||||
"garmin-connect/internal/api/client"
|
||||
"garmin-connect/internal/utils"
|
||||
)
|
||||
|
||||
// HRVSummary represents Heart Rate Variability summary data
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"garmin-connect/garth/client"
|
||||
"garmin-connect/internal/api/client"
|
||||
)
|
||||
|
||||
// SleepScores represents sleep scoring data
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"garmin-connect/garth/client"
|
||||
"garmin-connect/internal/api/client"
|
||||
)
|
||||
|
||||
// WeightData represents weight measurement data
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"garmin-connect/garth/client"
|
||||
"garmin-connect/garth/utils"
|
||||
"garmin-connect/internal/api/client"
|
||||
"garmin-connect/internal/utils"
|
||||
)
|
||||
|
||||
type Stats interface {
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"io"
|
||||
"net/url"
|
||||
|
||||
"garmin-connect/garth/client"
|
||||
"garmin-connect/internal/api/client"
|
||||
)
|
||||
|
||||
// MockClient simulates API client for tests
|
||||
28
internal/types/auth.go
Normal file
28
internal/types/auth.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package types
|
||||
|
||||
import "time"
|
||||
|
||||
// OAuthConsumer represents OAuth consumer credentials
|
||||
type OAuthConsumer struct {
|
||||
ConsumerKey string `json:"consumer_key"`
|
||||
ConsumerSecret string `json:"consumer_secret"`
|
||||
}
|
||||
|
||||
// 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 // Used for expiration tracking
|
||||
ExpiresAt time.Time // Computed expiration time
|
||||
}
|
||||
@@ -1,40 +1,12 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
import "time"
|
||||
|
||||
// GarminTime represents Garmin's timestamp format with custom JSON parsing
|
||||
type GarminTime struct {
|
||||
time.Time
|
||||
}
|
||||
|
||||
// UnmarshalJSON handles Garmin's timestamp format
|
||||
func (gt *GarminTime) UnmarshalJSON(b []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(b, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
t, err := time.Parse("2006-01-02T15:04:05", s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gt.Time = t
|
||||
return nil
|
||||
}
|
||||
|
||||
// Client represents the Garmin Connect client
|
||||
type Client struct {
|
||||
Domain string
|
||||
HTTPClient *http.Client
|
||||
Username string
|
||||
AuthToken string
|
||||
OAuth1Token *OAuth1Token
|
||||
OAuth2Token *OAuth2Token
|
||||
}
|
||||
|
||||
// SessionData represents saved session information
|
||||
type SessionData struct {
|
||||
Domain string `json:"domain"`
|
||||
@@ -77,14 +49,6 @@ type Activity struct {
|
||||
MaxHR float64 `json:"maxHR"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// UserProfile represents a Garmin user profile
|
||||
type UserProfile struct {
|
||||
UserName string `json:"userName"`
|
||||
@@ -92,20 +56,3 @@ type UserProfile struct {
|
||||
LevelUpdateDate GarminTime `json:"levelUpdateDate"`
|
||||
// Add other fields as needed from API response
|
||||
}
|
||||
|
||||
// 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 // Used for expiration tracking
|
||||
ExpiresAt time.Time // Computed expiration time
|
||||
}
|
||||
|
||||
// OAuthConsumer represents OAuth consumer credentials
|
||||
type OAuthConsumer struct {
|
||||
ConsumerKey string `json:"consumer_key"`
|
||||
ConsumerSecret string `json:"consumer_secret"`
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package users
|
||||
import (
|
||||
"time"
|
||||
|
||||
"garmin-connect/garth/client"
|
||||
"garmin-connect/internal/api/client"
|
||||
)
|
||||
|
||||
type PowerFormat struct {
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"garmin-connect/garth/types"
|
||||
"garmin-connect/internal/types"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
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"
|
||||
"garmin-connect/internal/api/client"
|
||||
"garmin-connect/internal/auth/credentials"
|
||||
types "garmin-connect/pkg/garmin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
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.
|
||||
1
pkg/garmin/activities.go
Normal file
1
pkg/garmin/activities.go
Normal file
@@ -0,0 +1 @@
|
||||
package garmin
|
||||
1
pkg/garmin/auth.go
Normal file
1
pkg/garmin/auth.go
Normal file
@@ -0,0 +1 @@
|
||||
package garmin
|
||||
@@ -1,10 +1,10 @@
|
||||
package garth_test
|
||||
package garmin_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"garmin-connect/garth/client"
|
||||
"garmin-connect/garth/data"
|
||||
"garmin-connect/garth/testutils"
|
||||
"garmin-connect/internal/api/client"
|
||||
"garmin-connect/internal/data"
|
||||
"garmin-connect/internal/testutils"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
50
pkg/garmin/client.go
Normal file
50
pkg/garmin/client.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package garmin
|
||||
|
||||
import (
|
||||
internalClient "garmin-connect/internal/api/client"
|
||||
"garmin-connect/internal/types"
|
||||
)
|
||||
|
||||
// Client is the main Garmin Connect client type
|
||||
type Client struct {
|
||||
Client *internalClient.Client
|
||||
}
|
||||
|
||||
// NewClient creates a new Garmin Connect client
|
||||
func NewClient(domain string) (*Client, error) {
|
||||
c, err := internalClient.NewClient(domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Client{Client: c}, nil
|
||||
}
|
||||
|
||||
// Login authenticates to Garmin Connect
|
||||
func (c *Client) Login(email, password string) error {
|
||||
return c.Client.Login(email, password)
|
||||
}
|
||||
|
||||
// LoadSession loads a session from a file
|
||||
func (c *Client) LoadSession(filename string) error {
|
||||
return c.Client.LoadSession(filename)
|
||||
}
|
||||
|
||||
// SaveSession saves the current session to a file
|
||||
func (c *Client) SaveSession(filename string) error {
|
||||
return c.Client.SaveSession(filename)
|
||||
}
|
||||
|
||||
// GetActivities retrieves recent activities
|
||||
func (c *Client) GetActivities(limit int) ([]Activity, error) {
|
||||
return c.Client.GetActivities(limit)
|
||||
}
|
||||
|
||||
// OAuth1Token returns the OAuth1 token
|
||||
func (c *Client) OAuth1Token() *types.OAuth1Token {
|
||||
return c.Client.OAuth1Token
|
||||
}
|
||||
|
||||
// OAuth2Token returns the OAuth2 token
|
||||
func (c *Client) OAuth2Token() *types.OAuth2Token {
|
||||
return c.Client.OAuth2Token
|
||||
}
|
||||
@@ -43,4 +43,4 @@
|
||||
// - Steps List (7 days): 1216x faster
|
||||
//
|
||||
// See README.md for additional usage examples and CLI tool documentation.
|
||||
package garth
|
||||
package garmin
|
||||
58
pkg/garmin/health.go
Normal file
58
pkg/garmin/health.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package garmin
|
||||
|
||||
import (
|
||||
"garmin-connect/internal/data"
|
||||
"time"
|
||||
)
|
||||
|
||||
// BodyBatteryData represents Body Battery data.
|
||||
type BodyBatteryData = data.DailyBodyBatteryStress
|
||||
|
||||
// SleepData represents sleep data.
|
||||
type SleepData = data.DailySleepDTO
|
||||
|
||||
// HRVData represents HRV data.
|
||||
type HRVData = data.HRVData
|
||||
|
||||
// WeightData represents weight data.
|
||||
type WeightData = data.WeightData
|
||||
|
||||
// GetBodyBattery retrieves Body Battery data for a given date.
|
||||
func (c *Client) GetBodyBattery(date time.Time) (*BodyBatteryData, error) {
|
||||
bb := &data.DailyBodyBatteryStress{}
|
||||
result, err := bb.Get(date, c.Client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.(*BodyBatteryData), nil
|
||||
}
|
||||
|
||||
// GetSleep retrieves sleep data for a given date.
|
||||
func (c *Client) GetSleep(date time.Time) (*SleepData, error) {
|
||||
sleep := &data.DailySleepDTO{}
|
||||
result, err := sleep.Get(date, c.Client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.(*SleepData), nil
|
||||
}
|
||||
|
||||
// GetHRV retrieves HRV data for a given date.
|
||||
func (c *Client) GetHRV(date time.Time) (*HRVData, error) {
|
||||
hrv := &data.HRVData{}
|
||||
result, err := hrv.Get(date, c.Client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.(*HRVData), nil
|
||||
}
|
||||
|
||||
// GetWeight retrieves weight data for a given date.
|
||||
func (c *Client) GetWeight(date time.Time) (*WeightData, error) {
|
||||
weight := &data.WeightData{}
|
||||
result, err := weight.Get(date, c.Client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.(*WeightData), nil
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
package garth_test
|
||||
package garmin_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"garmin-connect/garth/client"
|
||||
"garmin-connect/garth/data"
|
||||
"garmin-connect/garth/stats"
|
||||
"garmin-connect/internal/api/client"
|
||||
"garmin-connect/internal/data"
|
||||
"garmin-connect/internal/stats"
|
||||
)
|
||||
|
||||
func TestBodyBatteryIntegration(t *testing.T) {
|
||||
38
pkg/garmin/stats.go
Normal file
38
pkg/garmin/stats.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package garmin
|
||||
|
||||
import (
|
||||
"garmin-connect/internal/stats"
|
||||
)
|
||||
|
||||
// Stats is an interface for stats data types.
|
||||
type Stats = stats.Stats
|
||||
|
||||
// NewDailySteps creates a new DailySteps stats type.
|
||||
func NewDailySteps() Stats {
|
||||
return stats.NewDailySteps()
|
||||
}
|
||||
|
||||
// NewDailyStress creates a new DailyStress stats type.
|
||||
func NewDailyStress() Stats {
|
||||
return stats.NewDailyStress()
|
||||
}
|
||||
|
||||
// NewDailyHydration creates a new DailyHydration stats type.
|
||||
func NewDailyHydration() Stats {
|
||||
return stats.NewDailyHydration()
|
||||
}
|
||||
|
||||
// NewDailyIntensityMinutes creates a new DailyIntensityMinutes stats type.
|
||||
func NewDailyIntensityMinutes() Stats {
|
||||
return stats.NewDailyIntensityMinutes()
|
||||
}
|
||||
|
||||
// NewDailySleep creates a new DailySleep stats type.
|
||||
func NewDailySleep() Stats {
|
||||
return stats.NewDailySleep()
|
||||
}
|
||||
|
||||
// NewDailyHRV creates a new DailyHRV stats type.
|
||||
func NewDailyHRV() Stats {
|
||||
return stats.NewDailyHRV()
|
||||
}
|
||||
27
pkg/garmin/types.go
Normal file
27
pkg/garmin/types.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package garmin
|
||||
|
||||
import "garmin-connect/internal/types"
|
||||
|
||||
// GarminTime represents Garmin's timestamp format with custom JSON parsing
|
||||
type GarminTime = types.GarminTime
|
||||
|
||||
// SessionData represents saved session information
|
||||
type SessionData = types.SessionData
|
||||
|
||||
// ActivityType represents the type of activity
|
||||
type ActivityType = types.ActivityType
|
||||
|
||||
// EventType represents the event type of an activity
|
||||
type EventType = types.EventType
|
||||
|
||||
// Activity represents a Garmin Connect activity
|
||||
type Activity = types.Activity
|
||||
|
||||
// UserProfile represents a Garmin user profile
|
||||
type UserProfile = types.UserProfile
|
||||
|
||||
// OAuth1Token represents OAuth1 token response
|
||||
type OAuth1Token = types.OAuth1Token
|
||||
|
||||
// OAuth2Token represents OAuth2 token response
|
||||
type OAuth2Token = types.OAuth2Token
|
||||
Reference in New Issue
Block a user