porting - part2 wk1 done

This commit is contained in:
2025-09-07 18:34:53 -07:00
parent dccb072f73
commit 5c44f01bc3
5 changed files with 919 additions and 4 deletions

130
garth/client/http.go Normal file
View File

@@ -0,0 +1,130 @@
package client
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"garmin-connect/garth/errors"
)
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
}
func (c *Client) Download(path string) ([]byte, error) {
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")
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(resp.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
}

View File

@@ -1,10 +1,14 @@
package data
import (
"encoding/json"
"fmt"
"sort"
"time"
"garmin-connect/garth/client"
"garmin-connect/garth/errors"
"garmin-connect/garth/utils"
)
// DailyBodyBatteryStress represents complete daily Body Battery and stress data
@@ -109,8 +113,37 @@ func ParseStressReadings(valuesArray [][]int) []StressReading {
// Get implements the Data interface for DailyBodyBatteryStress
func (d *DailyBodyBatteryStress) Get(day time.Time, client *client.Client) (any, error) {
// Implementation to be added
return nil, nil
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
}
// List implements the Data interface for concurrent fetching

View File

@@ -1,9 +1,13 @@
package data
import (
"encoding/json"
"fmt"
"time"
"garmin-connect/garth/client"
"garmin-connect/garth/errors"
"garmin-connect/garth/utils"
)
// SleepScores represents sleep scoring data
@@ -37,8 +41,47 @@ type DailySleepDTO struct {
// Get implements the Data interface for DailySleepDTO
func (d *DailySleepDTO) Get(day time.Time, client *client.Client) (any, error) {
// Implementation to be added
return nil, nil
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, ok := response.(map[string]interface{})
if !ok {
return nil, &errors.IOError{GarthError: errors.GarthError{
Message: "Invalid response format"}}
}
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
}
// List implements the Data interface for concurrent fetching

39
garth/integration_test.go Normal file
View File

@@ -0,0 +1,39 @@
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")
if err != nil {
t.Fatalf("Failed to create client: %v", 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)
if err != nil {
t.Errorf("Get failed: %v", err)
}
if result != nil {
bbData := result.(*data.DailyBodyBatteryStress)
if bbData.UserProfilePK == 0 {
t.Error("UserProfilePK is zero")
}
}
}