Skip to content

Commit 23d3c12

Browse files
committed
Coderabbit fixes
1 parent 162a085 commit 23d3c12

File tree

3 files changed

+55
-47
lines changed

3 files changed

+55
-47
lines changed

.pre-commit-config.yaml

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ repos:
2222
rev: 24.8.0
2323
hooks:
2424
- id: black
25-
additional_dependencies: ['.[jupyter]']
2625
language_version: python3
2726

2827
- repo: https://github.com/astral-sh/ruff-pre-commit
@@ -34,15 +33,9 @@ repos:
3433

3534
- repo: local
3635
hooks:
37-
- id: format
38-
name: format
39-
entry: .venv/bin/pdm run format
40-
types: [python]
41-
language: system
42-
pass_filenames: false
43-
- id: lint
44-
name: lint
45-
entry: .venv/bin/pdm run lint
36+
- id: mypy
37+
name: mypy type checking
38+
entry: .venv/bin/pdm run mypy garminconnect tests
4639
types: [python]
4740
language: system
4841
pass_filenames: false

README.md

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ Select a category:
2525
Make your selection:
2626
```
2727

28-
### API Coverage Statistics
28+
## API Coverage Statistics
2929

3030
- **Total API Methods**: 101 unique endpoints
3131
- **Categories**: 11 organized sections
@@ -76,7 +76,7 @@ pip3 install garminconnect
7676

7777
## Run demo software (recommended)
7878

79-
```
79+
```bash
8080
python3 -m venv .venv --copies
8181
source .venv/bin/activate # On Windows: .venv\Scripts\activate
8282
pip install pdm
@@ -167,8 +167,13 @@ The library uses the same OAuth authentication as the official Garmin Connect ap
167167
**Advanced Configuration:**
168168
```python
169169
# Optional: Custom OAuth consumer (before login)
170+
import os
170171
import garth
171-
garth.sso.OAUTH_CONSUMER = {'key': 'your_key', 'secret': 'your_secret'}
172+
garth.sso.OAUTH_CONSUMER = {
173+
'key': os.getenv('GARTH_OAUTH_KEY', '<YOUR_KEY>'),
174+
'secret': os.getenv('GARTH_OAUTH_SECRET', '<YOUR_SECRET>'),
175+
}
176+
# Note: Set these env vars securely; placeholders are non-sensitive.
172177
```
173178

174179
**Token Storage:**
@@ -201,10 +206,6 @@ pdm run testcov # Run tests with coverage report
201206

202207
For package maintainers:
203208

204-
## 📦 Publishing
205-
206-
For package maintainers:
207-
208209
**Setup PyPI credentials:**
209210
```bash
210211
pip install twine
@@ -264,9 +265,11 @@ git push origin your-branch
264265
```
265266

266267
### Jupyter Notebook
268+
267269
Explore the API interactively with our [reference notebook](https://github.com/cyberjunky/python-garminconnect/blob/master/reference.ipynb).
268270

269271
### Python Code Examples
272+
270273
```python
271274
from garminconnect import Garmin
272275

@@ -275,11 +278,12 @@ client = Garmin('your_email', 'your_password')
275278
client.login()
276279

277280
# Get today's stats
278-
stats = client.get_stats('2023-08-31')
279-
print(f"Steps: {stats['totalSteps']}")
281+
from datetime import date
282+
_today = date.today().strftime('%Y-%m-%d')
283+
stats = client.get_stats(_today)
280284

281285
# Get heart rate data
282-
hr_data = client.get_heart_rates('2023-08-31')
286+
hr_data = client.get_heart_rates(_today)
283287
print(f"Resting HR: {hr_data['restingHeartRate']}")
284288
```
285289

garminconnect/__init__.py

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def _validate_date_format(date_str: str, param_name: str = "date") -> str:
4343
# Validate that it's a real date
4444
datetime.strptime(date_str, DATE_FORMAT_STR)
4545
except ValueError as e:
46-
raise ValueError(f"Invalid {param_name}: {e}") from e
46+
raise ValueError(f"invalid {param_name}: {e}") from e
4747

4848
return date_str
4949

@@ -134,7 +134,7 @@ def __init__(
134134
"/usersummary-service/usersummary/hydration/daily"
135135
)
136136
self.garmin_connect_set_hydration_url = (
137-
"usersummary-service/usersummary/hydration/log"
137+
"/usersummary-service/usersummary/hydration/log"
138138
)
139139
self.garmin_connect_daily_stats_steps_url = (
140140
"/usersummary-service/stats/steps/daily"
@@ -192,7 +192,7 @@ def __init__(
192192
"/periodichealth-service/menstrualcycle/dayview"
193193
)
194194
self.garmin_connect_pregnancy_snapshot_url = (
195-
"periodichealth-service/menstrualcycle/pregnancysnapshot"
195+
"/periodichealth-service/menstrualcycle/pregnancysnapshot"
196196
)
197197
self.garmin_connect_goals_url = "/goal-service/goal/goals"
198198

@@ -226,7 +226,6 @@ def __init__(
226226
self.garmin_connect_daily_intensity_minutes = (
227227
"/wellness-service/wellness/daily/im"
228228
)
229-
self.garmin_all_day_stress_url = "/wellness-service/wellness/dailyStress"
230229
self.garmin_daily_events_url = "/wellness-service/wellness/dailyEvents"
231230
self.garmin_connect_activities = (
232231
"/activitylist-service/activities/search/activities"
@@ -350,8 +349,7 @@ def login(self, /, tokenstore: str | None = None) -> tuple[str | None, str | Non
350349
self.password,
351350
prompt_mfa=self.prompt_mfa,
352351
)
353-
# In MFA early-return mode, profile/settings are not loaded yet
354-
return token1, token2
352+
# Continue to load profile/settings below
355353

356354
# Validate profile data exists
357355
if not hasattr(self.garth, "profile") or not self.garth.profile:
@@ -544,7 +542,7 @@ def get_body_composition(
544542
datetime.strptime(startdate, DATE_FORMAT_STR).date()
545543
> datetime.strptime(enddate, DATE_FORMAT_STR).date()
546544
):
547-
raise ValueError("Startdate cannot be after enddate")
545+
raise ValueError("startdate cannot be after enddate")
548546
url = f"{self.garmin_connect_weight_url}/weight/dateRange"
549547
params = {"startDate": str(startdate), "endDate": str(enddate)}
550548
logger.debug("Requesting body composition")
@@ -612,7 +610,7 @@ def add_weigh_in(
612610
try:
613611
dt = datetime.fromisoformat(timestamp) if timestamp else datetime.now()
614612
except ValueError as e:
615-
raise ValueError(f"Invalid timestamp format: {e}") from e
613+
raise ValueError(f"invalid timestamp format: {e}") from e
616614

617615
# Apply timezone offset to get UTC/GMT time
618616
dtGMT = dt.astimezone(timezone.utc)
@@ -639,7 +637,7 @@ def add_weigh_in_with_timestamps(
639637
url = f"{self.garmin_connect_weight_url}/user-weight"
640638

641639
if unitKey not in VALID_WEIGHT_UNITS:
642-
raise ValueError(f"UnitKey must be one of {VALID_WEIGHT_UNITS}")
640+
raise ValueError(f"unitKey must be one of {VALID_WEIGHT_UNITS}")
643641
# Validate and format the timestamps
644642
dt = datetime.fromisoformat(dateTimestamp) if dateTimestamp else datetime.now()
645643
dtGMT = (
@@ -895,7 +893,7 @@ def get_lactate_threshold(
895893
}
896894

897895
if start_date is None:
898-
raise ValueError("You must either specify 'latest=True' or a start_date")
896+
raise ValueError("you must either specify 'latest=True' or a start_date")
899897

900898
if end_date is None:
901899
end_date = date.today().isoformat()
@@ -955,7 +953,7 @@ def add_hydration_data(
955953
raw_ts = datetime.strptime(cdate, "%Y-%m-%d")
956954
timestamp = datetime.strftime(raw_ts, "%Y-%m-%dT%H:%M:%S.%f")
957955
except ValueError as e:
958-
raise ValueError(f"Invalid cdate: {e}") from e
956+
raise ValueError(f"invalid cdate: {e}") from e
959957

960958
elif cdate is None and timestamp is not None:
961959
# If timestamp is not null, validate and set cdate equal to date part of timestamp
@@ -983,7 +981,7 @@ def add_hydration_data(
983981
except ValueError as e:
984982
if "doesn't match" in str(e):
985983
raise
986-
raise ValueError(f"Invalid timestamp format: {e}") from e
984+
raise ValueError(f"invalid timestamp format: {e}") from e
987985

988986
payload = {
989987
"calendarDate": cdate,
@@ -1034,7 +1032,7 @@ def get_all_day_stress(self, cdate: str) -> dict[str, Any]:
10341032
"""Return available all day stress data 'cdate' format 'YYYY-MM-DD'."""
10351033

10361034
cdate = _validate_date_format(cdate, "cdate")
1037-
url = f"{self.garmin_all_day_stress_url}/{cdate}"
1035+
url = f"{self.garmin_connect_daily_stress_url}/{cdate}"
10381036
logger.debug("Requesting all day stress data")
10391037

10401038
return self.connectapi(url)
@@ -1067,15 +1065,15 @@ def get_earned_badges(self) -> list[dict[str, Any]]:
10671065

10681066
return self.connectapi(url)
10691067

1070-
def get_available_badges(self) -> list[dict]:
1068+
def get_available_badges(self) -> list[dict[str, Any]]:
10711069
"""Return available badges for current user."""
10721070

10731071
url = self.garmin_connect_available_badges_url
10741072
logger.debug("Requesting available badges for user")
10751073

10761074
return self.connectapi(url, params={"showExclusiveBadge": "true"})
10771075

1078-
def get_in_progress_badges(self) -> list[dict]:
1076+
def get_in_progress_badges(self) -> list[dict[str, Any]]:
10791077
"""Return in progress badges for current user."""
10801078

10811079
logger.debug("Requesting in progress badges for user")
@@ -1283,7 +1281,7 @@ def get_race_predictions(
12831281
return self.connectapi(url, params=params)
12841282

12851283
else:
1286-
raise ValueError("You must either provide all parameters or no parameters")
1284+
raise ValueError("you must either provide all parameters or no parameters")
12871285

12881286
def get_training_status(self, cdate: str) -> dict[str, Any]:
12891287
"""Return training status data for current user."""
@@ -1546,12 +1544,12 @@ def upload_activity(self, activity_path: str) -> Any:
15461544

15471545
# Check if it's actually a file
15481546
if not p.is_file():
1549-
raise ValueError(f"Path is not a file: {activity_path}")
1547+
raise ValueError(f"path is not a file: {activity_path}")
15501548

15511549
file_base_name = p.name
15521550

15531551
if not file_base_name:
1554-
raise ValueError("Invalid file path - no filename found")
1552+
raise ValueError("invalid file path - no filename found")
15551553

15561554
# More robust extension checking
15571555
file_parts = file_base_name.split(".")
@@ -1787,7 +1785,7 @@ def download_activity(
17871785
Garmin.ActivityDownloadFormat.CSV: f"{self.garmin_connect_csv_download}/{activity_id}", # noqa
17881786
}
17891787
if dl_fmt not in urls:
1790-
raise ValueError(f"Unexpected value {dl_fmt} for dl_fmt")
1788+
raise ValueError(f"unexpected value {dl_fmt} for dl_fmt")
17911789
url = urls[dl_fmt]
17921790

17931791
logger.debug("Downloading activities from %s", url)
@@ -1863,17 +1861,19 @@ def get_activity_details(
18631861

18641862
return self.connectapi(url, params=params)
18651863

1866-
def get_activity_exercise_sets(self, activity_id: str) -> dict[str, Any]:
1864+
def get_activity_exercise_sets(self, activity_id: int | str) -> dict[str, Any]:
18671865
"""Return activity exercise sets."""
18681866

1867+
activity_id = _validate_positive_integer(int(activity_id), "activity_id")
18691868
url = f"{self.garmin_connect_activity}/{activity_id}/exerciseSets"
18701869
logger.debug("Requesting exercise sets for activity id %s", activity_id)
18711870

18721871
return self.connectapi(url)
18731872

1874-
def get_activity_gear(self, activity_id: str) -> dict[str, Any]:
1873+
def get_activity_gear(self, activity_id: int | str) -> dict[str, Any]:
18751874
"""Return gears used for activity id."""
18761875

1876+
activity_id = _validate_positive_integer(int(activity_id), "activity_id")
18771877
params = {
18781878
"activityId": str(activity_id),
18791879
}
@@ -1882,7 +1882,9 @@ def get_activity_gear(self, activity_id: str) -> dict[str, Any]:
18821882

18831883
return self.connectapi(url, params=params)
18841884

1885-
def get_gear_activities(self, gearUUID: str, limit: int = 9999) -> dict[str, Any]:
1885+
def get_gear_activities(
1886+
self, gearUUID: str, limit: int = 9999
1887+
) -> list[dict[str, Any]]:
18861888
"""Return activities where gear uuid was used.
18871889
:param gearUUID: UUID of the gear to get activities for
18881890
:param limit: Maximum number of activities to return (default: 9999)
@@ -1933,15 +1935,17 @@ def get_workouts(self, start: int = 0, limit: int = 100) -> dict[str, Any]:
19331935
params = {"start": start, "limit": limit}
19341936
return self.connectapi(url, params=params)
19351937

1936-
def get_workout_by_id(self, workout_id: str) -> dict[str, Any]:
1938+
def get_workout_by_id(self, workout_id: int | str) -> dict[str, Any]:
19371939
"""Return workout by id."""
19381940

1941+
workout_id = _validate_positive_integer(int(workout_id), "workout_id")
19391942
url = f"{self.garmin_workouts}/workout/{workout_id}"
19401943
return self.connectapi(url)
19411944

1942-
def download_workout(self, workout_id: str) -> bytes:
1945+
def download_workout(self, workout_id: int | str) -> bytes:
19431946
"""Download workout by id."""
19441947

1948+
workout_id = _validate_positive_integer(int(workout_id), "workout_id")
19451949
url = f"{self.garmin_workouts}/workout/FIT/{workout_id}"
19461950
logger.debug("Downloading workout from %s", url)
19471951

@@ -1961,9 +1965,11 @@ def upload_workout(
19611965
try:
19621966
payload = _json.loads(workout_json)
19631967
except Exception as e:
1964-
raise ValueError(f"Invalid workout_json string: {e}") from e
1968+
raise ValueError(f"invalid workout_json string: {e}") from e
19651969
else:
19661970
payload = workout_json
1971+
if not isinstance(payload, dict | list):
1972+
raise ValueError("workout_json must be a JSON object or array")
19671973
return self.garth.post("connectapi", url, json=payload, api=True).json()
19681974

19691975
def get_menstrual_data_for_date(self, fordate: str) -> dict[str, Any]:
@@ -1998,8 +2004,13 @@ def get_pregnancy_summary(self) -> dict[str, Any]:
19982004
return self.connectapi(url)
19992005

20002006
def query_garmin_graphql(self, query: dict[str, Any]) -> dict[str, Any]:
2001-
"""Returns the results of a POST request to the Garmin GraphQL Endpoints.
2002-
Requires a GraphQL structured query. See {TBD} for examples.
2007+
"""Execute a POST to Garmin's GraphQL endpoint.
2008+
2009+
Args:
2010+
query: A GraphQL request body, e.g. {"query": "...", "variables": {...}}
2011+
See example.py for example queries.
2012+
Returns:
2013+
Parsed JSON response as a dict.
20032014
"""
20042015

20052016
logger.debug(f"Querying Garmin GraphQL Endpoint with query: {query}")

0 commit comments

Comments
 (0)