Skip to content

Add Body Battery Support#116

Merged
matin merged 23 commits intomatin:mainfrom
bes-dev:bes-dev/body_battery_0001
Jun 2, 2025
Merged

Add Body Battery Support#116
matin merged 23 commits intomatin:mainfrom
bes-dev:bes-dev/body_battery_0001

Conversation

@bes-dev
Copy link
Copy Markdown
Contributor

@bes-dev bes-dev commented May 27, 2025

Add Body Battery Support

Summary

  • Add comprehensive Body Battery metric support to garth library
  • Implement both legacy Body Battery events and daily Body Battery with stress data
  • Provide complete test coverage following existing patterns

What's Added

New Data Classes

BodyBatteryData - Legacy Body Battery events data

# Get Body Battery events for a specific date (e.g., sleep events)
events = garth.BodyBatteryData.get("2023-07-20")
for event in events:
    if event.event:
        print(f"Event: {event.event.event_type}, Impact: {event.event.body_battery_impact}")
    print(f"Current level: {event.current_level}")

DailyBodyBatteryStress - Complete daily Body Battery and stress data

# Get comprehensive Body Battery and stress data for a day
daily_data = garth.DailyBodyBatteryStress.get("2023-07-20")
if daily_data:
    print(f"Current Body Battery: {daily_data.current_body_battery}")
    print(f"Daily change: {daily_data.body_battery_change}")
    print(f"Max stress: {daily_data.max_stress_level}")

    # Access individual readings
    for reading in daily_data.body_battery_readings:
        print(f"Time: {reading.timestamp}, Level: {reading.level}, Status: {reading.status}")

Key Features

  1. Dual API Support:
  • /wellness-service/wellness/bodyBattery/events/{date} - Event-based data (sleep, activities)
  • /wellness-service/wellness/dailyStress/{date} - Complete daily data with stress
  1. Rich Data Properties:
  • current_body_battery, max_body_battery, min_body_battery
  • body_battery_change - Daily Body Battery difference
  • body_battery_readings - Structured timestamp/level data
  • stress_readings - Parallel stress level data
  1. Consistent API:
  • .get(date) - Single day data
  • .list(end_date, period) - Multiple days with automatic date handling

API Endpoints Used

  • GET /wellness-service/wellness/bodyBattery/events/{date} - Body Battery events
  • GET /wellness-service/wellness/dailyStress/{date} - Daily stress + Body Battery data

Test Coverage

  • Complete test suite with 6 test functions covering all methods and edge cases
  • VCR cassettes for reproducible API testing
  • Property validation and error handling tests
  • Follows existing test patterns for HRV and Sleep data

Files Changed

  • src/garth/data/body_battery.py - New Body Battery implementation
  • src/garth/data/init.py - Export new classes
  • src/garth/init.py - Add to public API
  • tests/data/test_body_battery_data.py - Comprehensive test suite
  • tests/data/cassettes/ - 6 new test cassettes

Example Usage

import garth
from datetime import date

# Authenticate
garth.resume("~/.garth")

# Get current Body Battery data
today_data = garth.DailyBodyBatteryStress.get(date.today())
if today_data:
    print(f"🔋 Current Body Battery: {today_data.current_body_battery}")
    print(f"📊 Daily change: {today_data.body_battery_change}")
    print(f"😰 Average stress: {today_data.avg_stress_level}")

# Get Body Battery events (sleep, activities)
events = garth.BodyBatteryData.get(date.today())
for event in events:
    if event.event and event.event.event_type == "sleep":
        print(f"😴 Sleep impact: +{event.event.body_battery_impact}")

# Get week of data
week_data = garth.DailyBodyBatteryStress.list(period=7)
for day in week_data:
    print(f"{day.calendar_date}: {day.current_body_battery}")

This implementation fills a significant gap in the library's health metrics coverage, providing access to one of Garmin's most popular features alongside existing HRV, sleep, stress, and activity data.

Summary by CodeRabbit

  • New Features

    • Introduced support for Body Battery and stress data, including daily summaries and event-based details.
    • Added new data structures for Body Battery readings, stress readings, and related metrics.
    • Enabled retrieval and listing of Body Battery and stress data over multiple days.
  • Tests

    • Added comprehensive tests for Body Battery and stress data retrieval, properties, and error handling.
  • Chores

    • Updated development dependencies management and reorganized dependency groups in project configuration.
    • Improved installation commands in the Makefile for development, linting, and testing dependencies.

bes-dev added 2 commits May 27, 2025 22:39
  - Add DailyBodyBatteryStress class for complete daily data
  - Add BodyBatteryData class for sleep events
  - Support for full day Body Battery and stress readings
  - API endpoints: /wellness-service/wellness/dailyStress/{date}
  - API endpoints: /wellness-service/wellness/bodyBattery/events/{date}
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented May 27, 2025

Walkthrough

The changes introduce new data models and API access methods for Body Battery and stress data, expanding the public API at multiple package levels. Supporting parsing utilities and tests for these models were added. The dependency management was updated by moving optional dependency groups into a [dependency-groups] section. The Makefile install target and a test helper function were also modified.

Changes

File(s) Change Summary
src/garth/init.py Expanded imports and __all__ to include BodyBatteryData and DailyBodyBatteryStress.
src/garth/data/init.py Added exports for BodyBatteryData, BodyBatteryEvent, BodyBatteryReading, DailyBodyBatteryStress, and StressReading to the module's public API.
src/garth/data/body_battery/init.py New module defining __all__ for body_battery package, exporting five entities and importing them from submodules.
src/garth/data/body_battery/daily_stress.py New dataclass DailyBodyBatteryStress with fields, computed properties, and a class method to fetch daily data from an API.
src/garth/data/body_battery/events.py New module defining BodyBatteryEvent and BodyBatteryData dataclasses, properties to process readings, and class methods to fetch and aggregate event data from an API, including concurrent retrieval for multiple days.
src/garth/data/body_battery/readings.py New module with BodyBatteryReading and StressReading dataclasses, and parsing functions for reading arrays.
tests/data/test_body_battery_data.py New test module with pytest-based tests for BodyBatteryData and DailyBodyBatteryStress, covering data retrieval, properties, edge cases, and HTTP interaction replay.
pyproject.toml Moved dev, linting, and testing optional dependency groups from [project.optional-dependencies] to a new [dependency-groups] section with unchanged contents.
tests/conftest.py Modified sanitize_response to check type and existence before sanitizing sensitive fields to prevent errors.
Makefile Changed install target to separate editable install and dependency installation by compiling and installing dev, linting, and testing groups separately instead of a single editable install with extras.
.github/workflows/ci.yml Modified lint and test job steps to install base package and dependency groups separately instead of a single editable install with extras.
src/garth/data/_base.py Expanded return type of abstract get method to allow `Self
src/garth/version.py Updated version from "0.5.10" to "0.5.11".

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Client
    participant API
    participant BodyBatteryData
    participant DailyBodyBatteryStress

    User->>BodyBatteryData: get(date, client)
    BodyBatteryData->>Client: GET /wellness-api/rest/bodyBatteryEvents
    Client->>API: HTTP request
    API-->>Client: JSON response
    Client-->>BodyBatteryData: parsed data
    BodyBatteryData-->>User: list[BodyBatteryData]

    User->>DailyBodyBatteryStress: get(day, client)
    DailyBodyBatteryStress->>Client: GET /wellness-api/rest/bodyBatteryStress
    Client->>API: HTTP request
    API-->>Client: JSON response
    Client-->>DailyBodyBatteryStress: parsed data
    DailyBodyBatteryStress-->>User: DailyBodyBatteryStress | None
Loading
sequenceDiagram
    participant User
    participant BodyBatteryData
    participant ThreadPool
    participant Client
    participant API

    User->>BodyBatteryData: list(end, days, client, max_workers)
    BodyBatteryData->>ThreadPool: submit get(date) for each day
    ThreadPool->>BodyBatteryData: parallel get(date)
    BodyBatteryData->>Client: GET /wellness-api/rest/bodyBatteryEvents
    Client->>API: HTTP request
    API-->>Client: JSON response
    Client-->>BodyBatteryData: parsed data
    BodyBatteryData-->>ThreadPool: BodyBatteryData
    ThreadPool-->>BodyBatteryData: aggregate results
    BodyBatteryData-->>User: list[BodyBatteryData]
Loading

📜 Recent review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between e60be53 and f42f0b0.

📒 Files selected for processing (3)
  • README.md (1 hunks)
  • src/garth/data/body_battery/daily_stress.py (1 hunks)
  • tests/data/test_body_battery_data.py (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • README.md
🧰 Additional context used
📓 Path-based instructions (1)
`tests/**`: - test functions shouldn't have a return type hint - it's ok to use `assert` instead of `pytest.assume()`

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

  • it's ok to use assert instead of pytest.assume()
  • tests/data/test_body_battery_data.py
🪛 Pylint (3.3.7)
src/garth/data/body_battery/daily_stress.py

[convention] 1-1: Missing module docstring

(C0114)


[error] 5-5: Unable to import 'pydantic.dataclasses'

(E0401)


[refactor] 20-20: Too many instance attributes (12/7)

(R0902)

tests/data/test_body_battery_data.py

[convention] 1-1: Missing module docstring

(C0114)


[error] 6-6: Unable to import 'garth'

(E0401)


[error] 7-7: Unable to import 'garth.http'

(E0401)


[convention] 11-11: Missing function or method docstring

(C0116)


[convention] 45-45: Missing function or method docstring

(C0116)


[convention] 56-56: Missing function or method docstring

(C0116)


[convention] 110-110: Missing function or method docstring

(C0116)


[convention] 118-118: Missing function or method docstring

(C0116)


[convention] 138-138: Missing function or method docstring

(C0116)


[convention] 304-304: Missing class docstring

(C0115)

🔇 Additional comments (6)
src/garth/data/body_battery/daily_stress.py (3)

19-35: Well-designed comprehensive dataclass.

The dataclass appropriately captures all the necessary fields for daily Body Battery and stress data. While static analysis flags the high number of attributes (12), this is justified for a comprehensive data model that needs to represent the full API response structure.

🧰 Tools
🪛 Pylint (3.3.7)

[refactor] 20-20: Too many instance attributes (12/7)

(R0902)


36-71: Excellent use of computed properties.

The implementation correctly uses @cached_property for expensive parsing operations (body_battery_readings) and regular @property for lightweight computations. The computed properties for current, max, min, and change values provide a clean API for accessing common metrics.


72-91: Robust API integration with proper error handling.

The get class method properly handles the API response by:

  • Using appropriate defaults for client and date formatting
  • Checking response type before processing
  • Converting camelCase to snake_case for consistent Python naming
  • Gracefully returning None for invalid responses
tests/data/test_body_battery_data.py (3)

10-42: Comprehensive integration test with proper structure validation.

The test thoroughly validates the BodyBatteryData.get() method by checking:

  • Return type and structure
  • Individual reading properties and attributes
  • Data type validation for computed properties

The conditional checks handle cases where data might be empty, making the test robust.

🧰 Tools
🪛 Pylint (3.3.7)

[convention] 11-11: Missing function or method docstring

(C0116)


55-107: Thorough testing of daily stress data with edge case handling.

This test excellently covers the DailyBodyBatteryStress functionality by:

  • Validating the complete data structure
  • Testing both stress and body battery readings
  • Checking computed properties with proper type validation
  • Handling edge cases where readings might be empty

The conditional logic ensures tests remain stable even with varying data availability.

🧰 Tools
🪛 Pylint (3.3.7)

[convention] 56-56: Missing function or method docstring

(C0116)


155-336: Exceptional error handling test coverage.

The extensive error handling tests demonstrate excellent defensive programming by covering:

  • API errors and invalid responses
  • Missing or malformed data scenarios
  • Invalid datetime formats and field types
  • Validation errors during object creation
  • Mixed valid/invalid data processing
  • Unexpected runtime errors

This comprehensive coverage ensures the implementation is robust against real-world API variations and failures.

🧰 Tools
🪛 Pylint (3.3.7)

[convention] 304-304: Missing class docstring

(C0115)

✨ Finishing Touches
  • 📝 Generate Docstrings

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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

🧹 Nitpick comments (5)
tests/data/test_body_battery_data.py (2)

86-88: Fix line length to comply with style guidelines.

These lines exceed the 79-character limit. Consider reformatting for better readability.

-            assert daily_data.current_body_battery is not None or daily_data.current_body_battery == 0
-            assert daily_data.max_body_battery is not None or daily_data.max_body_battery == 0
-            assert daily_data.min_body_battery is not None or daily_data.min_body_battery == 0
+            assert (daily_data.current_body_battery is not None or 
+                    daily_data.current_body_battery == 0)
+            assert (daily_data.max_body_battery is not None or 
+                    daily_data.max_body_battery == 0)
+            assert (daily_data.min_body_battery is not None or 
+                    daily_data.min_body_battery == 0)
🧰 Tools
🪛 Ruff (0.11.9)

86-86: Line too long (102 > 79)

(E501)


87-87: Line too long (94 > 79)

(E501)


88-88: Line too long (94 > 79)

(E501)


108-110: Minor formatting improvement needed.

Lines exceed the 79-character limit.

-    daily_data_list = DailyBodyBatteryStress.list(end, days, client=authed_client)
+    daily_data_list = DailyBodyBatteryStress.list(
+        end, days, client=authed_client
+    )
-    assert len(daily_data_list) <= days  # May be less if some days have no data
+    # May be less if some days have no data
+    assert len(daily_data_list) <= days
🧰 Tools
🪛 Ruff (0.11.9)

108-108: Line too long (82 > 79)

(E501)


110-110: Line too long (80 > 79)

(E501)

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

147-150: Minor line length adjustments needed.

-            stress_chart_value_offset=response.get("stressChartValueOffset", 0),
-            stress_chart_y_axis_origin=response.get("stressChartYAxisOrigin", 0),
+            stress_chart_value_offset=response.get(
+                "stressChartValueOffset", 0
+            ),
+            stress_chart_y_axis_origin=response.get(
+                "stressChartYAxisOrigin", 0
+            ),
-            body_battery_values_array=response.get("bodyBatteryValuesArray", [])
+            body_battery_values_array=response.get(
+                "bodyBatteryValuesArray", []
+            )
🧰 Tools
🪛 Ruff (0.11.9)

147-147: Line too long (80 > 79)

(E501)


148-148: Line too long (81 > 79)

(E501)


150-150: Line too long (80 > 79)

(E501)


162-162: Remove redundant timedelta import.

With the suggested import consolidation at the top of the file, this local import becomes unnecessary.

-        from datetime import timedelta

278-278: Remove redundant timedelta import.

With the suggested import consolidation at the top of the file, this local import becomes unnecessary.

-        from datetime import timedelta
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between d15e2af and 155823b.

⛔ Files ignored due to path filters (6)
  • tests/data/cassettes/test_body_battery_data_get.yaml is excluded by !tests/**/cassettes/**
  • tests/data/cassettes/test_body_battery_data_list.yaml is excluded by !tests/**/cassettes/**
  • tests/data/cassettes/test_body_battery_properties_edge_cases.yaml is excluded by !tests/**/cassettes/**
  • tests/data/cassettes/test_daily_body_battery_stress_get.yaml is excluded by !tests/**/cassettes/**
  • tests/data/cassettes/test_daily_body_battery_stress_get_no_data.yaml is excluded by !tests/**/cassettes/**
  • tests/data/cassettes/test_daily_body_battery_stress_list.yaml is excluded by !tests/**/cassettes/**
📒 Files selected for processing (4)
  • src/garth/__init__.py (2 hunks)
  • src/garth/data/__init__.py (1 hunks)
  • src/garth/data/body_battery.py (1 hunks)
  • tests/data/test_body_battery_data.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`tests/**`: - test functions shouldn't have a return type hint - it's ok to use `assert` instead of `pytest.assume()`

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

  • it's ok to use assert instead of pytest.assume()
  • tests/data/test_body_battery_data.py
🧬 Code Graph Analysis (1)
src/garth/data/__init__.py (1)
src/garth/data/body_battery.py (2)
  • BodyBatteryData (177-288)
  • DailyBodyBatteryStress (42-173)
🪛 Ruff (0.11.9)
tests/data/test_body_battery_data.py

86-86: Line too long (102 > 79)

(E501)


87-87: Line too long (94 > 79)

(E501)


88-88: Line too long (94 > 79)

(E501)


108-108: Line too long (82 > 79)

(E501)


110-110: Line too long (80 > 79)

(E501)

src/garth/data/body_battery.py

4-4: typing.ClassVar imported but unused

Remove unused import

(F401)


4-4: typing.Dict imported but unused

Remove unused import

(F401)


133-133: Line too long (104 > 79)

(E501)


134-134: Line too long (100 > 79)

(E501)


135-135: Line too long (108 > 79)

(E501)


136-136: Line too long (104 > 79)

(E501)


147-147: Line too long (80 > 79)

(E501)


148-148: Line too long (81 > 79)

(E501)


150-150: Line too long (80 > 79)

(E501)


248-248: Line too long (86 > 79)

(E501)


251-251: Line too long (89 > 79)

(E501)

🔇 Additional comments (6)
src/garth/data/__init__.py (1)

1-3: LGTM! Clean module exports.

The new Body Battery classes are properly added to the public API with maintained alphabetical ordering.

src/garth/__init__.py (1)

1-1: LGTM! Proper package-level exports.

The new Body Battery classes are correctly exposed in the package's public API with consistent alphabetical ordering.

Also applies to: 18-20

tests/data/test_body_battery_data.py (4)

9-35: Well-structured test with proper edge case handling.

The test correctly verifies the data structure and properties, with appropriate handling for both populated and empty data scenarios.


37-46: Good handling of potentially empty event data.

The test appropriately handles the scenario where no Body Battery events exist for the requested period.


96-102: Good edge case coverage for missing data.

The test appropriately handles the scenario where no data exists for the requested date.


119-134: Excellent edge case testing.

The test thoroughly validates the behavior of properties when data arrays are empty, ensuring robust handling of edge cases.

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: 0

♻️ Duplicate comments (1)
src/garth/data/body_battery.py (1)

155-158: Minor formatting: Address line length violations.

The static analysis correctly identifies lines that slightly exceed the 79-character limit.

-            stress_chart_value_offset=response.get("stressChartValueOffset", 0),
-            stress_chart_y_axis_origin=response.get("stressChartYAxisOrigin", 0),
+            stress_chart_value_offset=response.get(
+                "stressChartValueOffset", 0
+            ),
+            stress_chart_y_axis_origin=response.get(
+                "stressChartYAxisOrigin", 0
+            ),
-            body_battery_values_array=response.get("bodyBatteryValuesArray", [])
+            body_battery_values_array=response.get(
+                "bodyBatteryValuesArray", []
+            )
🧰 Tools
🪛 Ruff (0.11.9)

155-155: Line too long (80 > 79)

(E501)


156-156: Line too long (81 > 79)

(E501)


158-158: Line too long (80 > 79)

(E501)

🧹 Nitpick comments (1)
src/garth/data/body_battery.py (1)

57-114: Consider caching property calculations for better performance.

The property implementations are well-structured and handle edge cases properly. However, properties like body_battery_readings are recalculated on each access, and metric properties call this multiple times, which could be inefficient for large datasets.

Consider using functools.cached_property or similar caching mechanism:

+from functools import cached_property

-    @property
+    @cached_property
     def body_battery_readings(self) -> List[BodyBatteryReading]:
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 155823b and ab67ab3.

📒 Files selected for processing (1)
  • src/garth/data/body_battery.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.11.9)
src/garth/data/body_battery.py

155-155: Line too long (80 > 79)

(E501)


156-156: Line too long (81 > 79)

(E501)


158-158: Line too long (80 > 79)

(E501)

🔇 Additional comments (4)
src/garth/data/body_battery.py (4)

1-11: LGTM! Clean import structure.

The import statements are well-organized and all appear to be used throughout the file. The previous issues with unused imports have been addressed.


13-39: LGTM! Well-structured dataclasses.

The dataclass definitions provide clear, typed structures for representing Body Battery and stress data. The field types are appropriate and the documentation is clear.


161-179: LGTM! Consistent list method implementations.

Both list methods follow appropriate patterns for their respective data types:

  • DailyBodyBatteryStress.list appends single data objects (one per day)
  • BodyBatteryData.list extends with event lists (multiple events per day)

The date iteration logic and error handling are implemented correctly.

Also applies to: 278-295


182-276: LGTM! Well-implemented legacy events support.

The BodyBatteryData class appropriately handles the legacy event-based API with proper error handling for nested event data and optional fields. The same performance optimization suggestion for caching properties applies here as well.

The parsing logic correctly handles the more complex structure of event data compared to the daily summary data.

@matin
Copy link
Copy Markdown
Owner

matin commented May 27, 2025

@bes-dev looks good at first glace, but I'll review in detail

could you run make format to get lint to pass?

in general, it'll be easier if you enable pre commit hooks

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.

great functionality to add. thanks for taking the time to work on this.

the main thing I'd change is taking advantage of camel_to_snake_dict() and the fact that Pydantic will already manage coerce types like datetimes and handle defaults. that gives you the ability to do cls(**response) in many cases, especially because pydantic will apply the logic to custom types for individual fields as ell. those changes would simply your code

please also break up each resource into a different file for clarity


readings = []
for values in self.body_battery_values_array:
if len(values) >= 4:
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

what's the significance of a length of 4 or more? a comment would be helpful

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

Comment on lines +133 to +145
# Parse timestamps
start_gmt = datetime.fromisoformat(
response.get("startTimestampGMT", "").replace("Z", "+00:00")
)
end_gmt = datetime.fromisoformat(
response.get("endTimestampGMT", "").replace("Z", "+00:00")
)
start_local = datetime.fromisoformat(
response.get("startTimestampLocal", "").replace("Z", "+00:00")
)
end_local = datetime.fromisoformat(
response.get("endTimestampLocal", "").replace("Z", "+00:00")
)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

this isn't necessary. pydantic will automatically do this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

Comment on lines +148 to +168
user_profile_pk=response.get("userProfilePK", 0),
calendar_date=response.get("calendarDate", ""),
start_timestamp_gmt=start_gmt,
end_timestamp_gmt=end_gmt,
start_timestamp_local=start_local,
end_timestamp_local=end_local,
max_stress_level=response.get("maxStressLevel", 0),
avg_stress_level=response.get("avgStressLevel", 0),
stress_chart_value_offset=response.get(
"stressChartValueOffset", 0
),
stress_chart_y_axis_origin=response.get(
"stressChartYAxisOrigin", 0
),
stress_values_array=response.get(
"stressValuesArray", []
),
body_battery_values_array=response.get(
"bodyBatteryValuesArray", []
)
)
Copy link
Copy Markdown
Owner

@matin matin May 27, 2025

Choose a reason for hiding this comment

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

why not use ..utils.camel_to_snake_dict() and field defaults? that'd let you do cls(**response)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done



@dataclass
class BodyBatteryData:
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

let's break up the two resources into difference files

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

Comment on lines +171 to +189
def list(
cls,
end: date | str | None = None,
period: int = 1,
*,
client: http.Client | None = None,
) -> List[Self]:
"""Get Body Battery and stress data for multiple days."""
end_date = format_end_date(end)
all_data = []

for i in range(period):
date_to_fetch = end_date - timedelta(days=i)
data = cls.get(date_to_fetch, client=client)
if data:
all_data.append(data)

return all_data

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

why not use Data.list(). it functionally does the same thing but with threads

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done



@dataclass
class DailyBodyBatteryStress:
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

why not inherit from Data? that'd give you .list() and consistency

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

activity_type: str | None = None
activity_id: str | None = None
average_stress: float | None = None
stress_values_array: List[List[int]] | None = None
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

please use list[] instead of List[] for consistency

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

Comment on lines +205 to +206
if not self.body_battery_values_array:
return []
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

is this necessary? if self.body_batter_values_array is empty, the for loop would just not run

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

removed. done.

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: 1

🧹 Nitpick comments (4)
pyproject.toml (1)

78-80: Tighten version constraints for ruff
To prevent untested major releases from breaking your CI, add an upper bound to the version specifier. For example:

[dependency-groups]
-dev = [
-    "ruff>=0.11.5",
-]
+dev = [
+    "ruff>=0.11.5,<0.12.0",
+]
src/garth/data/body_battery_events.py (1)

39-54: Improve data validation and consider performance optimization.

The property logic is correct but could be more robust:

  1. Data validation: The code assumes array elements have correct types but doesn't validate them
  2. Performance: The property recreates the list on every access, which could be inefficient for large datasets

Consider these improvements:

-    @property
+    @cached_property
     def body_battery_readings(self) -> List[BodyBatteryReading]:
         """Convert body battery values array to structured readings."""
         readings = []
         for values in self.body_battery_values_array or []:
             # Each reading requires 4 values: timestamp, status, level, version
-            if len(values) >= 4:
+            if len(values) >= 4:
+                try:
                     readings.append(
                         BodyBatteryReading(
                             timestamp=values[0],
                             status=values[1],
                             level=values[2],
                             version=values[3],
                         )
                     )
+                except (TypeError, ValueError):
+                    # Skip invalid readings
+                    continue
         return readings

Don't forget to import cached_property from functools.

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

50-65: Good use of cached_property for performance optimization.

The cached property implementation is excellent for performance. However, consider adding type validation similar to the suggestion for the first file.

Consider adding error handling for invalid data:

     @cached_property
     def body_battery_readings(self) -> List[BodyBatteryReading]:
         """Convert body battery values array to structured readings."""
         readings = []
         for values in self.body_battery_values_array or []:
             # Each reading requires 4 values: timestamp, status, level, version
             if len(values) >= 4:
+                try:
                     readings.append(
                         BodyBatteryReading(
                             timestamp=values[0],
                             status=values[1],
                             level=values[2],
                             version=values[3],
                         )
                     )
+                except (TypeError, ValueError):
+                    continue
         return readings

67-76: Consider using cached_property for consistency and performance.

The implementation is correct, but for consistency with body_battery_readings and better performance, consider using @cached_property.

-    @property
+    @cached_property
     def stress_readings(self) -> List[StressReading]:
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 51e67a2 and 7f2f813.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • pyproject.toml (1 hunks)
  • src/garth/data/body_battery.py (1 hunks)
  • src/garth/data/body_battery_events.py (1 hunks)
  • src/garth/data/daily_body_battery_stress.py (1 hunks)
  • tests/data/test_body_battery_data.py (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/garth/data/body_battery.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/data/test_body_battery_data.py
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/garth/data/daily_body_battery_stress.py (3)
src/garth/utils.py (2)
  • camel_to_snake_dict (17-33)
  • format_end_date (36-41)
src/garth/data/_base.py (1)
  • Data (14-42)
src/garth/http.py (2)
  • Client (19-239)
  • connectapi (181-187)
🔇 Additional comments (10)
pyproject.toml (1)

77-81:

Details

❓ Verification inconclusive

Non-standard dependency section: verify build-system support
The new [dependency-groups] block is not part of PEP 621 and may be ignored by Hatchling (the configured build-backend). Consider moving these dev tools under the existing [project.optional-dependencies] (e.g., into the dev or linting groups), or confirm that your tooling supports [dependency-groups].


Non-standard [dependency-groups] section – verify build-system support
The [dependency-groups] table isn’t defined by PEP 621 and may be ignored by Hatchling (your configured build-backend). To ensure your dev tools are installed correctly:
• Move ruff>=0.11.5 into a PEP 621-compliant group under [project.optional-dependencies], for example:

[project.optional-dependencies]
dev = ["ruff>=0.11.5"]

• Or confirm that your Hatch/Hatchling version explicitly supports the non-standard [dependency-groups] table.

File: pyproject.toml lines 77–81

src/garth/data/body_battery_events.py (5)

1-12: LGTM! Import structure is well-organized.

The imports are appropriate and follow good practices. The cross-module dependency on BodyBatteryReading is acceptable since it's only importing a dataclass.


14-25: LGTM! Well-structured dataclass for event data.

The BodyBatteryEvent dataclass is well-defined with appropriate field types and clear naming.


27-38: LGTM! Flexible dataclass structure for aggregated data.

The BodyBatteryData dataclass appropriately handles optional fields and different data types. The structure supports both event-based and array-based data.


56-72: LGTM! Well-implemented statistical properties.

The properties correctly extract Body Battery statistics. The implementation properly handles empty data cases.

Note: The current_level property assumes readings are chronologically ordered. Ensure the API returns data in the correct order.


129-146: LGTM! Clean implementation of multi-day data retrieval.

The list method correctly implements the pattern for fetching multiple days of data. The logic is clear and follows good practices.

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

1-13: LGTM! Well-organized imports with proper base class inheritance.

The import structure is clean and follows good practices. The inheritance from Data base class ensures API consistency.


15-49: LGTM! Well-designed dataclasses with comprehensive field coverage.

The three dataclasses are appropriately structured:

  • BodyBatteryReading and StressReading provide clean abstractions for individual readings
  • DailyBodyBatteryStress inherits from Data and includes comprehensive daily metrics

The field types and naming are consistent and appropriate.


78-102: Excellent implementation of statistical properties with proper error handling.

The Body Battery properties are well-designed:

  • Proper handling of empty data cases
  • The body_battery_change method correctly validates minimum data requirements
  • Good use of the cached readings property for performance

104-122: LGTM! Clean and robust API implementation following base class patterns.

The get method is well-implemented:

  • Properly inherits from Data base class and follows the established pattern
  • Good use of camel_to_snake_dict for response transformation
  • Appropriate error handling with type checking
  • Clean and maintainable code structure

This is a much better implementation compared to the more complex parsing in the first file.

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: 0

🧹 Nitpick comments (3)
src/garth/data/body_battery/events.py (3)

1-1: Remove unused import.

The timedelta import is not used anywhere in the file.

-from datetime import date, datetime, timedelta
+from datetime import date, datetime
🧰 Tools
🪛 Ruff (0.11.9)

1-1: datetime.timedelta imported but unused

Remove unused import: datetime.timedelta

(F401)


88-92: Simplify datetime parsing logic.

The datetime parsing logic can be simplified by using a more direct approach.

-                    event_start_time_gmt=datetime.fromisoformat(
-                        event_data.get("eventStartTimeGmt", "").replace(
-                            "Z", "+00:00"
-                        )
-                    ),
+                    event_start_time_gmt=datetime.fromisoformat(
+                        event_data.get("eventStartTimeGmt", "").replace("Z", "+00:00")
+                    ),

Alternatively, consider using a utility function if this datetime parsing pattern is used elsewhere in the codebase.


81-115: Consider extracting event parsing logic.

The manual parsing of event data with multiple .get() calls could be extracted into a separate method or use a more structured approach for better maintainability.

Consider creating a helper method:

def _parse_event_data(self, event_data: dict) -> BodyBatteryEvent:
    """Parse event data dictionary into BodyBatteryEvent."""
    return BodyBatteryEvent(
        event_type=event_data.get("eventType", ""),
        event_start_time_gmt=datetime.fromisoformat(
            event_data.get("eventStartTimeGmt", "").replace("Z", "+00:00")
        ),
        timezone_offset=event_data.get("timezoneOffset", 0),
        duration_in_milliseconds=event_data.get("durationInMilliseconds", 0),
        body_battery_impact=event_data.get("bodyBatteryImpact", 0),
        feedback_type=event_data.get("feedbackType", ""),
        short_feedback=event_data.get("shortFeedback", ""),
    )

This would improve readability and make the parsing logic reusable.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 7f2f813 and 18133b5.

📒 Files selected for processing (5)
  • src/garth/data/__init__.py (1 hunks)
  • src/garth/data/body_battery/__init__.py (1 hunks)
  • src/garth/data/body_battery/daily_stress.py (1 hunks)
  • src/garth/data/body_battery/events.py (1 hunks)
  • src/garth/data/body_battery/readings.py (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/garth/data/body_battery/init.py
🧰 Additional context used
🪛 Ruff (0.11.9)
src/garth/data/body_battery/events.py

1-1: datetime.timedelta imported but unused

Remove unused import: datetime.timedelta

(F401)

🔇 Additional comments (5)
src/garth/data/body_battery/readings.py (1)

1-53: Well-designed foundational module.

The data structures and parsing functions are well-implemented with appropriate type hints, graceful error handling, and clear separation of concerns. The validation of array lengths before unpacking prevents potential index errors, and the handling of None inputs is robust.

src/garth/data/__init__.py (1)

1-20: Proper API expansion.

The public API updates correctly expose the new Body Battery functionality while maintaining backward compatibility. The exports are properly organized and all new classes are appropriately included.

src/garth/data/body_battery/daily_stress.py (2)

36-70: Excellent use of computed properties with robust error handling.

The implementation demonstrates good design patterns:

  • Appropriate use of @cached_property for expensive parsing operations
  • Robust error handling when readings are empty (returning None instead of raising exceptions)
  • Clear separation between data storage and computed metrics
  • The body_battery_change calculation properly handles edge cases by checking for sufficient readings

72-90: Well-implemented API integration.

The get method demonstrates solid API design:

  • Proper client handling with sensible defaults
  • Appropriate response validation before processing
  • Clean use of utility functions for date formatting and data transformation
  • Graceful error handling for malformed responses
src/garth/data/body_battery/events.py (1)

118-142: Excellent concurrent implementation.

The list method demonstrates solid concurrent programming practices:

  • Proper use of ThreadPoolExecutor for I/O-bound operations
  • Configurable max_workers parameter with sensible default
  • Clean aggregation of results from multiple concurrent requests
  • Proper resource management with context manager

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

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between e67d257 and 6e18d6d.

📒 Files selected for processing (1)
  • src/garth/data/body_battery/events.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.11.9)
src/garth/data/body_battery/events.py

1-1: datetime.timedelta imported but unused

Remove unused import: datetime.timedelta

(F401)

🔇 Additional comments (2)
src/garth/data/body_battery/events.py (2)

16-28: LGTM!

The BodyBatteryEvent dataclass is well-structured with appropriate field types and clear naming.


125-150: LGTM!

The concurrent implementation using ThreadPoolExecutor is well-designed for fetching multiple days of data efficiently. The configurable max_workers parameter provides flexibility for different use cases.

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: 0

🧹 Nitpick comments (1)
src/garth/data/body_battery/events.py (1)

64-181: Excellent validation improvements, but needs formatting.

The comprehensive validation and error handling addresses the concerns from previous reviews perfectly. The implementation handles missing data, invalid types, and malformed datetime strings robustly.

However, there are multiple line length violations flagged by static analysis.

Apply formatting to fix line length violations:

-                        logging.error(f"Missing eventStartTimeGmt in event data: {event_data}")
+                        logging.error(
+                            f"Missing eventStartTimeGmt in event data: {event_data}"
+                        )
-                        raise ValueError("eventStartTimeGmt is required but missing")
+                        raise ValueError(
+                            "eventStartTimeGmt is required but missing"
+                        )

Consider running make format as suggested in the PR comments to automatically fix all formatting issues.

🧰 Tools
🪛 Ruff (0.11.9)

99-99: Line too long (95 > 79)

(E501)


100-100: Line too long (85 > 79)

(E501)


107-107: Line too long (95 > 79)

(E501)


108-108: Line too long (108 > 79)

(E501)


113-113: Line too long (106 > 79)

(E501)


118-118: Line too long (109 > 79)

(E501)


123-123: Line too long (107 > 79)

(E501)


138-138: Line too long (85 > 79)

(E501)


139-139: Line too long (105 > 79)

(E501)


143-143: Line too long (87 > 79)

(E501)


144-144: Line too long (111 > 79)

(E501)


149-149: Line too long (87 > 79)

(E501)


150-150: Line too long (98 > 79)

(E501)


167-167: Line too long (95 > 79)

(E501)


171-171: Line too long (109 > 79)

(E501)


179-179: Line too long (136 > 79)

(E501)

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 6e18d6d and 16dd024.

📒 Files selected for processing (2)
  • src/garth/data/body_battery/events.py (1 hunks)
  • src/garth/data/body_battery/readings.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/garth/data/body_battery/readings.py
🧰 Additional context used
🪛 Ruff (0.11.9)
src/garth/data/body_battery/events.py

99-99: Line too long (95 > 79)

(E501)


100-100: Line too long (85 > 79)

(E501)


107-107: Line too long (95 > 79)

(E501)


108-108: Line too long (108 > 79)

(E501)


113-113: Line too long (106 > 79)

(E501)


118-118: Line too long (109 > 79)

(E501)


123-123: Line too long (107 > 79)

(E501)


138-138: Line too long (85 > 79)

(E501)


139-139: Line too long (105 > 79)

(E501)


143-143: Line too long (87 > 79)

(E501)


144-144: Line too long (111 > 79)

(E501)


149-149: Line too long (87 > 79)

(E501)


150-150: Line too long (98 > 79)

(E501)


167-167: Line too long (95 > 79)

(E501)


171-171: Line too long (109 > 79)

(E501)


179-179: Line too long (136 > 79)

(E501)

🔇 Additional comments (4)
src/garth/data/body_battery/events.py (4)

1-13: LGTM! Clean imports and reasonable constants.

The imports are appropriate and the unused timedelta import from the past review has been correctly removed. The MAX_WORKERS constant provides a good default for concurrent operations.


16-27: Clean and well-typed dataclass definition.

The BodyBatteryEvent dataclass is well-structured with appropriate type hints and clear field names.


29-62: Well-designed dataclass with useful property methods.

The BodyBatteryData class provides a clean interface with optional fields and convenient properties for accessing current, max, and min battery levels. The property implementations are efficient and handle empty data gracefully.


183-207: Clean concurrent implementation with proper resource management.

The list() method efficiently fetches data for multiple dates using ThreadPoolExecutor. The implementation properly manages resources with context manager usage and provides configurable concurrency control.

@bes-dev
Copy link
Copy Markdown
Contributor Author

bes-dev commented May 28, 2025

@matin PR is ready for review

Architectural Refactoring: Body Battery Module Split

Summary

Implemented the requested separation of Body Battery resources into dedicated files, following project patterns and addressing technical debt.

Changes Made

File Structure:
data/body_battery/
├── readings.py # Shared data classes + parsing utilities
├── events.py # Legacy BodyBatteryData + BodyBatteryEvent
├── daily_stress.py # Modern DailyBodyBatteryStress
└── init.py # Clean re-exports

Technical Improvements:

  • Separated resources as requested - events API vs daily stress API
  • Eliminated circular imports by extracting shared classes to readings.py
  • Implemented DRY principle with reusable parse_* functions
  • Added threading to BodyBatteryData.list() for consistency
  • Applied modern typing - list[] instead of List[] throughout
  • Simplified constructors - using camel_to_snake_dict() + cls(**response) pattern

@matin
Copy link
Copy Markdown
Owner

matin commented May 28, 2025

@bes-dev could you take a look at the failing tasks (lint and tests)?

@bes-dev
Copy link
Copy Markdown
Contributor Author

bes-dev commented May 28, 2025

@matin problems was fixed, could you approve workflow to check it?

@codecov
Copy link
Copy Markdown

codecov bot commented May 28, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 100.00%. Comparing base (d15e2af) to head (f42f0b0).
Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##              main      #116    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files           36        41     +5     
  Lines         1429      1771   +342     
==========================================
+ Hits          1429      1771   +342     
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.

@bes-dev
Copy link
Copy Markdown
Contributor Author

bes-dev commented May 28, 2025

@matin please, restart workflow

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: 4

🧹 Nitpick comments (1)
tests/data/test_body_battery_data.py (1)

46-46: Remove meaningless assertion.

The assertion assert len(body_battery_data) >= 0 is always True for any list, making it meaningless as a test condition.

Apply this diff to remove the meaningless assertion:

-    # Test that we get data (may be empty if no events)
-    assert len(body_battery_data) >= 0

Or replace with a more meaningful assertion if you want to verify the structure:

-    # Test that we get data (may be empty if no events)
-    assert len(body_battery_data) >= 0
+    # Verify we get a list (may be empty if no events)
+    # Length check is implicit in isinstance(body_battery_data, list)
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between a431422 and 38d14a2.

⛔ Files ignored due to path filters (6)
  • tests/data/cassettes/test_body_battery_data_get.yaml is excluded by !tests/**/cassettes/**
  • tests/data/cassettes/test_body_battery_data_list.yaml is excluded by !tests/**/cassettes/**
  • tests/data/cassettes/test_body_battery_properties_edge_cases.yaml is excluded by !tests/**/cassettes/**
  • tests/data/cassettes/test_daily_body_battery_stress_get.yaml is excluded by !tests/**/cassettes/**
  • tests/data/cassettes/test_daily_body_battery_stress_get_no_data.yaml is excluded by !tests/**/cassettes/**
  • tests/data/cassettes/test_daily_body_battery_stress_list.yaml is excluded by !tests/**/cassettes/**
📒 Files selected for processing (1)
  • tests/data/test_body_battery_data.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`tests/**`: - test functions shouldn't have a return type hint - it's ok to use `assert` instead of `pytest.assume()`

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

  • it's ok to use assert instead of pytest.assume()
  • tests/data/test_body_battery_data.py
🧬 Code Graph Analysis (1)
tests/data/test_body_battery_data.py (4)
src/garth/data/body_battery/events.py (1)
  • BodyBatteryData (31-253)
src/garth/data/body_battery/daily_stress.py (1)
  • DailyBodyBatteryStress (20-90)
src/garth/http.py (2)
  • Client (19-239)
  • connectapi (181-187)
tests/conftest.py (3)
  • vcr (79-82)
  • authed_client (65-75)
  • client (21-22)
🔇 Additional comments (3)
tests/data/test_body_battery_data.py (3)

118-121: Good practice using max_workers=1 for VCR compatibility.

This is a thoughtful implementation detail that prevents VCR issues with concurrent requests while still testing the list functionality.


153-287: Comprehensive error handling test coverage.

The error handling tests thoroughly cover various failure scenarios including API errors, invalid responses, missing data, malformed datetime formats, type validation errors, and mixed valid/invalid data. This demonstrates good defensive programming practices and ensures the library handles edge cases gracefully.


1-287: Overall test structure follows best practices.

The test module demonstrates good practices:

  • Proper use of VCR for API testing
  • Comprehensive error handling with mocks
  • Following coding guidelines (no return type hints for test functions)
  • Good separation between functional tests and error handling tests
  • Appropriate use of pytest fixtures

@bes-dev
Copy link
Copy Markdown
Contributor Author

bes-dev commented May 28, 2025

@matin Could you restart the workflow? Sorry for the many commits, but local testing did not show the problems that your CI highlighted 😞

@bes-dev bes-dev requested a review from matin May 28, 2025 18:52
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: 1

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 2cc3f5d and adc7ac0.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • Makefile (1 hunks)
  • pyproject.toml (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • pyproject.toml
🔇 Additional comments (1)
Makefile (1)

16-16: Verify two-step installation replicates dev, linting, and testing extras
Splitting uv pip install -e . and then compiling/installing grouped requirements is intentional, but please confirm this covers all previously installed extras and doesn't omit any transitive dependencies.

Test the install target both locally and in CI to ensure no missing packages.

Makefile Outdated
install: .uv .pre-commit
uv pip install -e ".[dev,linting,testing]"
uv pip install -e .
uv pip install -r <(uv pip compile --group dev --group linting --group testing)
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

⚠️ Potential issue

Address shell compatibility with process substitution
Using <(...) for process substitution requires Bash and will break if Make’s default shell is /bin/sh (e.g., Dash). To avoid portability issues, either force Bash or switch to a POSIX-compatible pipeline.

Example fixes:

+SHELL := /bin/bash

 install: .uv .pre-commit
-	uv pip install -r <(uv pip compile --group dev --group linting --group testing)
+	uv pip compile --group dev --group linting --group testing | uv pip install -r /dev/stdin
🤖 Prompt for AI Agents
In the Makefile at line 17, the use of process substitution `<(...)` is not
portable because it requires Bash and will fail if the default shell is /bin/sh.
To fix this, either explicitly set the shell to Bash at the top of the Makefile
or rewrite the command to use a POSIX-compatible pipeline without process
substitution, such as using a temporary file or a pipe with standard input.

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: 1

♻️ Duplicate comments (1)
.github/workflows/ci.yml (1)

73-74: Same check for the testing group
Ensure that pip ≥ 23.2 on each runner OS supports --group testing and that the testing group is correctly declared in pyproject.toml.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 910be3f and 7cf71c4.

📒 Files selected for processing (2)
  • .github/workflows/ci.yml (2 hunks)
  • pyproject.toml (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • pyproject.toml

Comment on lines +39 to +40
uv pip install --system -e .
uv pip install --system --group linting
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.

💡 Verification agent

🧩 Analysis chain

Verify PEP 621 dependency-group installation
Splitting the editable install and the optional group install aligns with the new [dependency-groups] layout in pyproject.toml. However, the --group flag requires pip ≥ 23.2 and the group names (linting) must match those defined in your pyproject.toml.

You can run this script in CI to confirm both pip support and group definitions:


🏁 Script executed:

#!/bin/bash
# 1. Check pip version
uv pip --version

# 2. Show the dependency-groups section (and verify 'linting' is present)
rg '\[dependency-groups\]' -A 5 pyproject.toml

Length of output: 246


🏁 Script executed:

#!/bin/bash
# Verify that 'linting' group is defined in pyproject.toml
rg "linting" -n pyproject.toml

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Verify that pip ≥ 23.2 is available for the `--group` flag
pip --version || python -m pip --version

Length of output: 80


CI requires pip ≥ 23.2 for --group installs
Your CI runner is using pip 23.0.1, which doesn’t support the --group flag. To fix, upgrade pip before your install steps and use a reliable invocation:

• In .github/workflows/ci.yml, before line 39, add:

- name: Upgrade pip
  run: python -m pip install --upgrade "pip>=23.2"

• Replace uv pip calls with python -m pip for consistency:

 .github/workflows/ci.yml
 steps:
+  - name: Upgrade pip
+    run: python -m pip install --upgrade "pip>=23.2"
 - name: Install package
-    run: uv pip install --system -e .
+    run: python -m pip install --system -e .
 - name: Install linting deps
-    run: uv pip install --system --group linting
+    run: python -m pip install --system --group linting
🤖 Prompt for AI Agents
In .github/workflows/ci.yml around lines 39 to 40, the CI uses pip 23.0.1 which
does not support the `--group` flag needed for the linting dependency group
install. To fix this, add a step before line 39 to upgrade pip to version 23.2
or higher using `python -m pip install --upgrade "pip>=23.2"`. Also, replace the
`uv pip` commands with `python -m pip` to ensure consistent and reliable pip
invocation.

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: 0

🧹 Nitpick comments (1)
src/garth/data/_base.py (1)

20-20: Interface change introduces flexibility but breaks consistency.

Expanding the return type to Self | list[Self] | None allows implementations to return either single instances or lists, which is necessary for Body Battery data that has multiple events per day. However, this introduces inconsistency in the interface where different implementations return different types.

Consider documenting this behavior change clearly, as it's a breaking change to the abstract interface that subclasses must handle appropriately.

Consider adding a docstring to clarify when implementations should return single instances vs. lists:

 @classmethod
 @abstractmethod
 def get(
     cls, day: date | str, *, client: http.Client | None = None
-) -> Self | list[Self] | None: ...
+) -> Self | list[Self] | None: 
+    """Get data for a specific date.
+    
+    Returns:
+        Single instance for data types with one record per day,
+        list of instances for data types with multiple records per day,
+        or None if no data exists.
+    """
+    ...
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 7cf71c4 and e60be53.

📒 Files selected for processing (3)
  • src/garth/data/_base.py (3 hunks)
  • src/garth/data/body_battery/events.py (1 hunks)
  • src/garth/version.py (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/garth/version.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/garth/data/body_battery/events.py
🔇 Additional comments (2)
src/garth/data/_base.py (2)

4-4: LGTM: Import addition supports the flattening logic.

The addition of chain from itertools is appropriate and necessary for the list flattening implementation in the list method.


43-47: Excellent implementation of list flattening logic.

The flattening logic correctly handles both single instances and lists returned by get methods:

  • Uses isinstance(day, list) to differentiate between single instances and lists
  • Applies chain.from_iterable to efficiently flatten nested structures
  • Maintains type safety and handles edge cases properly

The walrus operator check in line 35 (if day := cls.get(date_, client=client):) works correctly with this change since both single instances and non-empty lists are truthy, while None and empty lists are falsy.

@matin
Copy link
Copy Markdown
Owner

matin commented May 29, 2025

@bes-dev looks good! thanks for the work on this. I'll merge and release tomorrow

@matin
Copy link
Copy Markdown
Owner

matin commented Jun 1, 2025

@coderabbitai issue a PR updating the README to reflect the changes made in this branch

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Jun 1, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

I'll help you create a PR to update the README with the new Body Battery functionality. Let me first examine the current README structure to understand how to properly document these changes.


🏁 Script executed:

#!/bin/bash
# Examine the current README structure
cat README.md

Length of output: 23748


Now let me examine the Body Battery data structures to understand what should be documented:

Now I'll create a comprehensive plan to update the README with Body Battery documentation:

flowchart TB
    A["Task: Update README with Body Battery documentation"]
    A --o B["Add Body Battery section to Stats resources"]
    B --o C["Document BodyBatteryData usage"]
    B --o D["Document DailyBodyBatteryStress usage"]
    B --o E["Add example data structures"]
    C --o F["Show .get() method example"]
    C --o G["Show .list() method example"]
    C --o H["Show returned data structure"]
    D --o I["Show .get() method example"]
    D --o J["Show .list() method example"]
    D --o K["Show returned data structure"]
Loading

@matin I'll create a pull request to update the README with comprehensive Body Battery documentation. The plan includes adding a new Body Battery section to the Stats resources area, documenting both BodyBatteryData (legacy events API) and DailyBodyBatteryStress (modern daily API) with examples showing their usage patterns, data structures, and computed properties like current/max/min Body Battery levels.


🏁 Script executed:

#!/bin/bash
# Examine the Body Battery dataclass structures
echo "=== BodyBatteryData and BodyBatteryEvent structures ==="
cat src/garth/data/body_battery/events.py

echo -e "\n=== DailyBodyBatteryStress structure ==="
cat src/garth/data/body_battery/daily_stress.py

echo -e "\n=== Reading structures ==="
cat src/garth/data/body_battery/readings.py

Length of output: 13692


Now I have all the information needed to create a comprehensive README update. Let me plan the changes:

flowchart TB
    A["Task: Update README with Body Battery documentation"]
    A --o B["Add Body Battery section to Stats resources"]
    B --o C["Document BodyBatteryData (legacy events API)"]
    B --o D["Document DailyBodyBatteryStress (modern daily API)"]
    B --o E["Show computed properties examples"]
    C --o F["Show .get() method returning list"]
    C --o G["Show .list() method for multiple days"]
    C --o H["Show BodyBatteryEvent structure"]
    D --o I["Show .get() method returning single object"]
    D --o J["Show .list() method for multiple days"]
    D --o K["Show properties: current, max, min, change"]
Loading

@matin I've planned the README update to document the new Body Battery functionality. The documentation will be added to the Stats resources section and include:

Body Battery Events (Legacy API)

  • Shows BodyBatteryData.get() returning event-based data with sleep impacts
  • Demonstrates accessing Body Battery levels from events using computed properties
  • Includes BodyBatteryData.list() for multiple days

Daily Body Battery & Stress (Modern API)

  • Shows DailyBodyBatteryStress.get() returning comprehensive daily data
  • Demonstrates computed properties: current_body_battery, max_body_battery, min_body_battery, and body_battery_change
  • Includes DailyBodyBatteryStress.list() for multiple days

The documentation follows the same pattern as existing HRV and Sleep sections, with realistic example data structures based on the actual dataclass definitions. The execution agent will handle creating the actual pull request with these changes.


⏳ Preparing changes...

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Jun 1, 2025

A summary of the edits CodeRabbit can apply:

Insert comprehensive Body Battery documentation in the Stats resources section of README.md.

A plan of each step that CodeRabbit will take in order to execute the edits for each file (with inline rough draft snippets):

➕ README.md

Confirm that the Sleep section concludes with the code block for garth.SleepData.list on line 656. The insertion point will be immediately after, at line 657.


Insert the following content at line 657, preserving blank lines and code fences exactly as shown:

### Body Battery

Body Battery event data (legacy API for sleep-related Body Battery impacts)

```python
garth.BodyBatteryData.get("2023-07-20")
[
    BodyBatteryData(
        event=BodyBatteryEvent(
            event_type='SLEEP',
            event_start_time_gmt=datetime.datetime(2023, 7, 20, 5, 25),
            timezone_offset=-21600000,
            duration_in_milliseconds=24660000,
            body_battery_impact=42,
            feedback_type='POSITIVE',
            short_feedback='Good sleep recharged your Body Battery'
        ),
        activity_name=None,
        activity_type=None,
        activity_id=None,
        average_stress=17,
        stress_values_array=[[1689830700000, 18], [1689830760000, 16], ...],
        body_battery_values_array=[[1689830700000, 'charged', 58, 2.0], ...]
    )
]

List Body Battery events over several days

garth.BodyBatteryData.list("2023-07-20", 7)

Daily Body Battery and stress data (modern API)

garth.DailyBodyBatteryStress.get("2023-07-20")
DailyBodyBatteryStress(
    user_profile_pk=2591602,
    calendar_date=datetime.date(2023, 7, 20),
    start_timestamp_gmt=datetime.datetime(2023, 7, 20, 0, 0),
    end_timestamp_gmt=datetime.datetime(2023, 7, 20, 23, 59, 59),
    start_timestamp_local=datetime.datetime(2023, 7, 19, 18, 0),
    end_timestamp_local=datetime.datetime(2023, 7, 20, 17, 59, 59),
    max_stress_level=65,
    avg_stress_level=22,
    stress_chart_value_offset=0,
    stress_chart_y_axis_origin=0,
    stress_values_array=[[1689811200000, 25], [1689811800000, 18], ...],
    body_battery_values_array=[[1689811200000, 'charged', 85, 2.0], ...]
)

Access computed properties for current, max, min Body Battery levels

daily_data = garth.DailyBodyBatteryStress.get("2023-07-20")
print(f"Current: {daily_data.current_body_battery}")    # Latest Body Battery level
print(f"Max: {daily_data.max_body_battery}")            # Highest level of the day  
print(f"Min: {daily_data.min_body_battery}")            # Lowest level of the day
print(f"Change: {daily_data.body_battery_change}")      # Net change for the day

List daily Body Battery data over several days

garth.DailyBodyBatteryStress.list("2023-07-20", 30)

---

Save README.md and run a markdown linter or preview the file to verify that the new Body Battery section renders correctly and matches the style of the adjacent Stats resources sections.

</details>

- [ ] <!-- {"checkboxId": "32599f3c-a2a5-4bb9-b887-dee90db62b97", "radioGroupId": "edit-actions-group-2927337747"} --> ✅ Create PR with these edits
- [ ] <!-- {"checkboxId": "85e7c9f1-3a23-4b8e-9f25-d7a5b234a8c1", "radioGroupId": "edit-actions-group-2927337747"} --> 📌 Create commit in current branch
- [ ] <!-- {"checkboxId": "6b3df8e2-79f5-4eb8-9d4a-c7d3a6a5f7b1", "radioGroupId": "edit-actions-group-2927337747"} --> 📋 Get copyable edits

<!-- <agentComponentType>plan-edits</agentComponentType> -->

<!-- <rawResChunk><rationale>
The user requested a PR updating the README to reflect the changes made in the branch that refactored the Body Battery module into separate files and APIs. The relevant file to edit is README.md to add documentation about the new Body Battery resources, their usage, and examples.

No other files need editing or creation for this request.
</rationale>
<toEditFile>README.md</toEditFile></rawResChunk> -->

<!-- <rawResChunk><rationale>
- README.md: The main documentation file where the new Body Battery section and examples must be added.
- src/garth/data/body_battery/events.py: Defines BodyBatteryData and BodyBatteryEvent (classes and methods) that need to be described.
- src/garth/data/body_battery/daily_stress.py: Contains DailyBodyBatteryStress class and its properties for the modern API to document.
- src/garth/data/body_battery/readings.py: Holds shared data classes and parsing utilities; useful to show returned data shapes.
- tests/data/test_body_battery_data.py: Includes usage patterns and assertions for the legacy events API, ideal for code snippets.
- tests/data/cassettes/test_body_battery_data_get.yaml: Sample single‐day event response for example payloads.
- tests/data/cassettes/test_body_battery_data_list.yaml: Sample multi‐day event list response.
- tests/data/cassettes/test_daily_body_battery_stress_get.yaml: Sample single‐day stress API response.
- tests/data/cassettes/test_daily_body_battery_stress_list.yaml: Sample multi‐day stress API response.
- pyproject.toml: Contains project metadata (including README reference) that may require syncing if the README changes.
- .github/workflows/ci.yml: CI configuration that will validate markdown/linting after the README is updated.
</rationale>

<generated_script>
```shell
#!/bin/bash
# List the primary documentation file
fd -g README.md

# Project metadata file
fd -g pyproject.toml

# CI workflow configuration
fd -g .github/workflows/ci.yml

# All Body Battery source files to reference for signatures and types
fd -g 'src/garth/data/body_battery/*.py'

# Test code showing usage patterns for BodyBatteryData
fd -g 'tests/data/test_body_battery_data.py'

# Fixture cassettes for example payloads in README
fd -g 'tests/data/cassettes/test_body_battery_data_get.yaml'
fd -g 'tests/data/cassettes/test_body_battery_data_list.yaml'
fd -g 'tests/data/cassettes/test_daily_body_battery_stress_get.yaml'
fd -g 'tests/data/cassettes/test_daily_body_battery_stress_list.yaml'

</generated_script> -->

@matin
Copy link
Copy Markdown
Owner

matin commented Jun 1, 2025

@coderabbitai make the changes you're proposing

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Jun 1, 2025

Tip

For best results, initiate chat on the files or code changes.

Creating a PR to execute the edits....

The changes have been created in this pull request: View PR

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.

2 participants