Add Body Battery Support#116
Add Body Battery Support#116matin merged 23 commits intomatin:mainfrom bes-dev:bes-dev/body_battery_0001
Conversation
- 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}
WalkthroughThe 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 Changes
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
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]
📜 Recent review detailsConfiguration used: .coderabbit.yaml 📒 Files selected for processing (3)
✅ Files skipped from review due to trivial changes (1)
🧰 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()`
🪛 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)
✨ Finishing Touches
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed 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)
Other keywords and placeholders
Documentation and Community
|
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (6)
tests/data/cassettes/test_body_battery_data_get.yamlis excluded by!tests/**/cassettes/**tests/data/cassettes/test_body_battery_data_list.yamlis excluded by!tests/**/cassettes/**tests/data/cassettes/test_body_battery_properties_edge_cases.yamlis excluded by!tests/**/cassettes/**tests/data/cassettes/test_daily_body_battery_stress_get.yamlis excluded by!tests/**/cassettes/**tests/data/cassettes/test_daily_body_battery_stress_get_no_data.yamlis excluded by!tests/**/cassettes/**tests/data/cassettes/test_daily_body_battery_stress_list.yamlis 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
assertinstead ofpytest.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.
There was a problem hiding this comment.
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_readingsare recalculated on each access, and metric properties call this multiple times, which could be inefficient for large datasets.Consider using
functools.cached_propertyor 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
📒 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
listmethods follow appropriate patterns for their respective data types:
DailyBodyBatteryStress.listappends single data objects (one per day)BodyBatteryData.listextends 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
BodyBatteryDataclass 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.
|
@bes-dev looks good at first glace, but I'll review in detail could you run in general, it'll be easier if you enable pre commit hooks |
There was a problem hiding this comment.
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
src/garth/data/body_battery.py
Outdated
|
|
||
| readings = [] | ||
| for values in self.body_battery_values_array: | ||
| if len(values) >= 4: |
There was a problem hiding this comment.
what's the significance of a length of 4 or more? a comment would be helpful
src/garth/data/body_battery.py
Outdated
| # 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") | ||
| ) |
There was a problem hiding this comment.
this isn't necessary. pydantic will automatically do this
src/garth/data/body_battery.py
Outdated
| 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", [] | ||
| ) | ||
| ) |
There was a problem hiding this comment.
why not use ..utils.camel_to_snake_dict() and field defaults? that'd let you do cls(**response)
src/garth/data/body_battery.py
Outdated
|
|
||
|
|
||
| @dataclass | ||
| class BodyBatteryData: |
There was a problem hiding this comment.
let's break up the two resources into difference files
src/garth/data/body_battery.py
Outdated
| 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 | ||
|
|
There was a problem hiding this comment.
why not use Data.list(). it functionally does the same thing but with threads
src/garth/data/body_battery.py
Outdated
|
|
||
|
|
||
| @dataclass | ||
| class DailyBodyBatteryStress: |
There was a problem hiding this comment.
why not inherit from Data? that'd give you .list() and consistency
src/garth/data/body_battery.py
Outdated
| activity_type: str | None = None | ||
| activity_id: str | None = None | ||
| average_stress: float | None = None | ||
| stress_values_array: List[List[int]] | None = None |
There was a problem hiding this comment.
please use list[] instead of List[] for consistency
src/garth/data/body_battery.py
Outdated
| if not self.body_battery_values_array: | ||
| return [] |
There was a problem hiding this comment.
is this necessary? if self.body_batter_values_array is empty, the for loop would just not run
There was a problem hiding this comment.
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:
- Data validation: The code assumes array elements have correct types but doesn't validate them
- 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 readingsDon't forget to import
cached_propertyfromfunctools.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_readingsand 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
⛔ Files ignored due to path filters (1)
uv.lockis 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 thedevorlintinggroups), 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:
• Moveruff>=0.11.5into 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.tomllines 77–81src/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
BodyBatteryReadingis acceptable since it's only importing a dataclass.
14-25: LGTM! Well-structured dataclass for event data.The
BodyBatteryEventdataclass is well-defined with appropriate field types and clear naming.
27-38: LGTM! Flexible dataclass structure for aggregated data.The
BodyBatteryDatadataclass 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_levelproperty 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
listmethod 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
Database class ensures API consistency.
15-49: LGTM! Well-designed dataclasses with comprehensive field coverage.The three dataclasses are appropriately structured:
BodyBatteryReadingandStressReadingprovide clean abstractions for individual readingsDailyBodyBatteryStressinherits fromDataand includes comprehensive daily metricsThe 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_changemethod 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
getmethod is well-implemented:
- Properly inherits from
Database class and follows the established pattern- Good use of
camel_to_snake_dictfor 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.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (3)
src/garth/data/body_battery/events.py (3)
1-1: Remove unused import.The
timedeltaimport 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.timedeltaimported but unusedRemove 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
📒 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
Noneinputs 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_propertyfor expensive parsing operations- Robust error handling when readings are empty (returning
Noneinstead of raising exceptions)- Clear separation between data storage and computed metrics
- The
body_battery_changecalculation properly handles edge cases by checking for sufficient readings
72-90: Well-implemented API integration.The
getmethod 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
listmethod demonstrates solid concurrent programming practices:
- Proper use of
ThreadPoolExecutorfor I/O-bound operations- Configurable
max_workersparameter with sensible default- Clean aggregation of results from multiple concurrent requests
- Proper resource management with context manager
There was a problem hiding this comment.
Actionable comments posted: 3
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 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
BodyBatteryEventdataclass is well-structured with appropriate field types and clear naming.
125-150: LGTM!The concurrent implementation using
ThreadPoolExecutoris well-designed for fetching multiple days of data efficiently. The configurablemax_workersparameter provides flexibility for different use cases.
There was a problem hiding this comment.
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 formatas 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
📒 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
timedeltaimport 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
BodyBatteryEventdataclass is well-structured with appropriate type hints and clear field names.
29-62: Well-designed dataclass with useful property methods.The
BodyBatteryDataclass 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.
|
@matin PR is ready for review Architectural Refactoring: Body Battery Module SplitSummaryImplemented the requested separation of Body Battery resources into dedicated files, following project patterns and addressing technical debt. Changes MadeFile Structure: Technical Improvements:
|
|
@bes-dev could you take a look at the failing tasks (lint and tests)? |
|
@matin problems was fixed, could you approve workflow to check it? |
Codecov ReportAll modified and coverable lines are covered by tests ✅
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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
@matin please, restart workflow |
There was a problem hiding this comment.
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) >= 0is 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) >= 0Or 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
⛔ Files ignored due to path filters (6)
tests/data/cassettes/test_body_battery_data_get.yamlis excluded by!tests/**/cassettes/**tests/data/cassettes/test_body_battery_data_list.yamlis excluded by!tests/**/cassettes/**tests/data/cassettes/test_body_battery_properties_edge_cases.yamlis excluded by!tests/**/cassettes/**tests/data/cassettes/test_daily_body_battery_stress_get.yamlis excluded by!tests/**/cassettes/**tests/data/cassettes/test_daily_body_battery_stress_get_no_data.yamlis excluded by!tests/**/cassettes/**tests/data/cassettes/test_daily_body_battery_stress_list.yamlis 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
assertinstead ofpytest.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
|
@matin Could you restart the workflow? Sorry for the many commits, but local testing did not show the problems that your CI highlighted 😞 |
There was a problem hiding this comment.
Actionable comments posted: 1
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
uv.lockis 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
Splittinguv 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
installtarget 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) |
There was a problem hiding this comment.
🛠️ Refactor suggestion
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.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
.github/workflows/ci.yml (1)
73-74: Same check for thetestinggroup
Ensure that pip ≥ 23.2 on each runner OS supports--group testingand that thetestinggroup is correctly declared inpyproject.toml.
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 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
| uv pip install --system -e . | ||
| uv pip install --system --group linting |
There was a problem hiding this comment.
💡 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.tomlLength of output: 246
🏁 Script executed:
#!/bin/bash
# Verify that 'linting' group is defined in pyproject.toml
rg "linting" -n pyproject.tomlLength of output: 45
🏁 Script executed:
#!/bin/bash
# Verify that pip ≥ 23.2 is available for the `--group` flag
pip --version || python -m pip --versionLength 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.
There was a problem hiding this comment.
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] | Noneallows 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
📒 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
chainfromitertoolsis appropriate and necessary for the list flattening implementation in thelistmethod.
43-47: Excellent implementation of list flattening logic.The flattening logic correctly handles both single instances and lists returned by
getmethods:
- Uses
isinstance(day, list)to differentiate between single instances and lists- Applies
chain.from_iterableto 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.
|
@bes-dev looks good! thanks for the work on this. I'll merge and release tomorrow |
|
@coderabbitai issue a PR updating the README to reflect the changes made in this branch |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chainI'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: 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"]
@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 🏁 Script executed: 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"]
@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)
Daily Body Battery & Stress (Modern API)
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... |
|
A summary of the edits CodeRabbit can apply:
A plan of each step that CodeRabbit will take in order to execute the edits for each file (with inline rough draft snippets): ➕ README.mdConfirm 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 dayList daily Body Battery data over several days garth.DailyBodyBatteryStress.list("2023-07-20", 30)</generated_script> --> |
|
@coderabbitai make the changes you're proposing |
|
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 |
Add Body Battery Support
Summary
What's Added
New Data Classes
BodyBatteryData - Legacy Body Battery events data
DailyBodyBatteryStress - Complete daily Body Battery and stress data
Key Features
API Endpoints Used
Test Coverage
Files Changed
Example Usage
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
Tests
Chores