diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000..2b22bda --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,22 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json # Schema for CodeRabbit configurations +language: "en-US" +early_access: true +reviews: + profile: "assertive" + request_changes_workflow: false + high_level_summary: true + poem: false + review_status: true + collapse_walkthrough: false + auto_review: + enabled: true + drafts: false + path_filters: + - "!tests/**/cassettes/**" + path_instructions: + - path: "tests/**" + instructions: | + - test functions shouldn't have a return type hint + - it's ok to use `assert` instead of `pytest.assume()` +chat: + auto_reply: true diff --git a/garth/__init__.py b/garth/__init__.py index 51612ce..17640ac 100644 --- a/garth/__init__.py +++ b/garth/__init__.py @@ -2,6 +2,7 @@ from .http import Client, client from .stats import ( DailyHRV, + DailyHydration, DailyIntensityMinutes, DailySleep, DailySteps, @@ -16,6 +17,7 @@ __all__ = [ "Client", "DailyHRV", + "DailyHydration", "DailyIntensityMinutes", "DailySleep", "DailySteps", diff --git a/garth/http.py b/garth/http.py index 037a1ea..5fa62fc 100644 --- a/garth/http.py +++ b/garth/http.py @@ -181,7 +181,7 @@ def download(self, path: str, **kwargs) -> bytes: def upload( self, fp: IO[bytes], /, path: str = "/upload-service/upload" - ) -> Dict[str, Any]: + ) -> Optional[Dict[str, Any]]: fname = os.path.basename(fp.name) files = {"file": (fname, fp)} return self.connectapi( diff --git a/garth/stats/__init__.py b/garth/stats/__init__.py index 3a257ec..ff6f94d 100644 --- a/garth/stats/__init__.py +++ b/garth/stats/__init__.py @@ -1,5 +1,6 @@ __all__ = [ "DailyHRV", + "DailyHydration", "DailyIntensityMinutes", "DailySleep", "DailySteps", @@ -10,6 +11,7 @@ ] from .hrv import DailyHRV +from .hydration import DailyHydration from .intensity_minutes import DailyIntensityMinutes, WeeklyIntensityMinutes from .sleep import DailySleep from .steps import DailySteps, WeeklySteps diff --git a/garth/stats/_base.py b/garth/stats/_base.py index fb25c80..5d6aae7 100644 --- a/garth/stats/_base.py +++ b/garth/stats/_base.py @@ -1,13 +1,11 @@ from datetime import date, timedelta -from typing import ClassVar, List, Optional, Union +from typing import ClassVar, List, Optional, Type, Union from pydantic.dataclasses import dataclass from .. import http from ..utils import camel_to_snake_dict, format_end_date -BASE_PATH = "/usersummary-service/stats/stress" - @dataclass(frozen=True) class Stats: @@ -18,7 +16,7 @@ class Stats: @classmethod def list( - cls, + cls: Type["Stats"], end: Union[date, str, None] = None, period: int = 1, *, @@ -32,7 +30,7 @@ def list( page = cls.list(end, cls._page_size, client=client) if not page: return [] - page = ( + return ( cls.list( end - timedelta(**{period_type: cls._page_size}), period - cls._page_size, @@ -40,7 +38,6 @@ def list( ) + page ) - return page start = end - timedelta(**{period_type: period - 1}) path = cls._path.format(start=start, end=end, period=period) diff --git a/garth/stats/hydration.py b/garth/stats/hydration.py new file mode 100644 index 0000000..1dc9096 --- /dev/null +++ b/garth/stats/hydration.py @@ -0,0 +1,16 @@ +from typing import ClassVar + +from pydantic.dataclasses import dataclass + +from ._base import Stats + +BASE_PATH = "/usersummary-service/stats/hydration" + + +@dataclass(frozen=True) +class DailyHydration(Stats): + value_in_ml: float + goal_in_ml: float + + _path: ClassVar[str] = f"{BASE_PATH}/daily/{{start}}/{{end}}" + _page_size: ClassVar[int] = 28 diff --git a/garth/version.py b/garth/version.py index 42d5b2b..1df21f4 100644 --- a/garth/version.py +++ b/garth/version.py @@ -1 +1 @@ -__version__ = "0.4.46" +__version__ = "0.4.47" diff --git a/tests/data/test_hrv_data.py b/tests/data/test_hrv_data.py index 91abd71..329579a 100644 --- a/tests/data/test_hrv_data.py +++ b/tests/data/test_hrv_data.py @@ -6,7 +6,7 @@ from garth.http import Client -@pytest.mark.vcr +@pytest.mark.vcr() def test_hrv_data_get(authed_client: Client): hrv_data = HRVData.get("2023-07-20", client=authed_client) assert hrv_data @@ -16,7 +16,7 @@ def test_hrv_data_get(authed_client: Client): assert HRVData.get("2021-07-20", client=authed_client) is None -@pytest.mark.vcr +@pytest.mark.vcr() def test_hrv_data_list(authed_client: Client): days = 2 end = date(2023, 7, 20) diff --git a/tests/data/test_sleep_data.py b/tests/data/test_sleep_data.py index dfedb57..1901588 100644 --- a/tests/data/test_sleep_data.py +++ b/tests/data/test_sleep_data.py @@ -6,7 +6,7 @@ from garth.http import Client -@pytest.mark.vcr +@pytest.mark.vcr() def test_sleep_data_get(authed_client: Client): sleep_data = SleepData.get("2021-07-20", client=authed_client) assert sleep_data @@ -15,7 +15,7 @@ def test_sleep_data_get(authed_client: Client): assert sleep_data.daily_sleep_dto.sleep_end -@pytest.mark.vcr +@pytest.mark.vcr() def test_sleep_data_list(authed_client: Client): end = date(2021, 7, 20) days = 20 diff --git a/tests/stats/cassettes/test_daily_hydration.yaml b/tests/stats/cassettes/test_daily_hydration.yaml new file mode 100644 index 0000000..7b32582 --- /dev/null +++ b/tests/stats/cassettes/test_daily_hydration.yaml @@ -0,0 +1,73 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer SANITIZED + Connection: + - keep-alive + User-Agent: + - Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/605.1.15 + (KHTML, like Gecko) Mobile/15E148 + method: GET + uri: https://connectapi.garmin.com/usersummary-service/stats/hydration/daily/2024-06-29/2024-06-29 + response: + body: + string: '[{"calendarDate": "2024-06-29", "valueInML": 1750.0, "goalInML": 2800.0}]' + headers: + CF-Cache-Status: + - DYNAMIC + CF-RAY: + - 89baf564a97ac302-IAH + Cache-Control: + - no-cache, no-store, private + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Sun, 30 Jun 2024 03:09:37 GMT + NEL: + - '{"success_fraction":0.01,"report_to":"cf-nel","max_age":604800}' + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=GRxjJQH4AeepFPPzqKaKzuF%2FwWsdMivGK4yskWFLewsm9h57VSu9AdrdMi9bdST3vzQ4oVJDxEfU4vR6EXLlsbom6nYoo6nCAtih9MJ3sIlW7aih0DTEBXrNcwILDtsSsmHmUe%2FM1A%3D%3D"}],"group":"cf-nel","max_age":604800}' + Server: + - cloudflare + Set-Cookie: + - ADRUM_BTa=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED; + Secure + - ADRUM_BTa=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED; + Secure + - ADRUM_BT1=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED; + Secure + - ADRUM_BT1=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED; + Secure + - ADRUM_BT1=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED; + Secure + - _cfuvid=SANITIZED; path=SANITIZED; domain=SANITIZED; HttpOnly; Secure; SameSite=SANITIZED + Transfer-Encoding: + - chunked + alt-svc: + - h3=":443"; ma=86400 + pragma: + - no-cache + set-cookie: + - ADRUM_BTa=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED; + Secure + - ADRUM_BTa=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED; + Secure + - ADRUM_BT1=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED; + Secure + - ADRUM_BT1=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED; + Secure + - ADRUM_BT1=SANITIZED; Max-Age=SANITIZED; Expires=SANITIZED; Path=SANITIZED; + Secure + - _cfuvid=SANITIZED; path=SANITIZED; domain=SANITIZED; HttpOnly; Secure; SameSite=SANITIZED + status: + code: 200 + message: OK +version: 1 diff --git a/tests/stats/test_hrv.py b/tests/stats/test_hrv.py index 98b6167..f4c34ee 100644 --- a/tests/stats/test_hrv.py +++ b/tests/stats/test_hrv.py @@ -6,7 +6,7 @@ from garth.http import Client -@pytest.mark.vcr +@pytest.mark.vcr() def test_daily_hrv(authed_client: Client): end = date(2023, 7, 20) days = 20 @@ -15,7 +15,7 @@ def test_daily_hrv(authed_client: Client): assert len(daily_hrv) == days -@pytest.mark.vcr +@pytest.mark.vcr() def test_daily_hrv_paginate(authed_client: Client): end = date(2023, 7, 20) days = 40 @@ -24,14 +24,14 @@ def test_daily_hrv_paginate(authed_client: Client): assert len(daily_hrv) == days -@pytest.mark.vcr +@pytest.mark.vcr() def test_daily_hrv_no_results(authed_client: Client): end = date(1990, 7, 20) daily_hrv = DailyHRV.list(end, client=authed_client) assert daily_hrv == [] -@pytest.mark.vcr +@pytest.mark.vcr() def test_daily_hrv_paginate_no_results(authed_client: Client): end = date(1990, 7, 20) days = 40 diff --git a/tests/stats/test_hydration.py b/tests/stats/test_hydration.py new file mode 100644 index 0000000..c49aca2 --- /dev/null +++ b/tests/stats/test_hydration.py @@ -0,0 +1,13 @@ +from datetime import date + +import pytest + +from garth import DailyHydration +from garth.http import Client + + +@pytest.mark.vcr() +def test_daily_hydration(authed_client: Client): + end = date(2024, 6, 29) + daily_hydration = DailyHydration.list(end, client=authed_client) + assert daily_hydration[-1].calendar_date == end diff --git a/tests/stats/test_intensity_minutes.py b/tests/stats/test_intensity_minutes.py index 603146b..20852d5 100644 --- a/tests/stats/test_intensity_minutes.py +++ b/tests/stats/test_intensity_minutes.py @@ -6,7 +6,7 @@ from garth.http import Client -@pytest.mark.vcr +@pytest.mark.vcr() def test_daily_intensity_minutes(authed_client: Client): end = date(2023, 7, 20) days = 20 @@ -15,7 +15,7 @@ def test_daily_intensity_minutes(authed_client: Client): assert len(daily_im) == days -@pytest.mark.vcr +@pytest.mark.vcr() def test_weekly_intensity_minutes(authed_client: Client): end = date(2023, 7, 20) weeks = 12 diff --git a/tests/stats/test_sleep_stats.py b/tests/stats/test_sleep_stats.py index d61bd0e..5b0f659 100644 --- a/tests/stats/test_sleep_stats.py +++ b/tests/stats/test_sleep_stats.py @@ -6,7 +6,7 @@ from garth.http import Client -@pytest.mark.vcr +@pytest.mark.vcr() def test_daily_sleep(authed_client: Client): end = date(2023, 7, 20) days = 20 diff --git a/tests/stats/test_steps.py b/tests/stats/test_steps.py index 4dd289f..dd6a9a7 100644 --- a/tests/stats/test_steps.py +++ b/tests/stats/test_steps.py @@ -6,7 +6,7 @@ from garth.http import Client -@pytest.mark.vcr +@pytest.mark.vcr() def test_daily_steps(authed_client: Client): end = date(2023, 7, 20) days = 20 @@ -15,7 +15,7 @@ def test_daily_steps(authed_client: Client): assert len(daily_steps) == days -@pytest.mark.vcr +@pytest.mark.vcr() def test_weekly_steps(authed_client: Client): end = date(2023, 7, 20) weeks = 52 diff --git a/tests/stats/test_stress.py b/tests/stats/test_stress.py index ef1863c..f79bb31 100644 --- a/tests/stats/test_stress.py +++ b/tests/stats/test_stress.py @@ -6,7 +6,7 @@ from garth.http import Client -@pytest.mark.vcr +@pytest.mark.vcr() def test_daily_stress(authed_client: Client): end = date(2023, 7, 20) days = 20 @@ -15,7 +15,7 @@ def test_daily_stress(authed_client: Client): assert len(daily_stress) == days -@pytest.mark.vcr +@pytest.mark.vcr() def test_daily_stress_pagination(authed_client: Client): end = date(2023, 7, 20) days = 60 @@ -23,7 +23,7 @@ def test_daily_stress_pagination(authed_client: Client): assert len(daily_stress) == days -@pytest.mark.vcr +@pytest.mark.vcr() def test_weekly_stress(authed_client: Client): end = date(2023, 7, 20) weeks = 52 @@ -32,7 +32,7 @@ def test_weekly_stress(authed_client: Client): assert weekly_stress[-1].calendar_date == end - timedelta(days=6) -@pytest.mark.vcr +@pytest.mark.vcr() def test_weekly_stress_pagination(authed_client: Client): end = date(2023, 7, 20) weeks = 60 @@ -41,7 +41,7 @@ def test_weekly_stress_pagination(authed_client: Client): assert weekly_stress[-1].calendar_date == end - timedelta(days=6) -@pytest.mark.vcr +@pytest.mark.vcr() def test_weekly_stress_beyond_data(authed_client: Client): end = date(2023, 7, 20) weeks = 1000 diff --git a/tests/test_http.py b/tests/test_http.py index a8c3084..1a64f09 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -102,7 +102,7 @@ def test_pool_connections(client: Client): assert adapter._pool_maxsize == 99 -@pytest.mark.vcr +@pytest.mark.vcr() def test_client_request(client: Client): resp = client.request("GET", "connect", "/") assert resp.ok @@ -112,7 +112,7 @@ def test_client_request(client: Client): assert "404" in str(e.value) -@pytest.mark.vcr +@pytest.mark.vcr() def test_login_success_mfa(monkeypatch, client: Client): def mock_input(_): return "327751" @@ -126,14 +126,14 @@ def mock_input(_): assert client.oauth2_token -@pytest.mark.vcr +@pytest.mark.vcr() def test_username(authed_client: Client): assert authed_client._profile is None assert authed_client.username assert authed_client._profile -@pytest.mark.vcr +@pytest.mark.vcr() def test_connectapi(authed_client: Client): stress = authed_client.connectapi( "/usersummary-service/stats/stress/daily/2023-07-21/2023-07-21" @@ -151,7 +151,7 @@ def test_connectapi(authed_client: Client): ] -@pytest.mark.vcr +@pytest.mark.vcr() def test_refresh_oauth2_token(authed_client: Client): assert authed_client.oauth2_token authed_client.oauth2_token.expires_at = int(time.time()) @@ -162,7 +162,7 @@ def test_refresh_oauth2_token(authed_client: Client): assert profile["userName"] -@pytest.mark.vcr +@pytest.mark.vcr() def test_download(authed_client: Client): downloaded = authed_client.download( "/download-service/files/activity/11998957007" @@ -172,7 +172,7 @@ def test_download(authed_client: Client): assert downloaded[:4] == zip_magic_number -@pytest.mark.vcr +@pytest.mark.vcr() def test_upload(authed_client: Client): fpath = "tests/12129115726_ACTIVITY.fit" with open(fpath, "rb") as f: @@ -180,7 +180,7 @@ def test_upload(authed_client: Client): assert uploaded -@pytest.mark.vcr +@pytest.mark.vcr() def test_delete(authed_client: Client): activity_id = "12135235656" path = f"/activity-service/activity/{activity_id}" @@ -195,7 +195,7 @@ def test_delete(authed_client: Client): assert "404" in str(e.value) -@pytest.mark.vcr +@pytest.mark.vcr() def test_put(authed_client: Client): data = [ { diff --git a/tests/test_sso.py b/tests/test_sso.py index daa80d9..abb62d7 100644 --- a/tests/test_sso.py +++ b/tests/test_sso.py @@ -8,13 +8,13 @@ from garth.http import Client -@pytest.mark.vcr +@pytest.mark.vcr() def test_login_email_password_fail(client: Client): with pytest.raises(GarthHTTPError): sso.login("user@example.com", "wrong_p@ssword", client=client) -@pytest.mark.vcr +@pytest.mark.vcr() def test_login_success(client: Client): oauth1, oauth2 = sso.login( "user@example.com", "correct_password", client=client @@ -26,7 +26,7 @@ def test_login_success(client: Client): assert isinstance(oauth2, OAuth2Token) -@pytest.mark.vcr +@pytest.mark.vcr() def test_login_success_mfa(monkeypatch, client: Client): def mock_input(_): return "671091" @@ -42,7 +42,7 @@ def mock_input(_): assert isinstance(oauth2, OAuth2Token) -@pytest.mark.vcr +@pytest.mark.vcr() def test_login_success_mfa_async(monkeypatch, client: Client): def mock_input(_): return "031174" @@ -77,7 +77,7 @@ def test_set_expirations(oauth2_token_dict: dict): ) -@pytest.mark.vcr +@pytest.mark.vcr() def test_exchange(authed_client: Client): assert authed_client.oauth1_token oauth1_token = authed_client.oauth1_token diff --git a/tests/test_users.py b/tests/test_users.py index c3421ea..1841873 100644 --- a/tests/test_users.py +++ b/tests/test_users.py @@ -4,13 +4,13 @@ from garth.http import Client -@pytest.mark.vcr +@pytest.mark.vcr() def test_user_profile(authed_client: Client): profile = UserProfile.get(client=authed_client) assert profile.user_name -@pytest.mark.vcr +@pytest.mark.vcr() def test_user_setttings(authed_client: Client): settings = UserSettings.get(client=authed_client) assert settings.user_data