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..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) @@ -164,6 +164,13 @@ def login(self, *args, **kwargs): self.oauth1_token, self.oauth2_token = sso.login( *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" diff --git a/garth/sso.py b/garth/sso.py index 4fe3448..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 +from typing import Callable, Dict, Literal, Tuple 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: @@ -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..43a1e95 100644 --- a/garth/version.py +++ b/garth/version.py @@ -1 +1 @@ -__version__ = "0.5.2" +__version__ = "0.5.3" diff --git a/tests/test_http.py b/tests/test_http.py index 1e02e8d..d92c9cf 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 @@ -247,3 +247,18 @@ def test_put(authed_client: Client): json=data, ) assert authed_client.connectapi(path) + + +@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" + ) + 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 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 = [