Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
language: "en-US"
early_access: true
reviews:
profile: "pythonic"
profile: "assertive"
request_changes_workflow: true
high_level_summary: true
poem: false
Expand All @@ -11,11 +11,7 @@ reviews:
auto_review:
enabled: true
drafts: false
auto_fix:
enabled: true
include_imports: true
include_type_hints: true
include_security_fixes: true

path_filters:
- "!tests/**/cassettes/**"
path_instructions:
Expand Down
11 changes: 10 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,17 @@ jobs:
# pdm run coverage xml
# continue-on-error: true

- name: Check for coverage report
id: coverage_check
run: |
if [ -f coverage.xml ]; then
echo "coverage_generated=true" >> "$GITHUB_OUTPUT"
else
echo "coverage_generated=false" >> "$GITHUB_OUTPUT"
fi

- name: Upload coverage artifact
if: matrix.python-version == '3.11'
if: matrix.python-version == '3.11' && steps.coverage_check.outputs.coverage_generated == 'true'
uses: actions/upload-artifact@v4
with:
name: coverage-xml
Expand Down
13 changes: 7 additions & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ repos:
- tomli
exclude: 'cassettes/'

- repo: https://github.com/psf/black
rev: 24.8.0
hooks:
- id: black
language_version: python3
# Removed black - using ruff-format instead to avoid formatting conflicts
# - repo: https://github.com/psf/black
# rev: 24.8.0
# hooks:
# - id: black
# language_version: python3

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.4
Expand All @@ -35,7 +36,7 @@ repos:
hooks:
- id: mypy
name: mypy type checking
entry: .venv/bin/pdm run mypy garminconnect tests
entry: pdm run mypy garminconnect tests
types: [python]
language: system
pass_filenames: false
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ pdm install --group :all
```

**Run Tests:**

```bash
pdm run test # Run all tests
pdm run testcov # Run tests with coverage report
Expand All @@ -232,6 +233,7 @@ pdm run test
For package maintainers:

**Setup PyPI credentials:**

```bash
pip install twine
# Edit with your preferred editor, or create via here-doc:
Expand All @@ -241,6 +243,7 @@ pip install twine
# password = <PyPI_API_TOKEN>
# EOF
```

```ini
[pypi]
username = __token__
Expand All @@ -256,11 +259,13 @@ export TWINE_PASSWORD="<PyPI_API_TOKEN>"
```

**Publish new version:**

```bash
pdm run publish # Build and publish to PyPI
```

**Alternative publishing steps:**

```bash
pdm run build # Build package only
pdm publish # Publish pre-built package
Expand Down
58 changes: 30 additions & 28 deletions demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,7 @@ def __init__(self):

# Activity settings
self.activitytype = "" # Possible values: cycling, running, swimming, multi_sport, fitness_equipment, hiking, walking, other
self.activityfile = (
"test_data/sample_activity.gpx" # Supported file types: .fit .gpx .tcx
)
self.activityfile = "test_data/*.gpx" # Supported file types: .fit .gpx .tcx
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

config.activityfile is no longer used in the upload flow.

The glob pattern is defined here but never referenced in upload_activity_file (lines 1287-1403), which now performs its own file discovery via glob.glob("test_data/*.gpx"). Additionally, the menu description at line 227 displays config.activityfile, which now shows a glob pattern instead of a single file path, potentially confusing users.

Consider either:

  1. Remove self.activityfile from Config if it's truly unused, or
  2. Use config.activityfile as the glob pattern in upload_activity_file for consistency:
-        gpx_files = glob.glob("test_data/*.gpx")
+        gpx_files = glob.glob(config.activityfile)

And update the menu description at line 227 to be more accurate:

-                "desc": f"Upload activity data from {config.activityfile}",
+                "desc": "Upload activity data (interactive file selection)",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
self.activityfile = "test_data/*.gpx" # Supported file types: .fit .gpx .tcx
# In upload_activity_file (replace hard-coded glob with config)
gpx_files = glob.glob(config.activityfile)
Suggested change
self.activityfile = "test_data/*.gpx" # Supported file types: .fit .gpx .tcx
# In the menu definition (make description generic)
"desc": "Upload activity data (interactive file selection)",

self.workoutfile = "test_data/sample_workout.json" # Sample workout JSON file

# Export settings
Expand Down Expand Up @@ -1288,37 +1286,42 @@ def get_solar_data(api: Garmin) -> None:

def upload_activity_file(api: Garmin) -> None:
"""Upload activity data from file."""
import glob

try:
# Default activity file from config
print(f"📤 Uploading activity from file: {config.activityfile}")
# List all .gpx files in test_data
gpx_files = glob.glob(config.activityfile)
if not gpx_files:
print("❌ No .gpx files found in test_data directory.")
print("ℹ️ Please add GPX files to test_data before uploading.")
return

# Check if file exists
import os
print("Select a GPX file to upload:")
for idx, fname in enumerate(gpx_files, 1):
print(f" {idx}. {fname}")

if not os.path.exists(config.activityfile):
print(f"❌ File not found: {config.activityfile}")
print(
"ℹ️ Please place your activity file (.fit, .gpx, or .tcx) under the 'test_data' directory or update config.activityfile"
)
print("ℹ️ Supported formats: FIT, GPX, TCX")
return
while True:
try:
choice = int(input(f"Enter number (1-{len(gpx_files)}): "))
if 1 <= choice <= len(gpx_files):
selected_file = gpx_files[choice - 1]
break
else:
print("Invalid selection. Try again.")
except ValueError:
print("Please enter a valid number.")

# Upload the activity
result = api.upload_activity(config.activityfile)
print(f"📤 Uploading activity from file: {selected_file}")

if result:
print("✅ Activity uploaded successfully!")
call_and_display(
api.upload_activity,
config.activityfile,
method_name="upload_activity",
api_call_desc=f"api.upload_activity({config.activityfile})",
)
else:
print(f"❌ Failed to upload activity from {config.activityfile}")
call_and_display(
api.upload_activity,
selected_file,
method_name="upload_activity",
api_call_desc=f"api.upload_activity({selected_file})",
)

except FileNotFoundError:
print(f"❌ File not found: {config.activityfile}")
print(f"❌ File not found: {selected_file}")
Comment on lines +1303 to +1324
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Risk of UnboundLocalError if exception occurs before file selection.

The try block starts at line 1291, but selected_file is only assigned at line 1307 inside the while True loop. If an exception occurs before the user completes the selection (e.g., during glob.glob() or input()), the except FileNotFoundError handler at line 1323 will reference an undefined selected_file, causing an UnboundLocalError.

Initialize selected_file before the selection loop:

     try:
+        selected_file = None  # Initialize before potential exceptions
         # List all .gpx files in test_data
         gpx_files = glob.glob("test_data/*.gpx")

Then update the error message to handle the case where no file was selected:

     except FileNotFoundError:
-        print(f"❌ File not found: {selected_file}")
+        print(f"❌ File not found: {selected_file if selected_file else 'No file selected'}")
         print("ℹ️ Please ensure the activity file exists in the current directory")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
while True:
try:
choice = int(input(f"Enter number (1-{len(gpx_files)}): "))
if 1 <= choice <= len(gpx_files):
selected_file = gpx_files[choice - 1]
break
else:
print("Invalid selection. Try again.")
except ValueError:
print("Please enter a valid number.")
# Upload the activity
result = api.upload_activity(config.activityfile)
print(f"📤 Uploading activity from file: {selected_file}")
if result:
print("✅ Activity uploaded successfully!")
call_and_display(
api.upload_activity,
config.activityfile,
method_name="upload_activity",
api_call_desc=f"api.upload_activity({config.activityfile})",
)
else:
print(f"❌ Failed to upload activity from {config.activityfile}")
call_and_display(
api.upload_activity,
selected_file,
method_name="upload_activity",
api_call_desc=f"api.upload_activity({selected_file})",
)
except FileNotFoundError:
print(f"❌ File not found: {config.activityfile}")
print(f"❌ File not found: {selected_file}")
try:
selected_file = None # Initialize before potential exceptions
# List all .gpx files in test_data
gpx_files = glob.glob("test_data/*.gpx")
while True:
try:
choice = int(input(f"Enter number (1-{len(gpx_files)}): "))
if 1 <= choice <= len(gpx_files):
selected_file = gpx_files[choice - 1]
break
else:
print("Invalid selection. Try again.")
except ValueError:
print("Please enter a valid number.")
print(f"📤 Uploading activity from file: {selected_file}")
call_and_display(
api.upload_activity,
selected_file,
method_name="upload_activity",
api_call_desc=f"api.upload_activity({selected_file})",
)
except FileNotFoundError:
print(f"❌ File not found: {selected_file if selected_file else 'No file selected'}")
print("ℹ️ Please ensure the activity file exists in the current directory")
🤖 Prompt for AI Agents
In demo.py around lines 1303 to 1324, selected_file is only set inside the
user-selection loop which can lead to an UnboundLocalError in the
FileNotFoundError except block if an exception occurs earlier; initialize
selected_file to None before the selection loop (or at the start of the
enclosing try) and change the except FileNotFoundError handler to check if
selected_file is None and print a generic "❌ File not found or no file selected"
message, otherwise print the specific filename, ensuring the handler never
references an uninitialized variable.

print("ℹ️ Please ensure the activity file exists in the current directory")
except requests.exceptions.HTTPError as e:
if e.response.status_code == 409:
Expand Down Expand Up @@ -1363,7 +1366,6 @@ def upload_activity_file(api: Garmin) -> None:
print(f"❌ Too many requests: {e}")
print("💡 Please wait a few minutes before trying again")
except Exception as e:
# Check if this is a wrapped HTTP error from the Garmin library
error_str = str(e)
if "409 Client Error: Conflict" in error_str:
print(
Expand Down
Loading