This commit is contained in:
2025-09-21 11:03:52 -07:00
parent 667790030e
commit e04cd5160e
138 changed files with 17338 additions and 0 deletions

101
internal/stats/base.go Normal file
View File

@@ -0,0 +1,101 @@
package stats
import (
"encoding/json"
"fmt"
"strings"
"time"
"go-garth/internal/api/client"
"go-garth/internal/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)
var allData []interface{}
var errs []error
for period > 0 {
pageSize := b.PageSize
if period < pageSize {
pageSize = period
}
page, err := b.fetchPage(endDate, pageSize, client)
if err != nil {
errs = append(errs, err)
// Continue to next page even if current fails
} else {
allData = append(page, allData...)
}
// Move to previous page
endDate = endDate.AddDate(0, 0, -pageSize)
period -= pageSize
}
// Return partial data with aggregated errors
var finalErr error
if len(errs) > 0 {
finalErr = fmt.Errorf("partial failure: %v", errs)
}
return allData, finalErr
}
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)
}
data, err := client.ConnectAPI(path, "GET", nil, nil)
if err != nil {
return nil, err
}
if len(data) == 0 {
return []interface{}{}, nil
}
var responseSlice []map[string]interface{}
if err := json.Unmarshal(data, &responseSlice); err != nil {
return nil, err
}
if len(responseSlice) == 0 {
return []interface{}{}, nil
}
var results []interface{}
for _, itemMap := range responseSlice {
// 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
}

21
internal/stats/hrv.go Normal file
View File

@@ -0,0 +1,21 @@
package stats
import "time"
const BASE_HRV_PATH = "/usersummary-service/stats/hrv"
type DailyHRV struct {
CalendarDate time.Time `json:"calendar_date"`
RestingHR *int `json:"resting_hr"`
HRV *int `json:"hrv"`
BaseStats
}
func NewDailyHRV() *DailyHRV {
return &DailyHRV{
BaseStats: BaseStats{
Path: BASE_HRV_PATH + "/daily/{start}/{end}",
PageSize: 28,
},
}
}

View File

@@ -0,0 +1,40 @@
package stats
import (
"errors"
"time"
)
const WEEKLY_HRV_PATH = "/wellness-service/wellness/weeklyHrv"
type WeeklyHRV struct {
CalendarDate time.Time `json:"calendar_date"`
AverageHRV float64 `json:"average_hrv"`
MaxHRV float64 `json:"max_hrv"`
MinHRV float64 `json:"min_hrv"`
HRVQualifier string `json:"hrv_qualifier"`
WellnessDataDaysCount int `json:"wellness_data_days_count"`
BaseStats
}
func NewWeeklyHRV() *WeeklyHRV {
return &WeeklyHRV{
BaseStats: BaseStats{
Path: WEEKLY_HRV_PATH + "/{end}/{period}",
PageSize: 52,
},
}
}
func (w *WeeklyHRV) Validate() error {
if w.CalendarDate.IsZero() {
return errors.New("calendar_date is required")
}
if w.AverageHRV < 0 || w.MaxHRV < 0 || w.MinHRV < 0 {
return errors.New("HRV values must be non-negative")
}
if w.MaxHRV < w.MinHRV {
return errors.New("max_hrv must be greater than min_hrv")
}
return nil
}

View File

@@ -0,0 +1,20 @@
package stats
import "time"
const BASE_HYDRATION_PATH = "/usersummary-service/stats/hydration"
type DailyHydration struct {
CalendarDate time.Time `json:"calendar_date"`
TotalWaterML *int `json:"total_water_ml"`
BaseStats
}
func NewDailyHydration() *DailyHydration {
return &DailyHydration{
BaseStats: BaseStats{
Path: BASE_HYDRATION_PATH + "/daily/{start}/{end}",
PageSize: 28,
},
}
}

View File

@@ -0,0 +1,21 @@
package stats
import "time"
const BASE_INTENSITY_PATH = "/usersummary-service/stats/intensity_minutes"
type DailyIntensityMinutes struct {
CalendarDate time.Time `json:"calendar_date"`
ModerateIntensity *int `json:"moderate_intensity"`
VigorousIntensity *int `json:"vigorous_intensity"`
BaseStats
}
func NewDailyIntensityMinutes() *DailyIntensityMinutes {
return &DailyIntensityMinutes{
BaseStats: BaseStats{
Path: BASE_INTENSITY_PATH + "/daily/{start}/{end}",
PageSize: 28,
},
}
}

27
internal/stats/sleep.go Normal file
View File

@@ -0,0 +1,27 @@
package stats
import "time"
const BASE_SLEEP_PATH = "/usersummary-service/stats/sleep"
type DailySleep struct {
CalendarDate time.Time `json:"calendar_date"`
TotalSleepTime *int `json:"total_sleep_time"`
RemSleepTime *int `json:"rem_sleep_time"`
DeepSleepTime *int `json:"deep_sleep_time"`
LightSleepTime *int `json:"light_sleep_time"`
AwakeTime *int `json:"awake_time"`
SleepScore *int `json:"sleep_score"`
SleepStartTimestamp *int64 `json:"sleep_start_timestamp"`
SleepEndTimestamp *int64 `json:"sleep_end_timestamp"`
BaseStats
}
func NewDailySleep() *DailySleep {
return &DailySleep{
BaseStats: BaseStats{
Path: BASE_SLEEP_PATH + "/daily/{start}/{end}",
PageSize: 28,
},
}
}

41
internal/stats/steps.go Normal file
View File

@@ -0,0 +1,41 @@
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,
},
}
}

24
internal/stats/stress.go Normal file
View File

@@ -0,0 +1,24 @@
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,
},
}
}

View File

@@ -0,0 +1,36 @@
package stats
import (
"errors"
"time"
)
const WEEKLY_STRESS_PATH = "/wellness-service/wellness/weeklyStress"
type WeeklyStress struct {
CalendarDate time.Time `json:"calendar_date"`
TotalStressDuration int `json:"total_stress_duration"`
AverageStressLevel float64 `json:"average_stress_level"`
MaxStressLevel int `json:"max_stress_level"`
StressQualifier string `json:"stress_qualifier"`
BaseStats
}
func NewWeeklyStress() *WeeklyStress {
return &WeeklyStress{
BaseStats: BaseStats{
Path: WEEKLY_STRESS_PATH + "/{end}/{period}",
PageSize: 52,
},
}
}
func (w *WeeklyStress) Validate() error {
if w.CalendarDate.IsZero() {
return errors.New("calendar_date is required")
}
if w.TotalStressDuration < 0 {
return errors.New("total_stress_duration must be non-negative")
}
return nil
}