Skip to content

feat: Activity dataclass#87

Merged
matin merged 1 commit intomatin:mainfrom
juvilius:feat/activity
Jan 8, 2026
Merged

feat: Activity dataclass#87
matin merged 1 commit intomatin:mainfrom
juvilius:feat/activity

Conversation

@juvilius
Copy link
Copy Markdown
Contributor

@juvilius juvilius commented Dec 30, 2024

and ci: dependency group instead of optional dependency for linting

I've had some troubles pleasing mypy, so please check if I've made a mistake before considering to merge. :)

Also: your Data class is focused on there being one of "it" per day. I'm not sure how to best reconcile it with activities.

Summary by CodeRabbit

  • New Features
    • Activity data models and API: retrieve single activities and list activities with pagination; Activity added to the public package API.
  • Utilities
    • Added helpers to normalize API payload keys by removing trailing DTO suffixes.
  • Tests
    • Added tests for Activity listing/getting/pagination and for the new DTO-key normalization utilities.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 30, 2024

Walkthrough

Adds a new Activity data module with Pydantic dataclasses and class methods to fetch/list Garmin activities, introduces DTO-suffix removal utilities, and exposes Activity on the package public API; includes tests for activity endpoints and the new utilities.

Changes

Cohort / File(s) Summary
Public API Exposure
src/garth/__init__.py, src/garth/data/__init__.py
Re-exports Activity by importing it and adding "Activity" to __all__.
Activity Data Models & API
src/garth/data/activity.py
New module adding Activity, ActivityType, EventType, and Summary dataclasses plus Activity.get() and Activity.list() which call /activity-service/activity/{id} and /activitylist-service/activities/search/activities, normalize camelCase, strip DTO suffixes, and construct model instances.
Data Transformation Utilities
src/garth/utils.py
Adds remove_dto_suffix(key: str) -> str and remove_dto_suffix_from_dict(data: dict[str, Any]) -> dict[str, Any] to recursively remove trailing _dto from keys.
Tests — Activity & Utils
tests/data/test_activity.py, tests/test_utils.py
New VCR-backed tests for Activity.list(), Activity.get(), pagination behavior, and unit tests for DTO-suffix removal utilities.

Sequence Diagram(s)

sequenceDiagram
    participant Caller as Caller
    participant Activity as Activity.get()
    participant HTTP as http.client
    participant API as /activity-service/activity/{id}
    Caller->>Activity: get(activity_id)
    Activity->>HTTP: HTTP GET /activity-service/activity/{id}
    HTTP->>API: request
    API-->>HTTP: JSON response (camelCase, DTO keys)
    HTTP-->>Activity: response dict
    Activity->>Activity: camel_to_snake_dict()
    Activity->>Activity: remove_dto_suffix_from_dict()
    Activity->>Activity: construct Activity instance
    Activity-->>Caller: Activity
Loading
sequenceDiagram
    participant Caller as Caller
    participant Activity as Activity.list()
    participant HTTP as http.client
    participant API as /activitylist-service/activities/search/activities
    Caller->>Activity: list(limit, start)
    Activity->>HTTP: HTTP POST/GET with pagination params
    HTTP->>API: request
    API-->>HTTP: JSON list response (camelCase, DTO keys)
    HTTP-->>Activity: response list
    loop per item
        Activity->>Activity: camel_to_snake_dict()
        Activity->>Activity: remove_dto_suffix_from_dict()
        Activity->>Activity: construct Activity instance
    end
    Activity-->>Caller: list[Activity]
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: introducing an Activity dataclass to the codebase, which is the primary focus of the PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • 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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 922f3c3 and 8fccc98.

📒 Files selected for processing (5)
  • Makefile (1 hunks)
  • garth/data/__init__.py (1 hunks)
  • garth/data/activity.py (1 hunks)
  • garth/utils.py (1 hunks)
  • pyproject.toml (1 hunks)
🔇 Additional comments (7)
garth/data/__init__.py (2)

1-1: Add 'Activity' to the module's public API
This change looks good and aligns with making the Activity class a first-class exported entity.


3-3: Importing Activity
No apparent issues, and consistency with the updated __all__ is maintained.

garth/utils.py (1)

36-43: Check for consistent suffix handling
The function correctly handles both "_dto" and "DTO" suffixes. The logic appears straightforward, and no issues are found with the slicing.

garth/data/activity.py (4)

9-13: Ensure consistent type usage
ActivityType is a minimal, frozen dataclass that correctly represents activity metadata. If you anticipate changes (e.g., additional fields), ensure future expansions remain consistent with Activity.


72-80: Check optional fields
The optional cadence/steps attributes help track running metrics. Ensure that downstream usage handles None values gracefully to avoid potential TypeErrors.


82-91: Revisit offset approach
The _get_localized_datetime method calculates the local offset and applies it to the GMT timestamp. Watch out for edge cases like daylight saving time transitions or historical offsets outside standard rules.


92-97: Exposing activity_start
This property elegantly exposes a localized start time. It provides clarity and ensures consistent usage with _get_localized_datetime.

Comment thread pyproject.toml
Comment thread Makefile Outdated
uv run ruff format --check $(sources)
uv run ruff check $(sources)
uv run mypy $(sources)
uv run --group linting mypy $(sources)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Ensure consistent usage of dependency groups

The lint target uses the new --group linting flag for mypy but not for ruff commands. This inconsistency could lead to confusion about how dependencies are managed.

Consider updating all linting commands to use the same approach:

.PHONY: lint  ## Lint python source files
lint: .uv
-	uv run ruff format --check $(sources)
-	uv run ruff check $(sources)
-	uv run --group linting mypy $(sources)
+	uv run --group linting ruff format --check $(sources)
+	uv run --group linting ruff check $(sources)
+	uv run --group linting mypy $(sources)

Also, the install target needs to be updated to match the new dependency management approach:

.PHONY: install
install: .uv .pre-commit
-	uv pip install -e ".[dev,linting,testing]"
+	uv pip install -e ".[dev,testing]" --group linting

Committable suggestion skipped: line range outside the PR's diff.

Comment thread garth/utils.py Outdated
Comment thread garth/data/activity.py Outdated
Comment thread garth/data/activity.py Outdated
Comment thread garth/data/activity.py Outdated
@matin matin self-requested a review March 17, 2025 23:33
matin
matin previously requested changes Apr 10, 2025
Copy link
Copy Markdown
Owner

@matin matin left a comment

Choose a reason for hiding this comment

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

@juvilius can you add tests?

@matin
Copy link
Copy Markdown
Owner

matin commented May 27, 2025

@juvilius I apologize for not getting around to reviewing earlier. do you want to rebase, add tests and submit the changes?

adding this endpoint makes sense, especially to make available in garth-mcp-server

@vactomas
Copy link
Copy Markdown
Contributor

vactomas commented Jul 7, 2025

Hi, @juvilius,
Is there anything I could help you with in order to get this PR ready? I could use this feature for my personal project as well.

@juvilius
Copy link
Copy Markdown
Contributor Author

juvilius commented Sep 7, 2025

Hi @matin,

sorry for the holdup. Let me explain.
I had modified garth enough to be able to generate a map of my running tracks. To not have to rely on Garmin, I stored everything in a postgres DB. I therefore had to create a data model, which turned out to be a bother with Activities coming in all different shapes and sizes depending on the type of activity and even on the device/watch. This lead me to lazily store everything as JSON and hardcode only a handful of fields.

Long story short, I don't know how to easily collect every possible field for all types of activity in a dataclass.
If anybody has an idea, go ahead :)

@codecov
Copy link
Copy Markdown

codecov Bot commented Jan 8, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (16c214e) to head (9c20cc3).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##              main       #87    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files           51        53     +2     
  Lines         2113      2309   +196     
==========================================
+ Hits          2113      2309   +196     
Flag Coverage Δ
unittests 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In @src/garth/data/activity.py:
- Around line 129-146: Duplicate metric fields (distance, duration,
elapsed_duration, moving_duration, elevation_gain, elevation_loss,
average_speed, max_speed, calories, average_hr, max_hr, owner_id,
owner_display_name, owner_full_name, steps,
average_running_cadence_in_steps_per_minute,
max_running_cadence_in_steps_per_minute) in Activity conflict with
ActivitySummary and should be removed or relocated to avoid ambiguity. Fix by
splitting list-vs-detail representations: create a new dataclass
ActivityListItem (or similar) that contains the list-specific fields currently
declared on Activity, keep Activity as the detailed model (removing the
duplicated metric fields) and add an optional summary: ActivitySummary | None to
Activity; alternatively add a single nested field like list_metadata:
ActivityListMetadata on Activity and move those fields there. Update any code
constructing Activity (e.g., where Activity is built from list() responses) to
use ActivityListItem or populate list_metadata instead, and update docstrings
for Activity/ActivityListItem to state which endpoint populates which fields.
- Around line 148-173: The get classmethod currently uses assert statements to
validate the API response; replace these asserts with explicit exception
handling: after calling client.connectapi(path) check if data is falsy and raise
a clear exception (e.g., RuntimeError or a custom ActivityFetchError) that
includes the request path and any client error info, and instead of asserting
the type, raise a TypeError or ValueError with a message like "Expected dict
from {path}, got {type(data).__name__}"; keep the rest of the flow
(camel_to_snake_dict, remove_dto_from_dict, return cls(**data)) unchanged but
ensure exceptions include the activity_id and path for easier debugging.
- Around line 175-203: The list classmethod should validate parameters and
replace the runtime assert with proper exceptions: check that limit and start
are integers and non-negative (raise TypeError for wrong types and ValueError
for negative values) before calling http.client.connectapi, then call client =
client or http.client and path =
"/activitylist-service/activities/search/activities" as before; after calling
client.connectapi, if the returned data is not a list raise a RuntimeError (or a
custom exception) including the path and the actual type received instead of
using assert, and keep the loop that transforms items with camel_to_snake_dict
and constructs cls(**item).
- Line 10: Add unit tests in tests/test_utils.py for both camel_to_snake_dict
and remove_dto_from_dict: create test cases with nested dictionaries, lists of
dictionaries, empty structures, non-dict scalars, and realistic Garmin API-like
payloads to verify recursive behavior; for camel_to_snake_dict assert keys are
converted at all nesting levels and that existing snake_case keys and non-string
values are preserved; for remove_dto_from_dict assert that DTO wrapper keys
(e.g., keys named "dto" or with "Dto" suffix) are removed or unwrapped
recursively from nested dicts and lists and that original payload values are
retained after removal. Use the imported functions camel_to_snake_dict and
remove_dto_from_dict in these tests and include multiple assertions covering
edge cases (deep nesting, lists containing mixed types) to ensure full coverage.
- Around line 1-14: The project currently imports ConfigDict (a Pydantic 2.0+
API) in activity.py but the pyproject ranges pydantic>=1.10.12,<3.0.0 which
allows Pydantic 1.x; update the dependency constraint to pydantic>=2.0.0,<3.0.0
in pyproject.toml so the installed Pydantic supports ConfigDict usage (no code
changes needed in activity.py; keep the ConfigDict import and pydantic_config
as-is).
🧹 Nitpick comments (4)
src/garth/utils.py (1)

36-62: LGTM! Utility functions correctly handle DTO suffix removal.

The implementation correctly strips DTO suffixes from keys and recursively processes nested structures, mirroring the pattern established by camel_to_snake_dict.

💡 Optional: Simplify remove_dto logic

The two branches in remove_dto perform the same operation. Consider this more concise version:

 def remove_dto(key: str) -> str:
-    if key.endswith("_dto"):
-        return key[: -len("_dto")]
-    elif key.endswith("DTO"):
-        return key[: -len("DTO")]
-    else:
-        return key
+    for suffix in ("_dto", "DTO"):
+        if key.endswith(suffix):
+            return key[:-len(suffix)]
+    return key

This reduces duplication while maintaining the same behavior.

tests/data/test_activity.py (1)

18-32: LGTM! Thorough validation of get endpoint.

The test correctly validates the detailed activity retrieval, including summary data. The pattern of fetching an ID from the list first is appropriate for API integration tests with VCR cassettes.

💡 Optional: Improve assertion clarity on Line 31

For consistency with the distance is not None check, consider:

-    assert activity.summary.distance is not None or activity.summary.duration
+    assert activity.summary.distance is not None or activity.summary.duration is not None

This makes the intent clearer (checking for presence of either metric) rather than relying on truthiness of the duration value.

src/garth/data/activity.py (2)

42-98: Consider documenting units for metric fields.

Many fields in ActivitySummary represent measurements (distance, duration, speed, elevation, etc.) but don't document their units. Users may need to know:

  • Is distance in meters, kilometers, or miles?
  • Is duration in seconds or milliseconds?
  • Is elevation_gain in meters or feet?
  • What are the units for average_speed?

Adding this information to field docstrings or the class docstring would improve usability.

📝 Example documentation approach
@dataclass(config=pydantic_config)
class ActivitySummary:
    """Summary metrics for an activity.
    
    Most fields are optional since different activity types return different
    metrics. For example, running activities have cadence data while swimming
    activities have stroke data.
    
    Units:
    - distance: meters
    - duration/elapsed_duration/moving_duration: seconds
    - speed: meters per second
    - elevation: meters
    - temperature: Celsius
    - power: watts
    - heart rate: beats per minute
    """

42-43: Clarify timezone handling for datetime fields.

The start_time_local and start_time_gmt fields (and others) use datetime but don't specify whether these are timezone-aware or naive datetime objects. Document the expected timezone behavior to help users handle these correctly.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8fccc98 and 4d4b0cf.

⛔ Files ignored due to path filters (3)
  • tests/data/cassettes/test_activity_get.yaml is excluded by !tests/**/cassettes/**
  • tests/data/cassettes/test_activity_list.yaml is excluded by !tests/**/cassettes/**
  • tests/data/cassettes/test_activity_list_pagination.yaml is excluded by !tests/**/cassettes/**
📒 Files selected for processing (6)
  • Makefile
  • src/garth/__init__.py
  • src/garth/data/__init__.py
  • src/garth/data/activity.py
  • src/garth/utils.py
  • tests/data/test_activity.py
🧰 Additional context used
📓 Path-based instructions (4)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: Use make format to auto-format Python source files using ruff
Use make lint to lint Python source files (ruff format check, ruff check, mypy)

Files:

  • src/garth/__init__.py
  • src/garth/utils.py
  • src/garth/data/__init__.py
  • tests/data/test_activity.py
  • src/garth/data/activity.py
**/__init__.py

📄 CodeRabbit inference engine (CLAUDE.md)

Provide the main client instance as garth.client for direct API access

Files:

  • src/garth/__init__.py
  • src/garth/data/__init__.py
tests/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

tests/**/*.py: Use pytest with VCR cassettes for HTTP recording/playback in tests
Maintain comprehensive test coverage across all modules with separate test directories mirroring source structure

Files:

  • tests/data/test_activity.py
tests/**

⚙️ CodeRabbit configuration file

tests/**: - test functions shouldn't have a return type hint

  • it's ok to use assert instead of pytest.assume()

Files:

  • tests/data/test_activity.py
🧠 Learnings (1)
📚 Learning: 2026-01-08T00:25:14.763Z
Learnt from: CR
Repo: matin/garth PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-08T00:25:14.763Z
Learning: Applies to **/*.py : Use `make lint` to lint Python source files (ruff format check, ruff check, mypy)

Applied to files:

  • Makefile
🧬 Code graph analysis (3)
src/garth/__init__.py (1)
src/garth/data/activity.py (1)
  • Activity (102-203)
src/garth/data/__init__.py (1)
src/garth/data/activity.py (1)
  • Activity (102-203)
tests/data/test_activity.py (3)
src/garth/data/activity.py (2)
  • Activity (102-203)
  • get (149-173)
src/garth/http.py (1)
  • Client (19-245)
tests/conftest.py (3)
  • vcr (79-82)
  • authed_client (65-75)
  • client (21-22)
🔇 Additional comments (8)
src/garth/data/__init__.py (1)

1-29: LGTM! Clean public API exposure.

The Activity export follows the established pattern for other data classes in this module. The addition to __all__ and the import statement are correctly placed in alphabetical order.

Makefile (1)

34-34: LGTM! Aligns with dependency group strategy.

Moving mypy to the linting dependency group is consistent with the PR's goal of using dependency groups instead of optional dependencies. This change properly isolates linting tools.

Based on learnings, this maintains the expected make lint behavior while improving dependency organization.

src/garth/__init__.py (1)

1-67: LGTM! Consistent top-level API exposure.

Activity is properly exposed at the package level, following the established pattern for other data classes. The alphabetical ordering in both the import statement and __all__ is maintained.

tests/data/test_activity.py (2)

7-16: LGTM! Comprehensive validation of list endpoint.

The test properly validates the list endpoint with appropriate assertions for activity fields and nested type information. The use of @pytest.mark.vcr follows the project's testing guidelines.


34-46: LGTM! Excellent pagination validation.

The test thoroughly validates pagination behavior, including the important check that pages contain distinct activities using isdisjoint(). This ensures the pagination parameters work correctly.

src/garth/data/activity.py (3)

16-30: LGTM!

The ActivityType and EventType dataclasses are well-structured with appropriate use of optional fields.


33-98: LGTM! Well-documented design choice.

The ActivitySummary dataclass appropriately handles the varying nature of activity data by making all metric fields optional. The docstring clearly explains this design decision, and the grouping of swimming-specific fields aids readability.


1-203: Ensure Pydantic mypy plugin is configured in pyproject.toml or mypy.ini.

The code uses Pydantic dataclasses and Self from typing_extensions. For proper type checking:

  1. Add plugins = ["pydantic.mypy"] to [tool.mypy] in pyproject.toml (use "pydantic.v1.mypy" for Pydantic v1)
  2. Verify mypy version is 1.19+ (required for Self support with typing_extensions)
  3. Run type checking via make lint to confirm all type annotations resolve without errors

Comment thread src/garth/data/activity.py Outdated
Comment on lines +1 to +14
from __future__ import annotations

from datetime import datetime

from pydantic import ConfigDict
from pydantic.dataclasses import dataclass
from typing_extensions import Self

from .. import http
from ..utils import camel_to_snake_dict, remove_dto_from_dict


pydantic_config = ConfigDict(extra="ignore")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

fd -t f -E node_modules -E .git 'pyproject\.toml|setup\.py|requirements.*\.txt' | head -20

Repository: matin/garth

Length of output: 69


🏁 Script executed:

cat pyproject.toml

Repository: matin/garth

Length of output: 2105


🏁 Script executed:

# Check for other uses of ConfigDict in the codebase
rg "ConfigDict" --type py

# Check for other imports of typing_extensions
rg "from typing_extensions import|import typing_extensions" --type py

Repository: matin/garth

Length of output: 1091


🏁 Script executed:

# Check if typing_extensions is a transitive dependency by looking at Pydantic's own dependencies
# or if it's mentioned anywhere in lock files

fd -t f 'poetry.lock|requirements.lock|uv.lock|pip-requirements.txt' --exec cat {} \;

# Also check if there's any documentation about dependencies
cat README.md 2>/dev/null | head -50

Repository: matin/garth

Length of output: 50368


🏁 Script executed:

# Search for typing_extensions and pydantic in the lock file
rg "name = \"typing.extensions|name = \"pydantic" pyproject.toml

Repository: matin/garth

Length of output: 37


🏁 Script executed:

# Search for typing_extensions in the lock file
rg "^\[\[package\]\]|^name = \"typing" pyproject.toml | head -20

# Try searching in lock file
rg '^\[\[package\]\]' pyproject.toml | head -10

# Actually, let's check if there's a uv.lock file or poetry.lock
ls -la | grep -i lock

Repository: matin/garth

Length of output: 113


🏁 Script executed:

# Search for typing_extensions in uv.lock
rg 'name = "typing.extensions"' uv.lock -A 5

# Search for pydantic in uv.lock
rg 'name = "pydantic"' uv.lock -A 10

Repository: matin/garth

Length of output: 7416


Fix Pydantic version constraint to match ConfigDict usage.

The code imports ConfigDict from Pydantic on line 5, which is a Pydantic 2.0+ feature that does not exist in Pydantic 1.10.12. The pyproject.toml dependency constraint pydantic>=1.10.12,<3.0.0 allows Pydantic 1.10.12, making installation incompatible. Update the constraint to pydantic>=2.0.0,<3.0.0 to enforce Pydantic 2.0+.

Note: typing_extensions is correctly available as a transitive dependency from Pydantic and does not need to be added separately.

🤖 Prompt for AI Agents
In @src/garth/data/activity.py around lines 1 - 14, The project currently
imports ConfigDict (a Pydantic 2.0+ API) in activity.py but the pyproject ranges
pydantic>=1.10.12,<3.0.0 which allows Pydantic 1.x; update the dependency
constraint to pydantic>=2.0.0,<3.0.0 in pyproject.toml so the installed Pydantic
supports ConfigDict usage (no code changes needed in activity.py; keep the
ConfigDict import and pydantic_config as-is).

Comment thread src/garth/data/activity.py Outdated
from typing_extensions import Self

from .. import http
from ..utils import camel_to_snake_dict, remove_dto_from_dict
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Check if utils.py exists and contains the required functions
echo "=== Checking for function implementations ==="
rg -A 15 "def (camel_to_snake_dict|remove_dto_from_dict)" src/garth/utils.py

echo -e "\n=== Checking for existing tests ==="
rg -l "camel_to_snake|remove_dto" --type=py | head -20

echo -e "\n=== Checking imports in activity.py ==="
head -20 src/garth/data/activity.py

echo -e "\n=== Checking usage context in activity.py ==="
rg -B 2 -A 5 "(camel_to_snake_dict|remove_dto_from_dict)" src/garth/data/activity.py

Repository: matin/garth

Length of output: 2847


🏁 Script executed:

#!/bin/bash

echo "=== Checking test coverage for utility functions ==="
cat tests/test_utils.py

echo -e "\n=== Verify helper functions that these depend on ==="
rg -A 5 "def camel_to_snake|def remove_dto" src/garth/utils.py

echo -e "\n=== Check lines 171-172 and 201 in activity.py ==="
sed -n '165,210p' src/garth/data/activity.py

Repository: matin/garth

Length of output: 4082


Add missing test for remove_dto_from_dict and expand edge case coverage.

The utility functions camel_to_snake_dict and remove_dto_from_dict are properly implemented with recursive handling for nested dictionaries and lists. However, remove_dto_from_dict lacks test coverage entirely—it's imported in tests/test_utils.py but has no test function. Additionally, test_camel_to_snake_dict() only tests basic key conversion; neither function is tested against edge cases like nested dictionaries, lists of dictionaries, or the actual API response structures from Garmin.

Add comprehensive tests for both functions, including nested and complex structures that match real API responses.

🤖 Prompt for AI Agents
In @src/garth/data/activity.py at line 10, Add unit tests in tests/test_utils.py
for both camel_to_snake_dict and remove_dto_from_dict: create test cases with
nested dictionaries, lists of dictionaries, empty structures, non-dict scalars,
and realistic Garmin API-like payloads to verify recursive behavior; for
camel_to_snake_dict assert keys are converted at all nesting levels and that
existing snake_case keys and non-string values are preserved; for
remove_dto_from_dict assert that DTO wrapper keys (e.g., keys named "dto" or
with "Dto" suffix) are removed or unwrapped recursively from nested dicts and
lists and that original payload values are retained after removal. Use the
imported functions camel_to_snake_dict and remove_dto_from_dict in these tests
and include multiple assertions covering edge cases (deep nesting, lists
containing mixed types) to ensure full coverage.

Comment on lines +129 to +146
# Fields from list endpoint (not present in detail endpoint)
distance: float | None = None
duration: float | None = None
elapsed_duration: float | None = None
moving_duration: float | None = None
elevation_gain: float | None = None
elevation_loss: float | None = None
average_speed: float | None = None
max_speed: float | None = None
calories: float | None = None
average_hr: float | None = None
max_hr: float | None = None
owner_id: int | None = None
owner_display_name: str | None = None
owner_full_name: str | None = None
steps: int | None = None
average_running_cadence_in_steps_per_minute: float | None = None
max_running_cadence_in_steps_per_minute: float | None = None
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Address field duplication between Activity and ActivitySummary.

The duplicate fields (lines 130-146) that overlap with ActivitySummary create ambiguity about which fields should be accessed. Users may not know whether to read activity.distance or activity.summary.distance, and the behavior differs depending on whether the Activity was retrieved via get() or list().

Consider one of these approaches:

  1. Create a separate ActivityListItem dataclass for the list endpoint response
  2. Document clearly in the docstring which fields are populated by which endpoint
  3. Move list-specific fields to a nested list_metadata or similar field
💡 Suggested refactoring approach

Option 1: Separate dataclasses (recommended)

@dataclass(config=pydantic_config)
class ActivityListItem:
    """Simplified activity data from list endpoint."""
    activity_id: int
    activity_name: str
    activity_type: ActivityType
    start_time_local: datetime | None = None
    distance: float | None = None
    # ... other list-specific fields
    
@dataclass(config=pydantic_config) 
class Activity:
    """Detailed activity data from get endpoint."""
    activity_id: int
    activity_name: str
    activity_type: ActivityType
    summary: ActivitySummary | None = None
    # ... no duplicate fields

Option 2: Enhanced documentation
Add to the Activity docstring:

"""
Note on field availability:
- When retrieved via get(): summary contains metrics, top-level metric fields are None
- When retrieved via list(): top-level metric fields are populated, summary is None
"""
🤖 Prompt for AI Agents
In @src/garth/data/activity.py around lines 129 - 146, Duplicate metric fields
(distance, duration, elapsed_duration, moving_duration, elevation_gain,
elevation_loss, average_speed, max_speed, calories, average_hr, max_hr,
owner_id, owner_display_name, owner_full_name, steps,
average_running_cadence_in_steps_per_minute,
max_running_cadence_in_steps_per_minute) in Activity conflict with
ActivitySummary and should be removed or relocated to avoid ambiguity. Fix by
splitting list-vs-detail representations: create a new dataclass
ActivityListItem (or similar) that contains the list-specific fields currently
declared on Activity, keep Activity as the detailed model (removing the
duplicated metric fields) and add an optional summary: ActivitySummary | None to
Activity; alternatively add a single nested field like list_metadata:
ActivityListMetadata on Activity and move those fields there. Update any code
constructing Activity (e.g., where Activity is built from list() responses) to
use ActivityListItem or populate list_metadata instead, and update docstrings
for Activity/ActivityListItem to state which endpoint populates which fields.

@matin matin mentioned this pull request Jan 8, 2026
19 tasks
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In @src/garth/data/activity.py:
- Around line 196-198: Replace the runtime-optimizable assert with an explicit
type check: instead of using assert isinstance(data, list), add an if not
isinstance(data, list): raise TypeError(f"Expected list from {path}, got
{type(data).__name__}") so invalid input cannot slip through when Python is run
with -O; keep the same message and reference to data and path to preserve
context.
- Around line 199-202: The list method in src/garth/data/activity.py only calls
camel_to_snake_dict but must also strip DTO-suffixed keys like the get method
does; update the loop that builds activities (where camel_to_snake_dict(item)
and activities.append(cls(**item)) are used) to call remove_dto_from_dict after
converting keys (e.g., item = camel_to_snake_dict(item); item =
remove_dto_from_dict(item)) so each dict passed to cls(**item) is normalized
consistently with the get method.
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4d4b0cf and 66339f6.

⛔ Files ignored due to path filters (3)
  • tests/data/cassettes/test_activity_get.yaml is excluded by !tests/**/cassettes/**
  • tests/data/cassettes/test_activity_list.yaml is excluded by !tests/**/cassettes/**
  • tests/data/cassettes/test_activity_list_pagination.yaml is excluded by !tests/**/cassettes/**
📒 Files selected for processing (5)
  • src/garth/__init__.py
  • src/garth/data/__init__.py
  • src/garth/data/activity.py
  • src/garth/utils.py
  • tests/data/test_activity.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/garth/data/init.py
  • src/garth/init.py
🧰 Additional context used
📓 Path-based instructions (3)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: Use make format to auto-format Python source files using ruff
Use make lint to lint Python source files (ruff format check, ruff check, mypy)

Files:

  • src/garth/utils.py
  • tests/data/test_activity.py
  • src/garth/data/activity.py
tests/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

tests/**/*.py: Use pytest with VCR cassettes for HTTP recording/playback in tests
Maintain comprehensive test coverage across all modules with separate test directories mirroring source structure

Files:

  • tests/data/test_activity.py
tests/**

⚙️ CodeRabbit configuration file

tests/**: - test functions shouldn't have a return type hint

  • it's ok to use assert instead of pytest.assume()

Files:

  • tests/data/test_activity.py
🧠 Learnings (2)
📚 Learning: 2026-01-08T00:25:14.763Z
Learnt from: CR
Repo: matin/garth PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-08T00:25:14.763Z
Learning: Applies to data/**/*.py : Raw data retrieval from Garmin Connect API should be organized in the `data/` module with submodules for specific data types (body_battery/, hrv.py, sleep.py, weight.py)

Applied to files:

  • src/garth/data/activity.py
📚 Learning: 2026-01-08T00:25:14.763Z
Learnt from: CR
Repo: matin/garth PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-08T00:25:14.763Z
Learning: Applies to tests/**/*.py : Maintain comprehensive test coverage across all modules with separate test directories mirroring source structure

Applied to files:

  • src/garth/data/activity.py
🧬 Code graph analysis (2)
tests/data/test_activity.py (1)
src/garth/data/activity.py (2)
  • Activity (102-203)
  • get (149-173)
src/garth/data/activity.py (3)
src/garth/utils.py (2)
  • camel_to_snake_dict (17-33)
  • remove_dto_from_dict (45-62)
tests/conftest.py (1)
  • client (21-22)
src/garth/http.py (2)
  • Client (19-245)
  • connectapi (186-192)
🔇 Additional comments (9)
src/garth/utils.py (2)

36-42: LGTM!

The function correctly handles both _dto and DTO suffix removal with clear logic.


45-62: LGTM!

The recursive implementation correctly handles nested dictionaries and lists, and follows the same pattern as the existing camel_to_snake_dict function for consistency.

tests/data/test_activity.py (3)

7-15: LGTM!

The test appropriately validates that the list endpoint returns activities with the expected basic fields.


18-31: LGTM!

The test correctly validates the get endpoint by first obtaining an activity ID from the list, then fetching full details. The assertion on Line 31 appropriately checks that at least one summary metric is present.


34-46: LGTM!

The pagination test appropriately validates that different pages return distinct activities and that the pagination parameters work as expected.

src/garth/data/activity.py (4)

1-13: LGTM!

The use of extra="ignore" in the Pydantic config is appropriate for handling varying activity fields across different activity types and devices, which addresses the author's stated concern about field variation.


16-23: LGTM!

The ActivityType dataclass has appropriate fields and type annotations.


26-30: LGTM!

The EventType dataclass has appropriate fields and type annotations.


33-98: LGTM!

The ActivitySummary dataclass comprehensively covers various activity metrics with appropriate optional fields. The docstring clearly explains why most fields are optional, which aligns well with the author's stated approach to handling field variation.

Comment on lines +196 to +198
assert isinstance(data, list), (
f"Expected list from {path}, got {type(data).__name__}"
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Replace assertion with proper exception.

Lines 196-198 use an assert statement for validation. Assertions can be disabled with Python's -O optimization flag, which would allow invalid data types to pass through unchecked.

🔒 Proposed fix using proper exception
     path = "/activitylist-service/activities/search/activities"
     data = client.connectapi(path, params={"limit": limit, "start": start})
-    assert isinstance(data, list), (
-        f"Expected list from {path}, got {type(data).__name__}"
-    )
+    if not isinstance(data, list):
+        raise TypeError(
+            f"Expected list from {path}, got {type(data).__name__}"
+        )
     activities = []
📝 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
assert isinstance(data, list), (
f"Expected list from {path}, got {type(data).__name__}"
)
path = "/activitylist-service/activities/search/activities"
data = client.connectapi(path, params={"limit": limit, "start": start})
if not isinstance(data, list):
raise TypeError(
f"Expected list from {path}, got {type(data).__name__}"
)
activities = []
🤖 Prompt for AI Agents
In @src/garth/data/activity.py around lines 196 - 198, Replace the
runtime-optimizable assert with an explicit type check: instead of using assert
isinstance(data, list), add an if not isinstance(data, list): raise
TypeError(f"Expected list from {path}, got {type(data).__name__}") so invalid
input cannot slip through when Python is run with -O; keep the same message and
reference to data and path to preserve context.

Comment thread src/garth/data/activity.py
and ci: dependency group instead of optional dependency for linting
@matin matin requested review from matin and removed request for matin January 8, 2026 21:08
@matin matin dismissed their stale review January 8, 2026 21:09

already applied the changes

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @src/garth/data/activity.py:
- Around line 171-199: In Activity.classmethod list, replace the runtime
assertion that checks the API response with explicit error handling: after
calling client.connectapi(path, params=...), verify if data is a list and if not
raise a clear exception (e.g., TypeError or RuntimeError) that includes the path
and the actual type received; mirror the approach used in get() so callers can
catch the error, and keep the subsequent processing (camel_to_snake_dict and
cls(**item)) unchanged while still using client or http.client as before.
- Around line 144-169: Replace the runtime assertions in Activity.get with
explicit exception raising: after calling client.connectapi(path), if data is
falsy raise a RuntimeError (or custom exception) with a clear message including
the path; if data is not a dict raise a TypeError (or ValueError) that states
the expected dict and the actual type (use type(data).__name__); keep using
camel_to_snake_dict, remove_dto_suffix_from_dict, and cls(**data) after these
checks so failures surface reliably even when Python optimizations are enabled.
🧹 Nitpick comments (3)
src/garth/utils.py (1)

36-40: Minor docstring inconsistency.

The docstring mentions "Remove _dto or DTO suffix" but the implementation only checks for _dto (lowercase). Consider either updating the docstring to match the implementation or extending the function to handle both cases if needed.

📝 Option 1: Update docstring to match implementation
-    """Remove _dto or DTO suffix from a key."""
+    """Remove _dto suffix from a key."""
📝 Option 2: Handle both cases (if needed by the API)
 def remove_dto_suffix(key: str) -> str:
-    """Remove _dto or DTO suffix from a key."""
-    if key.endswith("_dto"):
-        return key[:-4]
+    """Remove _dto or DTO suffix from a key."""
+    if key.endswith("_dto") or key.endswith("DTO"):
+        return key[:-4] if key.endswith("_dto") else key[:-3]
     return key
src/garth/data/activity.py (2)

97-142: Consider clarifying endpoint differences in the docstring.

The Activity class handles two different API response shapes: the detail endpoint (used by get()) returns data in a summary object, while the list endpoint returns similar fields directly on the activity. While the inline comment on line 125 mentions this, the docstring examples don't clarify which endpoint each example uses.

📝 Suggested docstring enhancement
     """Garmin Connect activity data.
 
     Retrieve individual activities by ID or list recent activities.
+    
+    Note: The detail endpoint (get) returns metrics in the summary field,
+    while the list endpoint returns metrics directly on the activity.
 
     Example:
         >>> activity = Activity.get(12345678901)
         >>> activity.activity_name
         'Morning Run'
         >>> activity.summary.distance
         5000.0
 
         >>> activities = Activity.list(limit=10)
+        >>> activities[0].distance  # List endpoint returns distance directly
+        5000.0
         >>> len(activities)
         10
     """

172-177: Consider adding parameter validation.

The limit and start parameters lack validation. While the API might handle invalid values, adding bounds checking would provide better error messages and prevent unnecessary API calls.

🔍 Suggested parameter validation
     @classmethod
     def list(
         cls,
         limit: int = 20,
         start: int = 0,
         *,
         client: http.Client | None = None,
     ) -> list[Self]:
         """List recent activities with pagination.
 
         Args:
             limit: Maximum number of activities to return (default 20)
             start: Offset for pagination (default 0)
             client: Optional HTTP client (uses default if not provided)
 
         Returns:
             List of Activity instances (simplified, without full summary)
         """
+        if limit <= 0:
+            raise ValueError(f"limit must be positive, got {limit}")
+        if start < 0:
+            raise ValueError(f"start must be non-negative, got {start}")
         client = client or http.client
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 66339f6 and 9c20cc3.

⛔ Files ignored due to path filters (3)
  • tests/data/cassettes/test_activity_get.yaml is excluded by !tests/**/cassettes/**
  • tests/data/cassettes/test_activity_list.yaml is excluded by !tests/**/cassettes/**
  • tests/data/cassettes/test_activity_list_pagination.yaml is excluded by !tests/**/cassettes/**
📒 Files selected for processing (6)
  • src/garth/__init__.py
  • src/garth/data/__init__.py
  • src/garth/data/activity.py
  • src/garth/utils.py
  • tests/data/test_activity.py
  • tests/test_utils.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/garth/init.py
  • src/garth/data/init.py
  • tests/data/test_activity.py
🧰 Additional context used
📓 Path-based instructions (3)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: Use make format to auto-format Python source files using ruff
Use make lint to lint Python source files (ruff format check, ruff check, mypy)

Files:

  • src/garth/utils.py
  • tests/test_utils.py
  • src/garth/data/activity.py
tests/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

tests/**/*.py: Use pytest with VCR cassettes for HTTP recording/playback in tests
Maintain comprehensive test coverage across all modules with separate test directories mirroring source structure

Files:

  • tests/test_utils.py
tests/**

⚙️ CodeRabbit configuration file

tests/**: - test functions shouldn't have a return type hint

  • it's ok to use assert instead of pytest.assume()

Files:

  • tests/test_utils.py
🧠 Learnings (3)
📓 Common learnings
Learnt from: CR
Repo: matin/garth PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-08T00:25:14.763Z
Learning: Applies to data/**/*.py : Raw data retrieval from Garmin Connect API should be organized in the `data/` module with submodules for specific data types (body_battery/, hrv.py, sleep.py, weight.py)
📚 Learning: 2026-01-08T00:25:14.763Z
Learnt from: CR
Repo: matin/garth PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-08T00:25:14.763Z
Learning: Applies to data/**/*.py : Raw data retrieval from Garmin Connect API should be organized in the `data/` module with submodules for specific data types (body_battery/, hrv.py, sleep.py, weight.py)

Applied to files:

  • src/garth/data/activity.py
📚 Learning: 2026-01-08T00:25:14.763Z
Learnt from: CR
Repo: matin/garth PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-08T00:25:14.763Z
Learning: Applies to tests/**/*.py : Maintain comprehensive test coverage across all modules with separate test directories mirroring source structure

Applied to files:

  • src/garth/data/activity.py
🧬 Code graph analysis (2)
tests/test_utils.py (1)
src/garth/utils.py (2)
  • remove_dto_suffix (36-40)
  • remove_dto_suffix_from_dict (43-63)
src/garth/data/activity.py (2)
src/garth/utils.py (2)
  • camel_to_snake_dict (17-33)
  • remove_dto_suffix_from_dict (43-63)
src/garth/http.py (2)
  • Client (19-245)
  • connectapi (186-192)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test 3.12
🔇 Additional comments (5)
tests/test_utils.py (1)

70-132: LGTM! Comprehensive test coverage.

The tests thoroughly cover the new DTO suffix removal utilities including edge cases (empty strings, keys without suffix, nested structures, and lists). The test structure follows existing patterns in the file.

src/garth/utils.py (1)

43-63: LGTM! Clean recursive implementation.

The function correctly handles nested dictionaries and lists, mirroring the pattern used in camel_to_snake_dict. The docstring effectively explains the API inconsistency this function addresses.

src/garth/data/activity.py (3)

1-19: LGTM! Clean dataclass definition.

The imports are appropriate and the ActivityType dataclass is well-structured with clear required and optional fields.


22-26: LGTM!

Simple, well-defined dataclass.


29-94: LGTM! Comprehensive summary metrics.

The extensive optional fields are well-justified by the docstring, which explains that different activity types return different metrics. This design appropriately handles the varying API responses mentioned in the PR description.

Comment on lines +144 to +169
@classmethod
def get(
cls,
activity_id: int,
*,
client: http.Client | None = None,
) -> Self:
"""Get detailed activity data by activity ID.

Args:
activity_id: The Garmin activity ID
client: Optional HTTP client (uses default if not provided)

Returns:
Activity instance with full details including summary metrics
"""
client = client or http.client
path = f"/activity-service/activity/{activity_id}"
data = client.connectapi(path)
assert data, f"No data returned from {path}"
assert isinstance(data, dict), (
f"Expected dict from {path}, got {type(data).__name__}"
)
data = camel_to_snake_dict(data)
data = remove_dto_suffix_from_dict(data)
return cls(**data)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Replace assertions with proper error handling.

Using assert for runtime validation is problematic because assertions can be disabled with Python's -O optimization flag, which would cause the function to fail silently or with unhelpful errors in production.

🛡️ Proposed fix: Use explicit exception raising
         client = client or http.client
         path = f"/activity-service/activity/{activity_id}"
         data = client.connectapi(path)
-        assert data, f"No data returned from {path}"
-        assert isinstance(data, dict), (
-            f"Expected dict from {path}, got {type(data).__name__}"
-        )
+        if not data:
+            raise ValueError(f"No data returned from {path}")
+        if not isinstance(data, dict):
+            raise TypeError(
+                f"Expected dict from {path}, got {type(data).__name__}"
+            )
         data = camel_to_snake_dict(data)
         data = remove_dto_suffix_from_dict(data)
         return cls(**data)
📝 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
@classmethod
def get(
cls,
activity_id: int,
*,
client: http.Client | None = None,
) -> Self:
"""Get detailed activity data by activity ID.
Args:
activity_id: The Garmin activity ID
client: Optional HTTP client (uses default if not provided)
Returns:
Activity instance with full details including summary metrics
"""
client = client or http.client
path = f"/activity-service/activity/{activity_id}"
data = client.connectapi(path)
assert data, f"No data returned from {path}"
assert isinstance(data, dict), (
f"Expected dict from {path}, got {type(data).__name__}"
)
data = camel_to_snake_dict(data)
data = remove_dto_suffix_from_dict(data)
return cls(**data)
@classmethod
def get(
cls,
activity_id: int,
*,
client: http.Client | None = None,
) -> Self:
"""Get detailed activity data by activity ID.
Args:
activity_id: The Garmin activity ID
client: Optional HTTP client (uses default if not provided)
Returns:
Activity instance with full details including summary metrics
"""
client = client or http.client
path = f"/activity-service/activity/{activity_id}"
data = client.connectapi(path)
if not data:
raise ValueError(f"No data returned from {path}")
if not isinstance(data, dict):
raise TypeError(
f"Expected dict from {path}, got {type(data).__name__}"
)
data = camel_to_snake_dict(data)
data = remove_dto_suffix_from_dict(data)
return cls(**data)
🤖 Prompt for AI Agents
In @src/garth/data/activity.py around lines 144 - 169, Replace the runtime
assertions in Activity.get with explicit exception raising: after calling
client.connectapi(path), if data is falsy raise a RuntimeError (or custom
exception) with a clear message including the path; if data is not a dict raise
a TypeError (or ValueError) that states the expected dict and the actual type
(use type(data).__name__); keep using camel_to_snake_dict,
remove_dto_suffix_from_dict, and cls(**data) after these checks so failures
surface reliably even when Python optimizations are enabled.

Comment on lines +171 to +199
@classmethod
def list(
cls,
limit: int = 20,
start: int = 0,
*,
client: http.Client | None = None,
) -> list[Self]:
"""List recent activities with pagination.

Args:
limit: Maximum number of activities to return (default 20)
start: Offset for pagination (default 0)
client: Optional HTTP client (uses default if not provided)

Returns:
List of Activity instances (simplified, without full summary)
"""
client = client or http.client
path = "/activitylist-service/activities/search/activities"
data = client.connectapi(path, params={"limit": limit, "start": start})
assert isinstance(data, list), (
f"Expected list from {path}, got {type(data).__name__}"
)
activities = []
for item in data:
item = camel_to_snake_dict(item)
activities.append(cls(**item))
return activities
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Replace assertions with proper error handling.

Similar to the get() method, this uses assert for runtime validation which can be disabled with Python's -O flag.

🛡️ Proposed fix: Use explicit exception raising
         client = client or http.client
         path = "/activitylist-service/activities/search/activities"
         data = client.connectapi(path, params={"limit": limit, "start": start})
-        assert isinstance(data, list), (
-            f"Expected list from {path}, got {type(data).__name__}"
-        )
+        if not isinstance(data, list):
+            raise TypeError(
+                f"Expected list from {path}, got {type(data).__name__}"
+            )
         activities = []
         for item in data:
             item = camel_to_snake_dict(item)
             activities.append(cls(**item))
         return activities
📝 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
@classmethod
def list(
cls,
limit: int = 20,
start: int = 0,
*,
client: http.Client | None = None,
) -> list[Self]:
"""List recent activities with pagination.
Args:
limit: Maximum number of activities to return (default 20)
start: Offset for pagination (default 0)
client: Optional HTTP client (uses default if not provided)
Returns:
List of Activity instances (simplified, without full summary)
"""
client = client or http.client
path = "/activitylist-service/activities/search/activities"
data = client.connectapi(path, params={"limit": limit, "start": start})
assert isinstance(data, list), (
f"Expected list from {path}, got {type(data).__name__}"
)
activities = []
for item in data:
item = camel_to_snake_dict(item)
activities.append(cls(**item))
return activities
@classmethod
def list(
cls,
limit: int = 20,
start: int = 0,
*,
client: http.Client | None = None,
) -> list[Self]:
"""List recent activities with pagination.
Args:
limit: Maximum number of activities to return (default 20)
start: Offset for pagination (default 0)
client: Optional HTTP client (uses default if not provided)
Returns:
List of Activity instances (simplified, without full summary)
"""
client = client or http.client
path = "/activitylist-service/activities/search/activities"
data = client.connectapi(path, params={"limit": limit, "start": start})
if not isinstance(data, list):
raise TypeError(
f"Expected list from {path}, got {type(data).__name__}"
)
activities = []
for item in data:
item = camel_to_snake_dict(item)
activities.append(cls(**item))
return activities
🤖 Prompt for AI Agents
In @src/garth/data/activity.py around lines 171 - 199, In Activity.classmethod
list, replace the runtime assertion that checks the API response with explicit
error handling: after calling client.connectapi(path, params=...), verify if
data is a list and if not raise a clear exception (e.g., TypeError or
RuntimeError) that includes the path and the actual type received; mirror the
approach used in get() so callers can catch the error, and keep the subsequent
processing (camel_to_snake_dict and cls(**item)) unchanged while still using
client or http.client as before.

@matin matin merged commit 8028441 into matin:main Jan 8, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants