Skip to content

Commit a9305ce

Browse files
matinclaude
andcommitted
Add training status support
- Add DailyTrainingStatus, WeeklyTrainingStatus, MonthlyTrainingStatus classes - Custom parsing logic for complex nested API responses - Comprehensive test coverage with VCR cassettes - Documentation in README with usage examples - Follows existing patterns for consistency and DRY principles Closes #129 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent c1bdee8 commit a9305ce

12 files changed

+4553
-10
lines changed

CLAUDE.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working
4+
with code in this repository.
5+
6+
## Development Commands
7+
8+
### Setup and Installation
9+
10+
- `make install` - Install package, dependencies, and pre-commit hooks for
11+
local development
12+
- `make sync` - Sync dependencies and lockfiles (includes force reinstall)
13+
14+
### Code Quality
15+
16+
- `make lint` - Run full linting (ruff format check, ruff check, mypy)
17+
- `make format` - Auto-format code with ruff
18+
- `make codespell` - Run spellchecking
19+
20+
### Testing
21+
22+
- `make test` - Run all tests with coverage
23+
- `make testcov` - Run tests and generate coverage reports (HTML + XML)
24+
- `uv run pytest tests/path/to/test.py::test_name -v` - Run a single test
25+
- `uv run pytest tests/stats/ -v` - Run tests for a specific module
26+
27+
### Complete Workflow
28+
29+
- `make all` - Run the full CI pipeline (lint + codespell + testcov)
30+
31+
## Architecture Overview
32+
33+
### Core Structure
34+
35+
Garth is a Garmin Connect API client that uses OAuth1/OAuth2 authentication.
36+
The main components:
37+
38+
- **Authentication Layer**: `auth_tokens.py`, `sso.py` - Handles OAuth1/OAuth2
39+
tokens and Garmin SSO
40+
- **HTTP Client**: `http.py` - Main client with automatic token refresh and
41+
request handling
42+
- **Data Models**: `data/`, `stats/`, `users/` - Pydantic dataclasses for
43+
different API endpoints
44+
- **CLI**: `cli.py` - Command-line interface for authentication
45+
46+
### Data Module Pattern
47+
48+
All data modules follow a consistent pattern:
49+
50+
- Extend base classes (`_base.py` in stats, similar patterns elsewhere)
51+
- Use Pydantic dataclasses for validation
52+
- Implement `.list()` class methods for fetching collections
53+
- Implement `.get()` class methods for individual items
54+
- Handle API pagination automatically
55+
56+
### Stats Module Architecture
57+
58+
The stats module (`src/garth/stats/`) follows a specific inheritance pattern:
59+
60+
- `_base.py` contains the `Stats` base class with common `.list()` implementation
61+
- Individual stat types (stress, sleep, HRV, etc.) inherit from `Stats`
62+
- Each class defines `_path` and `_page_size` class variables
63+
- API responses are automatically converted from camelCase to snake_case
64+
65+
### Testing Strategy
66+
67+
- Uses pytest with VCR.py for HTTP request recording
68+
- Cassettes stored in `tests/*/cassettes/` directories
69+
- `authed_client` fixture provides authenticated client for tests
70+
- Environment variable `GARTH_HOME` controls whether to record new cassettes
71+
or use existing ones
72+
- Request/response sanitization removes sensitive data from cassettes
73+
74+
### Authentication Flow
75+
76+
1. Initial login with email/password (supports MFA)
77+
2. Receives OAuth1 token (long-lived, ~1 year)
78+
3. Exchanges for OAuth2 token (short-lived, auto-refreshed)
79+
4. Tokens can be saved/loaded from files for persistence
80+
81+
### Adding New Stats Endpoints
82+
83+
When adding new stats endpoints (like training status):
84+
85+
1. Create new module in `src/garth/stats/`
86+
2. Define dataclasses extending `Stats` base class
87+
3. Add imports to `src/garth/stats/__init__.py`
88+
4. Add exports to `src/garth/__init__.py`
89+
5. Create comprehensive tests in `tests/stats/`
90+
6. Run tests to generate VCR cassettes
91+
7. Update README.md with usage examples
92+
93+
### Key Patterns
94+
95+
- Use `garth.connectapi()` for direct API access with automatic authentication
96+
- API endpoints follow Garmin's internal mobile app structure
97+
- Dates are handled as `datetime.date` objects
98+
- All optional fields default to `None` in dataclasses
99+
- Custom parsing logic in training status module shows how to handle complex
100+
nested API responses

README.md

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,121 @@ HRVData(
618618
)
619619
```
620620

621+
### Training Status
622+
623+
Daily training status
624+
625+
```python
626+
garth.DailyTrainingStatus.list(period=1)
627+
```
628+
629+
```python
630+
[
631+
DailyTrainingStatus(
632+
calendar_date=datetime.date(2025, 6, 11),
633+
since_date="2025-05-31",
634+
weekly_training_load=None,
635+
training_status=7,
636+
timestamp=1749643909000,
637+
device_id=3469703076,
638+
load_tunnel_min=None,
639+
load_tunnel_max=None,
640+
load_level_trend=None,
641+
sport="RUNNING",
642+
sub_sport="GENERIC",
643+
fitness_trend_sport="RUNNING",
644+
fitness_trend=2,
645+
training_status_feedback_phrase="PRODUCTIVE_6",
646+
training_paused=False,
647+
primary_training_device=True,
648+
acwr_percent=57,
649+
acwr_status="OPTIMAL",
650+
acwr_status_feedback="FEEDBACK_3",
651+
daily_training_load_acute=399,
652+
max_training_load_chronic=450.0,
653+
min_training_load_chronic=240.0,
654+
daily_training_load_chronic=300,
655+
daily_acute_chronic_workload_ratio=1.3
656+
)
657+
]
658+
```
659+
660+
Weekly training status
661+
662+
```python
663+
garth.WeeklyTrainingStatus.list(period=4)
664+
```
665+
666+
```python
667+
[
668+
WeeklyTrainingStatus(
669+
calendar_date=datetime.date(2025, 5, 21),
670+
since_date=None,
671+
weekly_training_load=None,
672+
training_status=4,
673+
timestamp=1747839970000,
674+
device_id=3469703076,
675+
load_tunnel_min=None,
676+
load_tunnel_max=None,
677+
load_level_trend=None,
678+
sport="RUNNING",
679+
sub_sport="GENERIC",
680+
fitness_trend_sport="RUNNING",
681+
fitness_trend=2,
682+
training_status_feedback_phrase="MAINTAINING_2",
683+
training_paused=False,
684+
primary_training_device=True,
685+
acwr_percent=42,
686+
acwr_status="OPTIMAL",
687+
acwr_status_feedback="FEEDBACK_2",
688+
daily_training_load_acute=224,
689+
max_training_load_chronic=328.5,
690+
min_training_load_chronic=175.20000000000002,
691+
daily_training_load_chronic=219,
692+
daily_acute_chronic_workload_ratio=1.0
693+
),
694+
# ... more entries
695+
]
696+
```
697+
698+
Monthly training status
699+
700+
```python
701+
garth.MonthlyTrainingStatus.list(period=6)
702+
```
703+
704+
```python
705+
[
706+
MonthlyTrainingStatus(
707+
calendar_date=datetime.date(2025, 1, 1),
708+
since_date=None,
709+
weekly_training_load=None,
710+
training_status=4,
711+
timestamp=1735743916000,
712+
device_id=3469703076,
713+
load_tunnel_min=None,
714+
load_tunnel_max=None,
715+
load_level_trend=None,
716+
sport="RUNNING",
717+
sub_sport="GENERIC",
718+
fitness_trend_sport="RUNNING",
719+
fitness_trend=2,
720+
training_status_feedback_phrase="MAINTAINING_3",
721+
training_paused=False,
722+
primary_training_device=True,
723+
acwr_percent=29,
724+
acwr_status="LOW",
725+
acwr_status_feedback="FEEDBACK_1",
726+
daily_training_load_acute=160,
727+
max_training_load_chronic=328.5,
728+
min_training_load_chronic=175.20000000000002,
729+
daily_training_load_chronic=219,
730+
daily_acute_chronic_workload_ratio=0.7
731+
),
732+
# ... more entries
733+
]
734+
```
735+
621736
### Sleep
622737

623738
Daily sleep quality

src/garth/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@
77
DailySleep,
88
DailySteps,
99
DailyStress,
10+
DailyTrainingStatus,
11+
MonthlyTrainingStatus,
1012
WeeklyIntensityMinutes,
1113
WeeklySteps,
1214
WeeklyStress,
15+
WeeklyTrainingStatus,
1316
)
1417
from .users import UserProfile, UserSettings
1518
from .version import __version__
@@ -25,13 +28,16 @@
2528
"DailySleep",
2629
"DailySteps",
2730
"DailyStress",
31+
"DailyTrainingStatus",
2832
"HRVData",
33+
"MonthlyTrainingStatus",
2934
"SleepData",
3035
"UserProfile",
3136
"UserSettings",
3237
"WeeklyIntensityMinutes",
3338
"WeeklySteps",
3439
"WeeklyStress",
40+
"WeeklyTrainingStatus",
3541
"__version__",
3642
"client",
3743
"configure",

src/garth/sso.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -77,16 +77,13 @@ def login(
7777
embedWidget="true",
7878
gauthHost=SSO,
7979
)
80-
SIGNIN_PARAMS = {
81-
**SSO_EMBED_PARAMS,
82-
**dict(
83-
gauthHost=SSO_EMBED,
84-
service=SSO_EMBED,
85-
source=SSO_EMBED,
86-
redirectAfterAccountLoginUrl=SSO_EMBED,
87-
redirectAfterAccountCreationUrl=SSO_EMBED,
88-
),
89-
}
80+
SIGNIN_PARAMS = SSO_EMBED_PARAMS | dict(
81+
gauthHost=SSO_EMBED,
82+
service=SSO_EMBED,
83+
source=SSO_EMBED,
84+
redirectAfterAccountLoginUrl=SSO_EMBED,
85+
redirectAfterAccountCreationUrl=SSO_EMBED,
86+
)
9087

9188
# Set cookies
9289
client.get("sso", "/sso/embed", params=SSO_EMBED_PARAMS)

src/garth/stats/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
"DailySleep",
66
"DailySteps",
77
"DailyStress",
8+
"DailyTrainingStatus",
9+
"MonthlyTrainingStatus",
810
"WeeklyIntensityMinutes",
911
"WeeklyStress",
1012
"WeeklySteps",
13+
"WeeklyTrainingStatus",
1114
]
1215

1316
from .hrv import DailyHRV
@@ -16,3 +19,8 @@
1619
from .sleep import DailySleep
1720
from .steps import DailySteps, WeeklySteps
1821
from .stress import DailyStress, WeeklyStress
22+
from .training_status import (
23+
DailyTrainingStatus,
24+
MonthlyTrainingStatus,
25+
WeeklyTrainingStatus,
26+
)

0 commit comments

Comments
 (0)