-
-
Notifications
You must be signed in to change notification settings - Fork 223
Add training plans API support #285
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
Warning
|
Cohort / File(s) | Summary |
---|---|
Documentation updateREADME.md |
Added “📅 Training Plans” to the main category list, updated category count (11 → 12) and added API coverage line noting 3 Training Plans methods (text-only changes). |
CLI / demodemo.py |
Added public get_training_plan_by_id_data(api) ; extended execute_api_call mapping with get_training_plans and get_training_plan_by_id ; added main menu category "b" “📅 Training Plans” with options for listing plans and fetching by ID. |
Garmin API: Training Plansgarminconnect/__init__.py |
Added garmin_connect_training_plan_url attribute and three methods on Garmin : get_training_plans() , get_training_plan_by_id(plan_id) , and get_adaptive_training_plan_by_id(plan_id) which validate IDs, build endpoints (/plans /plans/{id} /fbt-adaptive/{id} ), log and delegate requests to connectapi . |
Sequence Diagram(s)
sequenceDiagram
autonumber
actor User
participant CLI as demo.py (menu)
participant Exec as execute_api_call
participant Garmin as Garmin
participant API as connectapi
User->>CLI: Select 📅 Training Plans
CLI->>Exec: Key "get_training_plans"
Exec->>Garmin: get_training_plans()
Garmin->>API: GET {garmin_connect_training_plan_url}/plans
API-->>Garmin: Plans list (JSON)
Garmin-->>Exec: Plans list
Exec-->>User: Display plans
rect rgba(200,230,255,0.3)
note over User,Garmin: Fetch training plan by ID (standard or adaptive)
User->>CLI: Choose "Get training plan by ID"
CLI->>Exec: Key "get_training_plan_by_id"
Exec->>CLI: Prompt for plan_id and (optional) category hint
CLI->>Exec: plan_id, category
alt Standard plan
Exec->>Garmin: get_training_plan_by_id(plan_id)
Garmin->>API: GET {garmin_connect_training_plan_url}/plans/{plan_id}
else Adaptive plan
Exec->>Garmin: get_adaptive_training_plan_by_id(plan_id)
Garmin->>API: GET {garmin_connect_training_plan_url}/fbt-adaptive/{plan_id}
end
API-->>Garmin: Plan details (JSON)
Garmin-->>Exec: Plan details
Exec-->>User: Display details
end
Estimated code review effort
🎯 3 (Moderate) | ⏱️ ~25 minutes
Poem
I hop through docs and code with cheer,
New calendars bloom, two choices appear.
IDs asked, adaptive or plain,
URLs stitched, responses gain.
A rabbit's nibble — menu and API near. 🐰📅
Pre-merge checks and finishing touches
✅ Passed checks (3 passed)
Check name | Status | Explanation |
---|---|---|
Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
Title Check | ✅ Passed | The title "Add training plans API support" is concise, specific, and accurately reflects the main change in the PR — introducing training plans endpoints and related demo/menu updates; it is clear and focused for a teammate scanning history. |
Docstring Coverage | ✅ Passed | Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. |
✨ Finishing touches
- 📝 Generate Docstrings
🧪 Generate unit tests
- Create PR with unit tests
- Post copyable unit tests in a comment
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help
to get the list of available commands and usage tips.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
README.md (1)
37-37
: Update category count to reflect new menu section (now 12).You added a new main category; adjust the count.
-- **Categories**: 11 organized sections +- **Categories**: 12 organized sections
🧹 Nitpick comments (3)
README.md (1)
27-27
: Fix title-case and trailing space in menu entry.Use “Training Plans” (title case) and drop the trailing space.
- [b] 📅 Training plans + [b] 📅 Training Plansgarminconnect/__init__.py (1)
269-270
: Align attribute naming with existinggarmin_connect_*
pattern.All other URL attributes use
garmin_connect_*
. Rename for consistency.- self.garmin_training_plan_url = "/trainingplan-service/trainingplan" + self.garmin_connect_training_plan_url = "/trainingplan-service/trainingplan"And update call sites:
- url = f"{self.garmin_training_plan_url}/plans" + url = f"{self.garmin_connect_training_plan_url}/plans"- url = f"{self.garmin_training_plan_url}/plans/{plan_id}" + url = f"{self.garmin_connect_training_plan_url}/plans/{plan_id}"- url = f"{self.garmin_training_plan_url}/fbt-adaptive/{plan_id}" + url = f"{self.garmin_connect_training_plan_url}/fbt-adaptive/{plan_id}"demo.py (1)
1773-1800
: Fix misleading docstring and make “by ID” actually accept an ID (with safe fallback).
- Docstring says adaptive not supported, but code does support it; update.
- Menu option says “by ID” but code always uses the last plan. Prompt for an ID and fallback to latest when omitted.
- Avoid KeyError if
trainingPlanList
is absent.-def get_training_plan_by_id_data(api: Garmin) -> None: - """Get training plan by ID. adaptive plans are not supported. use get_adaptive_training_plan_by_id instead""" +def get_training_plan_by_id_data(api: Garmin) -> None: + """Get training plan details by ID (routes FBT_ADAPTIVE plans to the adaptive endpoint).""" try: - training_plans = api.get_training_plans()["trainingPlanList"] - if training_plans: - plan_id = training_plans[-1]["trainingPlanId"] - plan_name = training_plans[-1]["name"] - plan_category = training_plans[-1]["trainingPlanCategory"] - - if plan_category == "FBT_ADAPTIVE": - call_and_display( - api.get_adaptive_training_plan_by_id, - plan_id, - method_name="get_adaptive_training_plan_by_id", - api_call_desc=f"api.get_adaptive_training_plan_by_id({plan_id}) - {plan_name}", - ) - else: - call_and_display( - api.get_training_plan_by_id, - plan_id, - method_name="get_training_plan_by_id", - api_call_desc=f"api.get_training_plan_by_id({plan_id}) - {plan_name}", - ) - else: - print("ℹ️ No training plans found") + resp = api.get_training_plans() or {} + training_plans = resp.get("trainingPlanList") or [] + if not training_plans: + print("ℹ️ No training plans found") + return + + user_input = input("Enter training plan ID (press Enter for most recent): ").strip() + selected = None + if user_input: + try: + wanted_id = int(user_input) + selected = next( + (p for p in training_plans if int(p.get("trainingPlanId", 0)) == wanted_id), + None, + ) + if not selected: + print(f"ℹ️ Plan ID {wanted_id} not found in your plans; attempting fetch anyway") + plan_id = wanted_id + plan_name = f"Plan {wanted_id}" + plan_category = None + else: + plan_id = int(selected["trainingPlanId"]) + plan_name = selected.get("name", str(plan_id)) + plan_category = selected.get("trainingPlanCategory") + except ValueError: + print("❌ Invalid plan ID") + return + else: + selected = training_plans[-1] + plan_id = int(selected["trainingPlanId"]) + plan_name = selected.get("name", str(plan_id)) + plan_category = selected.get("trainingPlanCategory") + + if plan_category == "FBT_ADAPTIVE": + call_and_display( + api.get_adaptive_training_plan_by_id, + plan_id, + method_name="get_adaptive_training_plan_by_id", + api_call_desc=f"api.get_adaptive_training_plan_by_id({plan_id}) - {plan_name}", + ) + else: + call_and_display( + api.get_training_plan_by_id, + plan_id, + method_name="get_training_plan_by_id", + api_call_desc=f"api.get_training_plan_by_id({plan_id}) - {plan_name}", + ) except Exception as e: print(f"❌ Error getting plan by ID: {e}")
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
README.md
(2 hunks)demo.py
(3 hunks)garminconnect/__init__.py
(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
demo.py (1)
garminconnect/__init__.py (3)
get_training_plans
(2168-2173)get_adaptive_training_plan_by_id
(2184-2191)get_training_plan_by_id
(2175-2182)
🔇 Additional comments (5)
garminconnect/__init__.py (3)
2168-2174
: LGTM: list endpoint wiring and logging are correct.Endpoint path and error flow via
connectapi
look consistent with the rest of the class.If some accounts/regions return a different envelope than
{"trainingPlanList": [...]}
, the demo may KeyError. Consider confirming envelope shape across profiles.
2175-2183
: LGTM: ID validation and fetch by ID.Casts to int and validates positivity; URL path looks correct.
2184-2192
: LGTM: adaptive plan by ID.Consistent validation and routing to adaptive endpoint.
demo.py (2)
418-424
: LGTM: New Training Plans menu category.Menu wiring is clear and matches the new API methods.
3163-3167
: LGTM: API mapping for new endpoints.Entries correctly route to the newly added functions.
Based on CodeRabbit feedback Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (1)
demo.py (1)
1773-1828
: Harden error handling and truly pick “most recent”; normalize adaptive check
- Wrap the initial fetch with safe_api_call to reuse uniform HTTP handling.
- When defaulting to “most recent,” sort by an available timestamp instead of taking the last element.
- Normalize the category comparison to be case-insensitive.
Suggested patch:
def get_training_plan_by_id_data(api: Garmin) -> None: """Get training plan details by ID (routes FBT_ADAPTIVE plans to the adaptive endpoint).""" - resp = api.get_training_plans() or {} - training_plans = resp.get("trainingPlanList") or [] + ok, resp, _ = safe_api_call(api.get_training_plans, method_name="get_training_plans") + if not ok or not resp: + return + training_plans = (resp or {}).get("trainingPlanList") or [] if not training_plans: print("ℹ️ No training plans found") return @@ - else: - selected = training_plans[-1] - plan_id = int(selected["trainingPlanId"]) - plan_name = selected.get("name", str(plan_id)) - plan_category = selected.get("trainingPlanCategory") + else: + # Choose the most recently updated/created plan when no ID is provided + candidates = ["lastUpdatedDate", "updatedDate", "createDate", "startDate"] + def _to_dt(p: dict) -> datetime.datetime: + for k in candidates: + v = p.get(k) + if isinstance(v, str): + with suppress(Exception): + return datetime.datetime.fromisoformat(v.replace("Z", "+00:00")) + return datetime.datetime.min + selected = max(training_plans, key=_to_dt, default=training_plans[-1]) + plan_id = int(selected["trainingPlanId"]) + plan_name = selected.get("name", str(plan_id)) + plan_category = selected.get("trainingPlanCategory") - if plan_category == "FBT_ADAPTIVE": + category_norm = (str(plan_category).upper() if plan_category is not None else "") + if category_norm == "FBT_ADAPTIVE": call_and_display( api.get_adaptive_training_plan_by_id, plan_id, method_name="get_adaptive_training_plan_by_id", api_call_desc=f"api.get_adaptive_training_plan_by_id({plan_id}) - {plan_name}", )Please confirm the training plan list objects expose one of these timestamp fields: lastUpdatedDate, updatedDate, createDate, or startDate. If they differ, adjust the candidates list accordingly.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
README.md
(3 hunks)demo.py
(3 hunks)garminconnect/__init__.py
(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- README.md
- garminconnect/init.py
🧰 Additional context used
🧬 Code graph analysis (1)
demo.py (1)
garminconnect/__init__.py (4)
Garmin
(94-2191)get_training_plans
(2168-2173)get_adaptive_training_plan_by_id
(2184-2191)get_training_plan_by_id
(2175-2182)
🔇 Additional comments (2)
demo.py (2)
418-424
: Nice addition: Training Plans category in the menuCategory wiring and option keys look consistent with the rest of the menu system.
3190-3195
: API method wiring — LGTMBoth new keys are correctly mapped; get_training_plan_by_id uses the helper that routes adaptive plans.
Hi Nick - This is a great start - Are you considering adding pushing training plans support as well? Note: I have some training plans created in my account, but when I asked to list the training plans, I got an empty array. |
That was my plan, but to keep the merge request small i started with adaptive training plan which is what is use. What kind of training plan have you setup in connect? is it a Garmin Coach plan or a regular training plan for eihter running or cycling |
I have created a Regular Plan through garmin connect - Wanted to know if I could programatically do it somehow like how (https://lifttrackapp.com/) does with Strength workouts |
I have added support for fetching training plan data.
There are 3 endpoints added:
get_training_plans()
- Retrieve all available training plansget_training_plan_by_id()
- Get details for a specific training planget_adaptive_training_plan_by_id()
- Get details for adaptive training plansThere are more training plan endpoints, like Strength Training Plans, Phased Training Plans, but this is a start.
Summary by CodeRabbit
New Features
Documentation