From e0b64a081ae91e8a560460ced6fe1e0010333987 Mon Sep 17 00:00:00 2001 From: Stainless Bot Date: Fri, 25 Oct 2024 23:27:11 +0000 Subject: [PATCH 01/11] feat: Add auth.start and auth.wait_for_completion sugar methods --- src/arcadepy/resources/auth.py | 124 ++++++++++++++++++++++++ tests/api_resources/test_auth_custom.py | 25 +++++ 2 files changed, 149 insertions(+) create mode 100644 tests/api_resources/test_auth_custom.py diff --git a/src/arcadepy/resources/auth.py b/src/arcadepy/resources/auth.py index 29c49e2..f89953b 100644 --- a/src/arcadepy/resources/auth.py +++ b/src/arcadepy/resources/auth.py @@ -23,6 +23,8 @@ __all__ = ["AuthResource", "AsyncAuthResource"] +_DEFAULT_LONGPOLL_WAIT_TIME = 45 + class AuthResource(SyncAPIResource): @cached_property @@ -90,6 +92,35 @@ def authorize( cast_to=AuthorizationResponse, ) + def start( + self, + user_id: str, + provider: str, + provider_type: str | None = "oauth2", + scopes: list[str] | None = None, + ) -> AuthorizationResponse: + """ + Starts the authorization process for a given provider and scopes. + + Args: + user_id: The user ID for which authorization is being requested. + provider: The authorization provider (e.g., 'github', 'google', 'linkedin', 'microsoft', 'slack', 'spotify', 'x', 'zoom'). + provider_type: The type of authorization provider. Optional, defaults to 'oauth2'. + scopes: A list of scopes required for authorization, if any. + Returns: + The authorization response. + """ + scopes = scopes or [] + auth_requirement = auth_authorize_params.AuthRequirement( + provider_id=provider, + provider_type=provider_type or "oauth2", + oauth2=auth_authorize_params.AuthRequirementOauth2(scopes=scopes), + ) + return self.authorize( + auth_requirement=auth_requirement, + user_id=user_id, + ) + def status( self, *, @@ -143,6 +174,38 @@ def status( cast_to=AuthorizationResponse, ) + def wait_for_completion( + self, + auth_response_or_id: AuthorizationResponse | str, + scopes: list[str] | None = None, + ) -> AuthorizationResponse: + """ + Waits for the authorization process to complete, for example: + + ```py + auth_response = auth.start("you@example.com", "github") + auth_response = auth.wait_for_completion(auth_response) + ``` + """ + if isinstance(auth_response_or_id, AuthorizationResponse): + auth_id_val = auth_response_or_id.authorization_id + if not auth_id_val: + raise ValueError("Authorization ID is required") + scopes_val = " ".join(auth_response_or_id.scopes) if auth_response_or_id.scopes else NOT_GIVEN + auth_response = auth_response_or_id + else: + auth_id_val = auth_response_or_id + scopes_val = " ".join(scopes) if scopes else NOT_GIVEN + auth_response = AuthorizationResponse() + + while auth_response.status != "completed": + auth_response = self.status( + authorization_id=auth_id_val, + scopes=scopes_val, + wait=_DEFAULT_LONGPOLL_WAIT_TIME, + ) + return auth_response + class AsyncAuthResource(AsyncAPIResource): @cached_property @@ -210,6 +273,35 @@ async def authorize( cast_to=AuthorizationResponse, ) + async def start( + self, + user_id: str, + provider: str, + provider_type: str | None = "oauth2", + scopes: list[str] | None = None, + ) -> AuthorizationResponse: + """ + Starts the authorization process for a given provider and scopes. + + Args: + user_id: The user ID for which authorization is being requested. + provider: The authorization provider (e.g., 'github', 'google', 'linkedin', 'microsoft', 'slack', 'spotify', 'x', 'zoom'). + provider_type: The type of authorization provider. Optional, defaults to 'oauth2'. + scopes: A list of scopes required for authorization, if any. + Returns: + The authorization response. + """ + scopes = scopes or [] + auth_requirement = auth_authorize_params.AuthRequirement( + provider_id=provider, + provider_type=provider_type or "oauth2", + oauth2=auth_authorize_params.AuthRequirementOauth2(scopes=scopes), + ) + return await self.authorize( + auth_requirement=auth_requirement, + user_id=user_id, + ) + async def status( self, *, @@ -263,6 +355,38 @@ async def status( cast_to=AuthorizationResponse, ) + async def wait_for_completion( + self, + auth_response_or_id: AuthorizationResponse | str, + scopes: list[str] | None = None, + ) -> AuthorizationResponse: + """ + Waits for the authorization process to complete, for example: + + ```py + auth_response = auth.start("you@example.com", "github") + auth_response = auth.wait_for_completion(auth_response) + ``` + """ + if isinstance(auth_response_or_id, AuthorizationResponse): + auth_id_val = auth_response_or_id.authorization_id + if not auth_id_val: + raise ValueError("Authorization ID is required") + scopes_val = " ".join(auth_response_or_id.scopes) if auth_response_or_id.scopes else NOT_GIVEN + auth_response = auth_response_or_id + else: + auth_id_val = auth_response_or_id + scopes_val = " ".join(scopes) if scopes else NOT_GIVEN + auth_response = AuthorizationResponse() + + while auth_response.status != "completed": + auth_response = await self.status( + authorization_id=auth_id_val, + scopes=scopes_val, + wait=_DEFAULT_LONGPOLL_WAIT_TIME, + ) + return auth_response + class AuthResourceWithRawResponse: def __init__(self, auth: AuthResource) -> None: diff --git a/tests/api_resources/test_auth_custom.py b/tests/api_resources/test_auth_custom.py new file mode 100644 index 0000000..1fd73ad --- /dev/null +++ b/tests/api_resources/test_auth_custom.py @@ -0,0 +1,25 @@ +from unittest.mock import AsyncMock + +import pytest + +from arcadepy._client import AsyncArcade +from arcadepy.resources.auth import AsyncAuthResource +from arcadepy.types.shared.authorization_response import AuthorizationResponse + + +@pytest.mark.asyncio +async def test_wait_for_completion_calls_status_with_correct_args(): + client = AsyncArcade(api_key="test") + auth = AsyncAuthResource(client) + auth.status = AsyncMock(return_value=AuthorizationResponse(status="completed")) + + auth_response_or_id = "auth_id" + scopes = ["scope1", "scope2"] + + await auth.wait_for_completion(auth_response_or_id, scopes) + + auth.status.assert_called_with( + authorization_id="auth_id", + scopes="scope1 scope2", + wait=45, + ) From 66606fe18b46808997f48c46336aaf6cbbad4165 Mon Sep 17 00:00:00 2001 From: Stainless Bot Date: Fri, 25 Oct 2024 23:27:12 +0000 Subject: [PATCH 02/11] feat: Another test --- tests/api_resources/test_auth_custom.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/api_resources/test_auth_custom.py b/tests/api_resources/test_auth_custom.py index 1fd73ad..b918009 100644 --- a/tests/api_resources/test_auth_custom.py +++ b/tests/api_resources/test_auth_custom.py @@ -8,7 +8,26 @@ @pytest.mark.asyncio -async def test_wait_for_completion_calls_status_with_correct_args(): +async def test_wait_for_completion_calls_status_from_auth_response(): + client = AsyncArcade(api_key="test") + auth = AsyncAuthResource(client) + auth.status = AsyncMock(return_value=AuthorizationResponse(status="completed")) + + auth_response_or_id = AuthorizationResponse( + status="pending", authorization_id="auth_id123", scopes=["scope1", "scope2"] + ) + + await auth.wait_for_completion(auth_response_or_id) + + auth.status.assert_called_with( + authorization_id="auth_id1234", + scopes="scope1 scope2", + wait=45, + ) + + +@pytest.mark.asyncio +async def test_wait_for_completion_calls_status_with_auth_id(): client = AsyncArcade(api_key="test") auth = AsyncAuthResource(client) auth.status = AsyncMock(return_value=AuthorizationResponse(status="completed")) From 6806ee5957042c292b80fbf7867f2bb275f8a004 Mon Sep 17 00:00:00 2001 From: Stainless Bot Date: Fri, 25 Oct 2024 23:27:13 +0000 Subject: [PATCH 03/11] feat: Fix lint --- src/arcadepy/resources/auth.py | 7 +++++-- tests/api_resources/test_auth_custom.py | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/arcadepy/resources/auth.py b/src/arcadepy/resources/auth.py index f89953b..03dbc10 100644 --- a/src/arcadepy/resources/auth.py +++ b/src/arcadepy/resources/auth.py @@ -187,10 +187,13 @@ def wait_for_completion( auth_response = auth.wait_for_completion(auth_response) ``` """ + auth_id_val: str + scopes_val: str | NotGiven = NOT_GIVEN + if isinstance(auth_response_or_id, AuthorizationResponse): - auth_id_val = auth_response_or_id.authorization_id - if not auth_id_val: + if not auth_response_or_id.authorization_id: raise ValueError("Authorization ID is required") + auth_id_val = auth_response_or_id.authorization_id scopes_val = " ".join(auth_response_or_id.scopes) if auth_response_or_id.scopes else NOT_GIVEN auth_response = auth_response_or_id else: diff --git a/tests/api_resources/test_auth_custom.py b/tests/api_resources/test_auth_custom.py index b918009..414fe63 100644 --- a/tests/api_resources/test_auth_custom.py +++ b/tests/api_resources/test_auth_custom.py @@ -8,7 +8,7 @@ @pytest.mark.asyncio -async def test_wait_for_completion_calls_status_from_auth_response(): +async def test_wait_for_completion_calls_status_from_auth_response() -> None: client = AsyncArcade(api_key="test") auth = AsyncAuthResource(client) auth.status = AsyncMock(return_value=AuthorizationResponse(status="completed")) @@ -27,7 +27,7 @@ async def test_wait_for_completion_calls_status_from_auth_response(): @pytest.mark.asyncio -async def test_wait_for_completion_calls_status_with_auth_id(): +async def test_wait_for_completion_calls_status_with_auth_id() -> None: client = AsyncArcade(api_key="test") auth = AsyncAuthResource(client) auth.status = AsyncMock(return_value=AuthorizationResponse(status="completed")) From 324505c86e54e7cfb95bcf6e72e881c291488c9b Mon Sep 17 00:00:00 2001 From: Stainless Bot Date: Fri, 25 Oct 2024 23:27:13 +0000 Subject: [PATCH 04/11] feat: Fix more lint --- src/arcadepy/resources/auth.py | 7 +++++-- tests/api_resources/test_auth_custom.py | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/arcadepy/resources/auth.py b/src/arcadepy/resources/auth.py index 03dbc10..d0a35eb 100644 --- a/src/arcadepy/resources/auth.py +++ b/src/arcadepy/resources/auth.py @@ -371,10 +371,13 @@ async def wait_for_completion( auth_response = auth.wait_for_completion(auth_response) ``` """ + auth_id_val: str + scopes_val: str | NotGiven = NOT_GIVEN + if isinstance(auth_response_or_id, AuthorizationResponse): - auth_id_val = auth_response_or_id.authorization_id - if not auth_id_val: + if not auth_response_or_id.authorization_id: raise ValueError("Authorization ID is required") + auth_id_val = auth_response_or_id.authorization_id scopes_val = " ".join(auth_response_or_id.scopes) if auth_response_or_id.scopes else NOT_GIVEN auth_response = auth_response_or_id else: diff --git a/tests/api_resources/test_auth_custom.py b/tests/api_resources/test_auth_custom.py index 414fe63..f83f1aa 100644 --- a/tests/api_resources/test_auth_custom.py +++ b/tests/api_resources/test_auth_custom.py @@ -11,7 +11,7 @@ async def test_wait_for_completion_calls_status_from_auth_response() -> None: client = AsyncArcade(api_key="test") auth = AsyncAuthResource(client) - auth.status = AsyncMock(return_value=AuthorizationResponse(status="completed")) + auth.status = AsyncMock(return_value=AuthorizationResponse(status="completed")) # type: ignore auth_response_or_id = AuthorizationResponse( status="pending", authorization_id="auth_id123", scopes=["scope1", "scope2"] @@ -20,7 +20,7 @@ async def test_wait_for_completion_calls_status_from_auth_response() -> None: await auth.wait_for_completion(auth_response_or_id) auth.status.assert_called_with( - authorization_id="auth_id1234", + authorization_id="auth_id123", scopes="scope1 scope2", wait=45, ) From caf98dbe69addd8b1edf3f68b2e06c8407d3fff6 Mon Sep 17 00:00:00 2001 From: Stainless Bot Date: Fri, 25 Oct 2024 23:27:14 +0000 Subject: [PATCH 05/11] feat: More tests --- tests/api_resources/test_auth_custom.py | 57 ++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/tests/api_resources/test_auth_custom.py b/tests/api_resources/test_auth_custom.py index f83f1aa..4e6f266 100644 --- a/tests/api_resources/test_auth_custom.py +++ b/tests/api_resources/test_auth_custom.py @@ -1,14 +1,59 @@ -from unittest.mock import AsyncMock +from typing import List, Union, Optional +from unittest.mock import Mock, AsyncMock import pytest -from arcadepy._client import AsyncArcade -from arcadepy.resources.auth import AsyncAuthResource +from arcadepy._types import NOT_GIVEN, NotGiven +from arcadepy._client import Arcade, AsyncArcade +from arcadepy.resources.auth import AuthResource, AsyncAuthResource from arcadepy.types.shared.authorization_response import AuthorizationResponse +@pytest.mark.parametrize( + "scopes, expected_scopes", + [ + (["scope1"], "scope1"), + (["scope1", "scope2"], "scope1 scope2"), + (None, NOT_GIVEN), + ], +) +def test_wait_for_completion_calls_status_from_auth_response( + scopes: Optional[List[str]], expected_scopes: Union[str, NotGiven] +) -> None: + client = Arcade(api_key="test") + auth = AuthResource(client) + auth.status = Mock(return_value=AuthorizationResponse(status="completed")) # type: ignore + + auth_response_or_id = AuthorizationResponse(status="pending", authorization_id="auth_id123", scopes=scopes) + + auth.wait_for_completion(auth_response_or_id) + + auth.status.assert_called_with( + authorization_id="auth_id123", + scopes=expected_scopes, + wait=45, + ) + + +def test_wait_for_completion_calls_status_with_auth_id() -> None: + client = Arcade(api_key="test") + auth = AuthResource(client) + auth.status = Mock(return_value=AuthorizationResponse(status="completed")) # type: ignore + + auth_response_or_id = "auth_id123" + scopes = ["scope1", "scope2"] + + auth.wait_for_completion(auth_response_or_id, scopes) + + auth.status.assert_called_with( + authorization_id="auth_id123", + scopes="scope1 scope2", + wait=45, + ) + + @pytest.mark.asyncio -async def test_wait_for_completion_calls_status_from_auth_response() -> None: +async def test_async_wait_for_completion_calls_status_from_auth_response() -> None: client = AsyncArcade(api_key="test") auth = AsyncAuthResource(client) auth.status = AsyncMock(return_value=AuthorizationResponse(status="completed")) # type: ignore @@ -27,10 +72,10 @@ async def test_wait_for_completion_calls_status_from_auth_response() -> None: @pytest.mark.asyncio -async def test_wait_for_completion_calls_status_with_auth_id() -> None: +async def test_async_wait_for_completion_calls_status_with_auth_id() -> None: client = AsyncArcade(api_key="test") auth = AsyncAuthResource(client) - auth.status = AsyncMock(return_value=AuthorizationResponse(status="completed")) + auth.status = AsyncMock(return_value=AuthorizationResponse(status="completed")) # type: ignore auth_response_or_id = "auth_id" scopes = ["scope1", "scope2"] From febe917faa6b5ea18393cf48b21b7feeee9bfd31 Mon Sep 17 00:00:00 2001 From: Stainless Bot Date: Fri, 25 Oct 2024 23:27:15 +0000 Subject: [PATCH 06/11] feat: Finish wait_for_authorization tests --- src/arcadepy/resources/auth.py | 8 +-- tests/api_resources/test_auth_custom.py | 77 ++++++++++++++----------- 2 files changed, 47 insertions(+), 38 deletions(-) diff --git a/src/arcadepy/resources/auth.py b/src/arcadepy/resources/auth.py index d0a35eb..3055630 100644 --- a/src/arcadepy/resources/auth.py +++ b/src/arcadepy/resources/auth.py @@ -183,8 +183,8 @@ def wait_for_completion( Waits for the authorization process to complete, for example: ```py - auth_response = auth.start("you@example.com", "github") - auth_response = auth.wait_for_completion(auth_response) + auth_response = client.auth.start("you@example.com", "github") + auth_response = client.auth.wait_for_completion(auth_response) ``` """ auth_id_val: str @@ -367,8 +367,8 @@ async def wait_for_completion( Waits for the authorization process to complete, for example: ```py - auth_response = auth.start("you@example.com", "github") - auth_response = auth.wait_for_completion(auth_response) + auth_response = client.auth.start("you@example.com", "github") + auth_response = client.auth.wait_for_completion(auth_response) ``` """ auth_id_val: str diff --git a/tests/api_resources/test_auth_custom.py b/tests/api_resources/test_auth_custom.py index 4e6f266..4db3237 100644 --- a/tests/api_resources/test_auth_custom.py +++ b/tests/api_resources/test_auth_custom.py @@ -8,8 +8,7 @@ from arcadepy.resources.auth import AuthResource, AsyncAuthResource from arcadepy.types.shared.authorization_response import AuthorizationResponse - -@pytest.mark.parametrize( +parametrize_scopes = pytest.mark.parametrize( "scopes, expected_scopes", [ (["scope1"], "scope1"), @@ -17,16 +16,31 @@ (None, NOT_GIVEN), ], ) -def test_wait_for_completion_calls_status_from_auth_response( - scopes: Optional[List[str]], expected_scopes: Union[str, NotGiven] -) -> None: + + +@pytest.fixture +def sync_auth_resource(): client = Arcade(api_key="test") auth = AuthResource(client) + return auth + + +@pytest.fixture +def async_auth_resource(): + client = AsyncArcade(api_key="test") + auth = AsyncAuthResource(client) + return auth + + +@parametrize_scopes +def test_wait_for_completion_calls_status_from_auth_response( + auth: AuthResource, scopes: Optional[List[str]], expected_scopes: Union[str, NotGiven] +) -> None: auth.status = Mock(return_value=AuthorizationResponse(status="completed")) # type: ignore - auth_response_or_id = AuthorizationResponse(status="pending", authorization_id="auth_id123", scopes=scopes) + auth_response = AuthorizationResponse(status="pending", authorization_id="auth_id123", scopes=scopes) - auth.wait_for_completion(auth_response_or_id) + auth.wait_for_completion(auth_response) auth.status.assert_called_with( authorization_id="auth_id123", @@ -35,55 +49,50 @@ def test_wait_for_completion_calls_status_from_auth_response( ) -def test_wait_for_completion_calls_status_with_auth_id() -> None: - client = Arcade(api_key="test") - auth = AuthResource(client) +@parametrize_scopes +def test_wait_for_completion_calls_status_with_auth_id( + auth: AuthResource, scopes: Optional[List[str]], expected_scopes: Union[str, NotGiven] +) -> None: auth.status = Mock(return_value=AuthorizationResponse(status="completed")) # type: ignore - auth_response_or_id = "auth_id123" - scopes = ["scope1", "scope2"] - - auth.wait_for_completion(auth_response_or_id, scopes) + auth.wait_for_completion("auth_id456", scopes) auth.status.assert_called_with( - authorization_id="auth_id123", - scopes="scope1 scope2", + authorization_id="auth_id456", + scopes=expected_scopes, wait=45, ) @pytest.mark.asyncio -async def test_async_wait_for_completion_calls_status_from_auth_response() -> None: - client = AsyncArcade(api_key="test") - auth = AsyncAuthResource(client) +@parametrize_scopes +async def test_async_wait_for_completion_calls_status_from_auth_response( + auth: AsyncAuthResource, scopes: Optional[List[str]], expected_scopes: Union[str, NotGiven] +) -> None: auth.status = AsyncMock(return_value=AuthorizationResponse(status="completed")) # type: ignore - auth_response_or_id = AuthorizationResponse( - status="pending", authorization_id="auth_id123", scopes=["scope1", "scope2"] - ) + auth_response = AuthorizationResponse(status="pending", authorization_id="auth_id789", scopes=scopes) - await auth.wait_for_completion(auth_response_or_id) + await auth.wait_for_completion(auth_response) auth.status.assert_called_with( - authorization_id="auth_id123", - scopes="scope1 scope2", + authorization_id="auth_id789", + scopes=expected_scopes, wait=45, ) @pytest.mark.asyncio -async def test_async_wait_for_completion_calls_status_with_auth_id() -> None: - client = AsyncArcade(api_key="test") - auth = AsyncAuthResource(client) +@parametrize_scopes +async def test_async_wait_for_completion_calls_status_with_auth_id( + auth: AsyncAuthResource, scopes: Optional[List[str]], expected_scopes: Union[str, NotGiven] +) -> None: auth.status = AsyncMock(return_value=AuthorizationResponse(status="completed")) # type: ignore - auth_response_or_id = "auth_id" - scopes = ["scope1", "scope2"] - - await auth.wait_for_completion(auth_response_or_id, scopes) + await auth.wait_for_completion("auth_id321", scopes) auth.status.assert_called_with( - authorization_id="auth_id", - scopes="scope1 scope2", + authorization_id="auth_id321", + scopes=expected_scopes, wait=45, ) From 1af15848b3fdc758818d1a7fcd692d8df68913cb Mon Sep 17 00:00:00 2001 From: Stainless Bot Date: Fri, 25 Oct 2024 23:27:15 +0000 Subject: [PATCH 07/11] feat: Fix tests --- tests/api_resources/test_auth_custom.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/api_resources/test_auth_custom.py b/tests/api_resources/test_auth_custom.py index 4db3237..f0f5a7e 100644 --- a/tests/api_resources/test_auth_custom.py +++ b/tests/api_resources/test_auth_custom.py @@ -19,14 +19,14 @@ @pytest.fixture -def sync_auth_resource(): +def sync_auth_resource() -> AuthResource: client = Arcade(api_key="test") auth = AuthResource(client) return auth @pytest.fixture -def async_auth_resource(): +def async_auth_resource() -> AsyncAuthResource: client = AsyncArcade(api_key="test") auth = AsyncAuthResource(client) return auth @@ -34,8 +34,9 @@ def async_auth_resource(): @parametrize_scopes def test_wait_for_completion_calls_status_from_auth_response( - auth: AuthResource, scopes: Optional[List[str]], expected_scopes: Union[str, NotGiven] + sync_auth_resource: AuthResource, scopes: Optional[List[str]], expected_scopes: Union[str, NotGiven] ) -> None: + auth = sync_auth_resource auth.status = Mock(return_value=AuthorizationResponse(status="completed")) # type: ignore auth_response = AuthorizationResponse(status="pending", authorization_id="auth_id123", scopes=scopes) @@ -51,8 +52,9 @@ def test_wait_for_completion_calls_status_from_auth_response( @parametrize_scopes def test_wait_for_completion_calls_status_with_auth_id( - auth: AuthResource, scopes: Optional[List[str]], expected_scopes: Union[str, NotGiven] + sync_auth_resource: AuthResource, scopes: Optional[List[str]], expected_scopes: Union[str, NotGiven] ) -> None: + auth = sync_auth_resource auth.status = Mock(return_value=AuthorizationResponse(status="completed")) # type: ignore auth.wait_for_completion("auth_id456", scopes) @@ -67,8 +69,9 @@ def test_wait_for_completion_calls_status_with_auth_id( @pytest.mark.asyncio @parametrize_scopes async def test_async_wait_for_completion_calls_status_from_auth_response( - auth: AsyncAuthResource, scopes: Optional[List[str]], expected_scopes: Union[str, NotGiven] + async_auth_resource: AsyncAuthResource, scopes: Optional[List[str]], expected_scopes: Union[str, NotGiven] ) -> None: + auth = async_auth_resource auth.status = AsyncMock(return_value=AuthorizationResponse(status="completed")) # type: ignore auth_response = AuthorizationResponse(status="pending", authorization_id="auth_id789", scopes=scopes) @@ -85,8 +88,9 @@ async def test_async_wait_for_completion_calls_status_from_auth_response( @pytest.mark.asyncio @parametrize_scopes async def test_async_wait_for_completion_calls_status_with_auth_id( - auth: AsyncAuthResource, scopes: Optional[List[str]], expected_scopes: Union[str, NotGiven] + async_auth_resource: AsyncAuthResource, scopes: Optional[List[str]], expected_scopes: Union[str, NotGiven] ) -> None: + auth = async_auth_resource auth.status = AsyncMock(return_value=AuthorizationResponse(status="completed")) # type: ignore await auth.wait_for_completion("auth_id321", scopes) From 7fa68e5bac09a5404f201d89141876d5d0de9562 Mon Sep 17 00:00:00 2001 From: Stainless Bot Date: Fri, 25 Oct 2024 23:27:16 +0000 Subject: [PATCH 08/11] feat: Finish tests --- tests/api_resources/test_auth_start.py | 96 +++++++++++++++++++ ...{test_auth_custom.py => test_auth_wait.py} | 19 ++++ 2 files changed, 115 insertions(+) create mode 100644 tests/api_resources/test_auth_start.py rename tests/api_resources/{test_auth_custom.py => test_auth_wait.py} (79%) diff --git a/tests/api_resources/test_auth_start.py b/tests/api_resources/test_auth_start.py new file mode 100644 index 0000000..e4cb8b5 --- /dev/null +++ b/tests/api_resources/test_auth_start.py @@ -0,0 +1,96 @@ +from typing import List, Optional +from unittest.mock import Mock, AsyncMock + +import pytest + +from arcadepy._client import Arcade, AsyncArcade +from arcadepy.resources.auth import AuthResource, AsyncAuthResource +from arcadepy.types.auth_authorize_params import AuthRequirement, AuthRequirementOauth2 +from arcadepy.types.shared.authorization_response import AuthorizationResponse + +parametrize_provider_type = pytest.mark.parametrize( + "provider_type, expected_provider_type", + [ + (None, "oauth2"), + ("oauth2", "oauth2"), + ("custom_type", "custom_type"), + ], +) + +parametrize_scopes = pytest.mark.parametrize( + "scopes, expected_scopes", + [ + (["scope1"], "scope1"), + (["scope1", "scope2"], "scope1 scope2"), + (None, []), + ], +) + + +@pytest.fixture +def sync_auth_resource() -> AuthResource: + client = Arcade(api_key="test") + auth = AuthResource(client) + return auth + + +@pytest.fixture +def async_auth_resource() -> AsyncAuthResource: + client = AsyncArcade(api_key="test") + auth = AsyncAuthResource(client) + return auth + + +@parametrize_provider_type +@parametrize_scopes +def test_start_calls_authorize_with_correct_params( + sync_auth_resource: AuthResource, + provider_type: Optional[str], + expected_provider_type: str, + scopes: Optional[List[str]], + expected_scopes: List[str], +) -> None: + auth = sync_auth_resource + auth.authorize = Mock(return_value=AuthorizationResponse(status="pending")) # type: ignore + + user_id = "user_id" + provider = "github" + + auth.start(user_id, provider, provider_type, scopes) + + auth.authorize.assert_called_with( + auth_requirement=AuthRequirement( + provider_id=provider, + provider_type=expected_provider_type, + oauth2=AuthRequirementOauth2(scopes=expected_scopes), + ), + user_id=user_id, + ) + + +@pytest.mark.asyncio +@parametrize_provider_type +@parametrize_scopes +async def test_async_start_calls_authorize_with_correct_params( + async_auth_resource: AsyncAuthResource, + provider_type: Optional[str], + expected_provider_type: str, + scopes: Optional[List[str]], + expected_scopes: List[str], +) -> None: + auth = async_auth_resource + auth.authorize = AsyncMock(return_value=AuthorizationResponse(status="pending")) # type: ignore + + user_id = "user_id" + provider = "github" + + await auth.start(user_id, provider, provider_type, scopes) + + auth.authorize.assert_called_with( + auth_requirement=AuthRequirement( + provider_id=provider, + provider_type=expected_provider_type, + oauth2=AuthRequirementOauth2(scopes=expected_scopes), + ), + user_id=user_id, + ) diff --git a/tests/api_resources/test_auth_custom.py b/tests/api_resources/test_auth_wait.py similarity index 79% rename from tests/api_resources/test_auth_custom.py rename to tests/api_resources/test_auth_wait.py index f0f5a7e..482ad8f 100644 --- a/tests/api_resources/test_auth_custom.py +++ b/tests/api_resources/test_auth_wait.py @@ -50,6 +50,14 @@ def test_wait_for_completion_calls_status_from_auth_response( ) +def test_wait_for_completion_raises_value_error_for_empty_authorization_id(sync_auth_resource: AuthResource) -> None: + auth = sync_auth_resource + auth_response = AuthorizationResponse(status="pending", authorization_id="", scopes=["scope1"]) + + with pytest.raises(ValueError, match="Authorization ID is required"): + auth.wait_for_completion(auth_response) + + @parametrize_scopes def test_wait_for_completion_calls_status_with_auth_id( sync_auth_resource: AuthResource, scopes: Optional[List[str]], expected_scopes: Union[str, NotGiven] @@ -85,6 +93,17 @@ async def test_async_wait_for_completion_calls_status_from_auth_response( ) +@pytest.mark.asyncio +async def test_async_wait_for_completion_raises_value_error_for_empty_authorization_id( + async_auth_resource: AsyncAuthResource, +) -> None: + auth = async_auth_resource + auth_response = AuthorizationResponse(status="pending", authorization_id="", scopes=["scope1"]) + + with pytest.raises(ValueError, match="Authorization ID is required"): + await auth.wait_for_completion(auth_response) + + @pytest.mark.asyncio @parametrize_scopes async def test_async_wait_for_completion_calls_status_with_auth_id( From 07d0cf8eee6d1aa527fb793bb09efacadbcfa7d2 Mon Sep 17 00:00:00 2001 From: Stainless Bot Date: Fri, 25 Oct 2024 23:27:16 +0000 Subject: [PATCH 09/11] feat: Fix test --- tests/api_resources/test_auth_start.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/api_resources/test_auth_start.py b/tests/api_resources/test_auth_start.py index e4cb8b5..61df07b 100644 --- a/tests/api_resources/test_auth_start.py +++ b/tests/api_resources/test_auth_start.py @@ -20,8 +20,7 @@ parametrize_scopes = pytest.mark.parametrize( "scopes, expected_scopes", [ - (["scope1"], "scope1"), - (["scope1", "scope2"], "scope1 scope2"), + (["scope1", "scope2"], ["scope1", "scope2"]), (None, []), ], ) From 32a92bc92611a09078db8ed3c99e884fa8e72ba9 Mon Sep 17 00:00:00 2001 From: Stainless Bot Date: Fri, 25 Oct 2024 23:27:17 +0000 Subject: [PATCH 10/11] feat: Better interface for optional params --- src/arcadepy/resources/auth.py | 2 ++ tests/api_resources/test_auth_start.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/arcadepy/resources/auth.py b/src/arcadepy/resources/auth.py index 3055630..fc9c37b 100644 --- a/src/arcadepy/resources/auth.py +++ b/src/arcadepy/resources/auth.py @@ -96,6 +96,7 @@ def start( self, user_id: str, provider: str, + *, provider_type: str | None = "oauth2", scopes: list[str] | None = None, ) -> AuthorizationResponse: @@ -280,6 +281,7 @@ async def start( self, user_id: str, provider: str, + *, provider_type: str | None = "oauth2", scopes: list[str] | None = None, ) -> AuthorizationResponse: diff --git a/tests/api_resources/test_auth_start.py b/tests/api_resources/test_auth_start.py index 61df07b..e44ec8d 100644 --- a/tests/api_resources/test_auth_start.py +++ b/tests/api_resources/test_auth_start.py @@ -55,7 +55,7 @@ def test_start_calls_authorize_with_correct_params( user_id = "user_id" provider = "github" - auth.start(user_id, provider, provider_type, scopes) + auth.start(user_id, provider, provider_type=provider_type, scopes=scopes) auth.authorize.assert_called_with( auth_requirement=AuthRequirement( @@ -83,7 +83,7 @@ async def test_async_start_calls_authorize_with_correct_params( user_id = "user_id" provider = "github" - await auth.start(user_id, provider, provider_type, scopes) + await auth.start(user_id, provider, provider_type=provider_type, scopes=scopes) auth.authorize.assert_called_with( auth_requirement=AuthRequirement( From b074087578b1cfd8d5e5074c4d2b66e8eec5a1b3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 25 Oct 2024 23:56:33 +0000 Subject: [PATCH 11/11] release: 0.1.2 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 17 +++++++++++++++++ pyproject.toml | 2 +- src/arcadepy/_version.py | 2 +- 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 5547f83..cda9cbd 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.1" + ".": "0.1.2" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a97690a..71f24e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## 0.1.2 (2024-10-25) + +Full Changelog: [v0.1.1...v0.1.2](https://github.com/ArcadeAI/arcade-py/compare/v0.1.1...v0.1.2) + +### Features + +* Add auth.start and auth.wait_for_completion sugar methods ([e0b64a0](https://github.com/ArcadeAI/arcade-py/commit/e0b64a081ae91e8a560460ced6fe1e0010333987)) +* Another test ([66606fe](https://github.com/ArcadeAI/arcade-py/commit/66606fe18b46808997f48c46336aaf6cbbad4165)) +* Better interface for optional params ([32a92bc](https://github.com/ArcadeAI/arcade-py/commit/32a92bc92611a09078db8ed3c99e884fa8e72ba9)) +* Finish tests ([7fa68e5](https://github.com/ArcadeAI/arcade-py/commit/7fa68e5bac09a5404f201d89141876d5d0de9562)) +* Finish wait_for_authorization tests ([febe917](https://github.com/ArcadeAI/arcade-py/commit/febe917faa6b5ea18393cf48b21b7feeee9bfd31)) +* Fix lint ([6806ee5](https://github.com/ArcadeAI/arcade-py/commit/6806ee5957042c292b80fbf7867f2bb275f8a004)) +* Fix more lint ([324505c](https://github.com/ArcadeAI/arcade-py/commit/324505c86e54e7cfb95bcf6e72e881c291488c9b)) +* Fix test ([07d0cf8](https://github.com/ArcadeAI/arcade-py/commit/07d0cf8eee6d1aa527fb793bb09efacadbcfa7d2)) +* Fix tests ([1af1584](https://github.com/ArcadeAI/arcade-py/commit/1af15848b3fdc758818d1a7fcd692d8df68913cb)) +* More tests ([caf98db](https://github.com/ArcadeAI/arcade-py/commit/caf98dbe69addd8b1edf3f68b2e06c8407d3fff6)) + ## 0.1.1 (2024-10-24) Full Changelog: [v0.1.0...v0.1.1](https://github.com/ArcadeAI/arcade-py/compare/v0.1.0...v0.1.1) diff --git a/pyproject.toml b/pyproject.toml index 4a2b22c..a9db9c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "arcadepy" -version = "0.1.1" +version = "0.1.2" description = "The official Python library for the Arcade API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/arcadepy/_version.py b/src/arcadepy/_version.py index 5562518..98b28f2 100644 --- a/src/arcadepy/_version.py +++ b/src/arcadepy/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "arcadepy" -__version__ = "0.1.1" # x-release-please-version +__version__ = "0.1.2" # x-release-please-version