From ecca094c0e7c737410766412f16c129871c65f65 Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Wed, 22 Jan 2025 14:12:28 +0100 Subject: [PATCH 1/8] Fixes for MFA --- README.md | 6 +++--- garth/http.py | 13 +++++++++++-- garth/sso.py | 11 ++++------- garth/version.py | 2 +- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 39d76dd..2240092 100644 --- a/README.md +++ b/README.md @@ -140,10 +140,10 @@ garth.login(email, password, prompt_mfa=lambda: input("Enter MFA code: ")) For advanced use cases (like async handling), MFA can be handled separately: ```python -result = garth.login(email, password, return_on_mfa=True) -if isinstance(result, dict): # MFA is required +result1, result2 = garth.login(email, password, return_on_mfa=True) +if result1 == "needs_mfa": # MFA is required mfa_code = "123456" # Get this from your custom MFA flow - oauth1, oauth2 = garth.resume_login(result['client_state'], mfa_code) + oauth1, oauth2 = garth.resume_login(result2, mfa_code) ``` ### Configure diff --git a/garth/http.py b/garth/http.py index d086d85..1027645 100644 --- a/garth/http.py +++ b/garth/http.py @@ -161,10 +161,19 @@ def put(self, *args, **kwargs) -> Response: return self.request("PUT", *args, **kwargs) def login(self, *args, **kwargs): - self.oauth1_token, self.oauth2_token = sso.login( + oauth1, oauth2 = sso.login( *args, **kwargs, client=self ) - + self.oauth1_token = oauth1 + self.oauth2_token = oauth2 + return oauth1, oauth2 + + def resume_login(self, *args, **kwargs): + self.oauth1_token, self.oauth2_token = sso.resume_login( + *args, **kwargs, # client=self + ) + return self.oauth1_token, self.oauth2_token + def refresh_oauth2(self): assert self.oauth1_token, "OAuth1 token is required for OAuth2 refresh" # There is a way to perform a refresh of an OAuth2 token, but it diff --git a/garth/sso.py b/garth/sso.py index 4fe3448..e3c29cf 100644 --- a/garth/sso.py +++ b/garth/sso.py @@ -141,13 +141,10 @@ def login( # Handle MFA if "MFA" in title: if return_on_mfa or prompt_mfa is None: - return { - "needs_mfa": True, - "client_state": { - "csrf_token": csrf_token, - "signin_params": SIGNIN_PARAMS, - "client": client, - }, + return "needs_mfa", { + "csrf_token": csrf_token, + "signin_params": SIGNIN_PARAMS, + "client": client, } handle_mfa(client, SIGNIN_PARAMS, prompt_mfa) diff --git a/garth/version.py b/garth/version.py index 7225152..dd9b22c 100644 --- a/garth/version.py +++ b/garth/version.py @@ -1 +1 @@ -__version__ = "0.5.2" +__version__ = "0.5.1" From 7de1671e3bb3b18a5633b4f9074be3c075db1afd Mon Sep 17 00:00:00 2001 From: Ron Date: Wed, 22 Jan 2025 14:14:04 +0100 Subject: [PATCH 2/8] Update version.py --- garth/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/garth/version.py b/garth/version.py index dd9b22c..43a1e95 100644 --- a/garth/version.py +++ b/garth/version.py @@ -1 +1 @@ -__version__ = "0.5.1" +__version__ = "0.5.3" From 2a22263a8cc04f914761c0edea5730eba8c7b702 Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Wed, 22 Jan 2025 14:23:20 +0100 Subject: [PATCH 3/8] Code cleanup --- garth/http.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/garth/http.py b/garth/http.py index 1027645..59f8a57 100644 --- a/garth/http.py +++ b/garth/http.py @@ -161,16 +161,14 @@ def put(self, *args, **kwargs) -> Response: return self.request("PUT", *args, **kwargs) def login(self, *args, **kwargs): - oauth1, oauth2 = sso.login( + self.oauth1_token, self.oauth2_token = sso.login( *args, **kwargs, client=self ) - self.oauth1_token = oauth1 - self.oauth2_token = oauth2 - return oauth1, oauth2 + return self.oauth1_token, self.oauth2_token def resume_login(self, *args, **kwargs): self.oauth1_token, self.oauth2_token = sso.resume_login( - *args, **kwargs, # client=self + *args, **kwargs ) return self.oauth1_token, self.oauth2_token From e97afb144e685f870f5d6ca0c574a2baef844ee6 Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Wed, 22 Jan 2025 14:27:46 +0100 Subject: [PATCH 4/8] Fixed linting --- garth/sso.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/garth/sso.py b/garth/sso.py index e3c29cf..37b7ffa 100644 --- a/garth/sso.py +++ b/garth/sso.py @@ -1,7 +1,7 @@ import asyncio import re import time -from typing import Callable, Dict, Tuple +from typing import Callable, Dict, Tuple, Literal from urllib.parse import parse_qs import requests @@ -74,7 +74,7 @@ def login( client: "http.Client | None" = None, prompt_mfa: Callable | None = lambda: input("MFA code: "), return_on_mfa: bool = False, -) -> Tuple[OAuth1Token, OAuth2Token] | dict: +) -> Tuple[OAuth1Token, OAuth2Token] | Tuple[Literal["needs_mfa"], dict]: """Login to Garmin Connect. Args: From 1d4e1038d78b8d436f2ca40ff77b5fb4aa9f2f87 Mon Sep 17 00:00:00 2001 From: Ron Date: Mon, 3 Feb 2025 18:24:01 +0100 Subject: [PATCH 5/8] Fix linting --- uv.lock | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/uv.lock b/uv.lock index a370ea4..24504a8 100644 --- a/uv.lock +++ b/uv.lock @@ -417,7 +417,6 @@ wheels = [ [[package]] name = "garth" -version = "0.5.2" source = { editable = "." } dependencies = [ { name = "pydantic" }, @@ -499,7 +498,7 @@ name = "ipykernel" version = "6.29.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "appnope", marker = "platform_system == 'Darwin'" }, + { name = "appnope", marker = "sys_platform == 'darwin'" }, { name = "comm" }, { name = "debugpy" }, { name = "ipython" }, @@ -1336,7 +1335,7 @@ version = "1.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, - { name = "vcrpy", version = "5.1.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation == 'PyPy' or python_full_version >= '3.11'" }, + { name = "vcrpy", version = "5.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' or platform_python_implementation == 'PyPy'" }, { name = "vcrpy", version = "6.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_python_implementation != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1a/60/104c619483c1a42775d3f8b27293f1ecfc0728014874d065e68cb9702d49/pytest-vcr-1.0.2.tar.gz", hash = "sha256:23ee51b75abbcc43d926272773aae4f39f93aceb75ed56852d0bf618f92e1896", size = 3810 } @@ -1692,9 +1691,9 @@ resolution-markers = [ "python_full_version >= '3.12'", ] dependencies = [ - { name = "pyyaml", marker = "platform_python_implementation == 'PyPy' or python_full_version >= '3.11'" }, - { name = "wrapt", marker = "platform_python_implementation == 'PyPy' or python_full_version >= '3.11'" }, - { name = "yarl", marker = "platform_python_implementation == 'PyPy' or python_full_version >= '3.11'" }, + { name = "pyyaml", marker = "python_full_version >= '3.11' or platform_python_implementation == 'PyPy'" }, + { name = "wrapt", marker = "python_full_version >= '3.11' or platform_python_implementation == 'PyPy'" }, + { name = "yarl", marker = "python_full_version >= '3.11' or platform_python_implementation == 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a5/ea/a166a3cce4ac5958ba9bbd9768acdb1ba38ae17ff7986da09fa5b9dbc633/vcrpy-5.1.0.tar.gz", hash = "sha256:bbf1532f2618a04f11bce2a99af3a9647a32c880957293ff91e0a5f187b6b3d2", size = 84576 } wheels = [ From f4e10d51ef96844e53ab38c9bce105563f9aaee9 Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Mon, 3 Feb 2025 18:33:41 +0100 Subject: [PATCH 6/8] Fix linting --- garth/http.py | 16 ++++++++-------- garth/sso.py | 2 +- tests/test_http.py | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/garth/http.py b/garth/http.py index 59f8a57..40503f0 100644 --- a/garth/http.py +++ b/garth/http.py @@ -97,9 +97,9 @@ def user_profile(self): self._user_profile = self.connectapi( "/userprofile-service/socialProfile" ) - assert isinstance( - self._user_profile, dict - ), "No profile from connectapi" + assert isinstance(self._user_profile, dict), ( + "No profile from connectapi" + ) return self._user_profile @property @@ -126,9 +126,9 @@ def request( if referrer is True and self.last_resp: headers["referer"] = self.last_resp.url if api: - assert ( - self.oauth1_token - ), "OAuth1 token is required for API requests" + assert self.oauth1_token, ( + "OAuth1 token is required for API requests" + ) if not self.oauth2_token or self.oauth2_token.expired: self.refresh_oauth2() headers["Authorization"] = str(self.oauth2_token) @@ -165,13 +165,13 @@ def login(self, *args, **kwargs): *args, **kwargs, client=self ) return self.oauth1_token, self.oauth2_token - + def resume_login(self, *args, **kwargs): self.oauth1_token, self.oauth2_token = sso.resume_login( *args, **kwargs ) return self.oauth1_token, self.oauth2_token - + def refresh_oauth2(self): assert self.oauth1_token, "OAuth1 token is required for OAuth2 refresh" # There is a way to perform a refresh of an OAuth2 token, but it diff --git a/garth/sso.py b/garth/sso.py index 37b7ffa..408f2b0 100644 --- a/garth/sso.py +++ b/garth/sso.py @@ -1,7 +1,7 @@ import asyncio import re import time -from typing import Callable, Dict, Tuple, Literal +from typing import Callable, Dict, Literal, Tuple from urllib.parse import parse_qs import requests diff --git a/tests/test_http.py b/tests/test_http.py index 1e02e8d..a2d33d7 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -113,9 +113,9 @@ def test_configure_pool_connections(client: Client): assert client.pool_connections == 99 adapter = client.sess.adapters["https://"] assert isinstance(adapter, HTTPAdapter) - assert ( - getattr(adapter, "_pool_connections", None) == 99 - ), "Pool connections not properly configured" + assert getattr(adapter, "_pool_connections", None) == 99, ( + "Pool connections not properly configured" + ) @pytest.mark.vcr From ea4621a23295c88cddf4bb250c629ef122f3d1ef Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Mon, 3 Feb 2025 18:46:36 +0100 Subject: [PATCH 7/8] Added test for resume_login --- tests/test_http.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_http.py b/tests/test_http.py index a2d33d7..99ac946 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -247,3 +247,18 @@ def test_put(authed_client: Client): json=data, ) assert authed_client.connectapi(path) + + +@pytest.mark.vcr +def test_resume_login(client: Client): + client.oauth1_token, client.oauth2_token = client.login( + "user@example.com", "correct_password" + ) + assert client.oauth1_token + assert client.oauth2_token + client.oauth1_token, client.oauth2_token = None, None + assert client.oauth1_token is None + assert client.oauth2_token is None + client.resume_login("user@example.com", "correct_password") + assert client.oauth1_token + assert client.oauth2_token From 41c5834f88e7efcca350ad21997e0c5034e0cb23 Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Mon, 3 Feb 2025 18:54:03 +0100 Subject: [PATCH 8/8] Fix tests --- tests/test_http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_http.py b/tests/test_http.py index 99ac946..d92c9cf 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -249,7 +249,7 @@ def test_put(authed_client: Client): assert authed_client.connectapi(path) -@pytest.mark.vcr +@pytest.mark.vcr(record_mode="once") def test_resume_login(client: Client): client.oauth1_token, client.oauth2_token = client.login( "user@example.com", "correct_password"