Skip to content

Commit 7a66df1

Browse files
committed
Coderabbit fixes
1 parent 8ea4374 commit 7a66df1

File tree

2 files changed

+44
-17
lines changed

2 files changed

+44
-17
lines changed

README.md

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
The Garmin Connect API demo (`example.py`) provides comprehensive access to **101 API methods** organized into **11 categories** for easy navigation:
44

5+
Note: The demo menu is generated dynamically; exact options may change between releases.
6+
57
```bash
68
$ ./example.py
79
🏃‍♂️ Garmin Connect API Demo - Main Menu
@@ -45,8 +47,8 @@ Make your selection:
4547

4648
- **Enhanced User Experience**: Categorized navigation with emoji indicators
4749
- **Smart Data Management**: Interactive weigh-in deletion with search capabilities
48-
- **Comprehensive Coverage**: All major Garmin Connect features accessible
49-
- **Error Handling**: Robust error handling and user-friendly prompts
50+
- **Comprehensive Coverage**: All major Garmin Connect features are accessible
51+
- **Error Handling**: Robust error handling with user-friendly prompts
5052
- **Data Export**: JSON export functionality for all data types
5153

5254
[![Donate via PayPal](https://img.shields.io/badge/Donate-PayPal-blue.svg?style=for-the-badge&logo=paypal)](https://www.paypal.me/cyberjunkynl/)
@@ -71,7 +73,8 @@ Compatible with all Garmin Connect accounts. See <https://connect.garmin.com/>
7173
Install from PyPI:
7274

7375
```bash
74-
pip3 install garminconnect
76+
python3 -m pip install --upgrade pip
77+
python3 -m pip install garminconnect
7578
```
7679

7780
## Run demo software (recommended)
@@ -81,7 +84,7 @@ python3 -m venv .venv --copies
8184
source .venv/bin/activate # On Windows: .venv\Scripts\activate
8285
pip install pdm
8386
pdm install --group :example
84-
./example.py
87+
python3 ./example.py
8588
```
8689

8790

@@ -178,6 +181,12 @@ garth.sso.OAUTH_CONSUMER = {
178181

179182
**Token Storage:**
180183
Tokens are automatically saved to `~/.garminconnect` directory for persistent authentication.
184+
For security, ensure restrictive permissions:
185+
186+
```bash
187+
chmod 700 ~/.garminconnect
188+
chmod 600 ~/.garminconnect/* 2>/dev/null || true
189+
```
181190

182191
## 🧪 Testing
183192

@@ -198,6 +207,14 @@ pdm run test # Run all tests
198207
pdm run testcov # Run tests with coverage report
199208
```
200209

210+
Optional: keep test tokens isolated
211+
212+
```bash
213+
export GARMINTOKENS="$(mktemp -d)"
214+
python3 ./example.py # create fresh tokens for tests
215+
pdm run test
216+
```
217+
201218
**Note:** Tests automatically use `~/.garminconnect` as the default token file location. You can override this by setting the `GARMINTOKENS` environment variable. Run `example.py` first to generate authentication tokens for testing.
202219

203220
**For Developers:** Tests use VCR cassettes to record/replay HTTP interactions. If tests fail with authentication errors, ensure valid tokens exist in `~/.garminconnect`
@@ -217,6 +234,14 @@ username = __token__
217234
password = <PyPI_API_TOKEN>
218235
```
219236

237+
# Recommended: use environment variables and restrict file perms
238+
239+
```bash
240+
chmod 600 ~/.pypirc
241+
export TWINE_USERNAME="__token__"
242+
export TWINE_PASSWORD="<PyPI_API_TOKEN>"
243+
```
244+
220245
**Publish new version:**
221246
```bash
222247
pdm run publish # Build and publish to PyPI
@@ -288,7 +313,7 @@ stats = client.get_stats(_today)
288313

289314
# Get heart rate data
290315
hr_data = client.get_heart_rates(_today)
291-
print(f"Resting HR: {hr_data['restingHeartRate']}")
316+
print(f"Resting HR: {hr_data.get('restingHeartRate', 'n/a')}")
292317
```
293318

294319
### Additional Resources

garminconnect/__init__.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def _validate_date_format(date_str: str, param_name: str = "date") -> str:
3434
# Remove any extra whitespace
3535
date_str = date_str.strip()
3636

37-
if not re.match(DATE_FORMAT_REGEX, date_str):
37+
if not re.fullmatch(DATE_FORMAT_REGEX, date_str):
3838
raise ValueError(
3939
f"{param_name} must be in format 'YYYY-MM-DD', got: {date_str}"
4040
)
@@ -299,11 +299,8 @@ def download(self, path: str, **kwargs: Any) -> Any:
299299
try:
300300
return self.garth.download(path, **kwargs)
301301
except Exception as e:
302-
logger.exception("Download error path=%s", path)
303302
status = getattr(getattr(e, "response", None), "status_code", None)
304-
logger.error(
305-
"Download failed for path '%s': %s (status=%s)", path, e, status
306-
)
303+
logger.exception("Download failed for path '%s' (status=%s)", path, status)
307304
if status == 401:
308305
raise GarminConnectAuthenticationError(f"Download error: {e}") from e
309306
if status == 429:
@@ -622,8 +619,8 @@ def add_weigh_in(
622619
# Apply timezone offset to get UTC/GMT time
623620
dtGMT = dt.astimezone(timezone.utc)
624621
payload = {
625-
"dateTimestamp": f"{_fmt_ts(dt)}.000",
626-
"gmtTimestamp": f"{_fmt_ts(dtGMT)}.000",
622+
"dateTimestamp": _fmt_ts(dt),
623+
"gmtTimestamp": _fmt_ts(dtGMT),
627624
"unitKey": unitKey,
628625
"sourceType": "MANUAL",
629626
"value": weight,
@@ -657,8 +654,8 @@ def add_weigh_in_with_timestamps(
657654
weight = _validate_positive_number(weight, "weight")
658655
# Build the payload
659656
payload = {
660-
"dateTimestamp": f"{_fmt_ts(dt)}.000", # Local time
661-
"gmtTimestamp": f"{_fmt_ts(dtGMT)}.000", # GMT/UTC time
657+
"dateTimestamp": _fmt_ts(dt), # Local time (ms)
658+
"gmtTimestamp": _fmt_ts(dtGMT), # GMT/UTC time (ms)
662659
"unitKey": unitKey,
663660
"sourceType": "MANUAL",
664661
"value": weight,
@@ -777,8 +774,8 @@ def set_blood_pressure(
777774
# Apply timezone offset to get UTC/GMT time
778775
dtGMT = dt.astimezone(timezone.utc)
779776
payload = {
780-
"measurementTimestampLocal": f"{_fmt_ts(dt)}.000",
781-
"measurementTimestampGMT": f"{_fmt_ts(dtGMT)}.000",
777+
"measurementTimestampLocal": _fmt_ts(dt),
778+
"measurementTimestampGMT": _fmt_ts(dtGMT),
782779
"systolic": systolic,
783780
"diastolic": diastolic,
784781
"pulse": pulse,
@@ -1738,6 +1735,9 @@ def get_goals(
17381735

17391736
goals = []
17401737
url = self.garmin_connect_goals_url
1738+
valid_statuses = {"active", "future", "past"}
1739+
if status not in valid_statuses:
1740+
raise ValueError(f"status must be one of {valid_statuses}")
17411741
start = _validate_positive_integer(start, "start")
17421742
limit = _validate_positive_integer(limit, "limit")
17431743
params = {
@@ -1924,7 +1924,7 @@ def get_activity_gear(self, activity_id: int | str) -> dict[str, Any]:
19241924
return self.connectapi(url, params=params)
19251925

19261926
def get_gear_activities(
1927-
self, gearUUID: str, limit: int = 9999
1927+
self, gearUUID: str, limit: int = 1000
19281928
) -> list[dict[str, Any]]:
19291929
"""Return activities where gear uuid was used.
19301930
:param gearUUID: UUID of the gear to get activities for
@@ -1933,6 +1933,8 @@ def get_gear_activities(
19331933
"""
19341934
gearUUID = str(gearUUID)
19351935
limit = _validate_positive_integer(limit, "limit")
1936+
# Optional: enforce a reasonable ceiling to avoid heavy responses
1937+
limit = min(limit, MAX_ACTIVITY_LIMIT)
19361938
url = f"{self.garmin_connect_activities_baseurl}{gearUUID}/gear?start=0&limit={limit}"
19371939
logger.debug("Requesting activities for gearUUID %s", gearUUID)
19381940

0 commit comments

Comments
 (0)