-
Notifications
You must be signed in to change notification settings - Fork 16
feat: add OAuth grants management (list + revoke) #233
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
dielduarte
wants to merge
4
commits into
main
Choose a base branch
from
feat/oauth-grants
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+529
−1
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
664af7a
feat: add OAuth grants management (list + revoke)
dielduarte cf852ab
test: rename oauth grant tests to drop 'should' per project convention
dielduarte 734b2d3
refactor: re-export OAuth grant symbols from package __init__
dielduarte 0cbe1c6
docs: add OAuth grants example files
dielduarte File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import asyncio | ||
| import os | ||
|
|
||
| import resend | ||
|
|
||
| if not os.environ["RESEND_API_KEY"]: | ||
| raise EnvironmentError("RESEND_API_KEY is missing") | ||
|
|
||
|
|
||
| async def main() -> None: | ||
| grants: resend.OAuthGrants.ListResponse = await resend.OAuthGrants.list_async() | ||
| print(f"Found {len(grants['data'])} OAuth grants") | ||
| for grant in grants["data"]: | ||
| print(grant["id"]) | ||
| print(grant["client_id"]) | ||
| print(grant["scopes"]) | ||
| print(grant["created_at"]) | ||
| print(grant["client"]["name"]) | ||
|
|
||
| if grants["data"]: | ||
| revoked = await resend.OAuthGrants.revoke_async( | ||
| oauth_grant_id=grants["data"][0]["id"] | ||
| ) | ||
| print(f"Revoked OAuth grant: {revoked['id']}") | ||
| print(f"Revoked at: {revoked['revoked_at']}") | ||
| print(f"Revoked reason: {revoked['revoked_reason']}") | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| asyncio.run(main()) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import os | ||
|
|
||
| import resend | ||
|
|
||
| if not os.environ["RESEND_API_KEY"]: | ||
| raise EnvironmentError("RESEND_API_KEY is missing") | ||
|
|
||
| grants: resend.OAuthGrants.ListResponse = resend.OAuthGrants.list() | ||
| print(f"Found {len(grants['data'])} OAuth grants") | ||
| print(f"Has more grants: {grants['has_more']}") | ||
| if not grants["data"]: | ||
| print("No OAuth grants found") | ||
| for grant in grants["data"]: | ||
| print(grant["id"]) | ||
| print(grant["client_id"]) | ||
| print(grant["scopes"]) | ||
| print(grant["created_at"]) | ||
| print(grant["client"]["name"]) | ||
|
|
||
| print("\n--- Using pagination parameters ---") | ||
| if grants["data"]: | ||
| paginated_params: resend.OAuthGrants.ListParams = { | ||
| "limit": 5, | ||
| "after": grants["data"][0]["id"], | ||
| } | ||
| paginated_grants: resend.OAuthGrants.ListResponse = resend.OAuthGrants.list( | ||
| params=paginated_params | ||
| ) | ||
| print(f"Retrieved {len(paginated_grants['data'])} grants with pagination") | ||
| print(f"Has more grants: {paginated_grants['has_more']}") | ||
| else: | ||
| print("No OAuth grants available for pagination example") | ||
|
|
||
| if grants["data"]: | ||
| revoked: resend.OAuthGrants.RevokeOAuthGrantResponse = resend.OAuthGrants.revoke( | ||
| oauth_grant_id=grants["data"][0]["id"] | ||
| ) | ||
| print(f"Revoked OAuth grant: {revoked['id']}") | ||
| print(f"Revoked at: {revoked['revoked_at']}") | ||
| print(f"Revoked reason: {revoked['revoked_reason']}") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| from resend.oauth_grants._oauth_grant import OAuthGrant, OAuthGrantClient | ||
| from resend.oauth_grants._oauth_grants import OAuthGrants | ||
|
|
||
| __all__ = ["OAuthGrant", "OAuthGrantClient", "OAuthGrants"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| from typing import List, Optional | ||
|
|
||
| from typing_extensions import TypedDict | ||
|
|
||
|
|
||
| class OAuthGrantClient(TypedDict): | ||
| name: str | ||
| """ | ||
| The name of the OAuth client the grant was issued to | ||
| """ | ||
| logo_uri: Optional[str] | ||
| """ | ||
| The URL of the OAuth client's logo, or None if not set | ||
| """ | ||
|
|
||
|
|
||
| class OAuthGrant(TypedDict): | ||
| id: str | ||
| """ | ||
| The OAuth grant ID | ||
| """ | ||
| client_id: str | ||
| """ | ||
| The ID of the OAuth client the grant was issued to | ||
| """ | ||
| scopes: List[str] | ||
| """ | ||
| The scopes granted to the OAuth client | ||
| """ | ||
| created_at: str | ||
| """ | ||
| The date and time the grant was created | ||
| """ | ||
| revoked_at: Optional[str] | ||
| """ | ||
| The date and time the grant was revoked, or None if it is still active | ||
| """ | ||
| revoked_reason: Optional[str] | ||
| """ | ||
| The reason the grant was revoked, or None if it is still active | ||
| """ | ||
| client: OAuthGrantClient | ||
| """ | ||
| The OAuth client the grant was issued to | ||
| """ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,165 @@ | ||
| from typing import Any, Dict, List, Optional, cast | ||
|
|
||
| from typing_extensions import NotRequired, TypedDict | ||
|
|
||
| from resend import request | ||
| from resend._base_response import BaseResponse | ||
| from resend.oauth_grants._oauth_grant import OAuthGrant | ||
| from resend.pagination_helper import PaginationHelper | ||
|
|
||
| # Async imports (optional - only available with pip install resend[async]) | ||
| try: | ||
| from resend.async_request import AsyncRequest | ||
| except ImportError: | ||
| pass | ||
|
|
||
|
|
||
| class OAuthGrants: | ||
|
|
||
| class ListResponse(BaseResponse): | ||
| """ | ||
| ListResponse type that wraps a list of OAuth grant objects with pagination metadata | ||
|
|
||
| Attributes: | ||
| object (str): The object type, always "list" | ||
| data (List[OAuthGrant]): A list of OAuth grant objects | ||
| has_more (bool): Whether there are more results available | ||
| """ | ||
|
|
||
| object: str | ||
| """ | ||
| The object type, always "list" | ||
| """ | ||
| data: List[OAuthGrant] | ||
| """ | ||
| A list of OAuth grant objects | ||
| """ | ||
| has_more: bool | ||
| """ | ||
| Whether there are more results available for pagination | ||
| """ | ||
|
|
||
| class RevokeOAuthGrantResponse(BaseResponse): | ||
| """ | ||
| RevokeOAuthGrantResponse is the type that wraps the revoked OAuth grant response. | ||
|
|
||
| Attributes: | ||
| object (str): The object type, always "oauth_grant" | ||
| id (str): The ID of the revoked OAuth grant | ||
| revoked_at (str): The date and time the grant was revoked | ||
| revoked_reason (str): The reason the grant was revoked | ||
| """ | ||
|
|
||
| object: str | ||
| """ | ||
| The object type, always "oauth_grant" | ||
| """ | ||
| id: str | ||
| """ | ||
| The ID of the revoked OAuth grant | ||
| """ | ||
| revoked_at: str | ||
| """ | ||
| The date and time the grant was revoked | ||
| """ | ||
| revoked_reason: str | ||
| """ | ||
| The reason the grant was revoked | ||
| """ | ||
|
|
||
| class ListParams(TypedDict): | ||
| limit: NotRequired[int] | ||
| """ | ||
| Number of OAuth grants to retrieve. Maximum is 100, and minimum is 1. | ||
| """ | ||
| after: NotRequired[str] | ||
| """ | ||
| The ID after which we'll retrieve more OAuth grants (for pagination). | ||
| This ID will not be included in the returned list. | ||
| Cannot be used with the before parameter. | ||
| """ | ||
| before: NotRequired[str] | ||
| """ | ||
| The ID before which we'll retrieve more OAuth grants (for pagination). | ||
| This ID will not be included in the returned list. | ||
| Cannot be used with the after parameter. | ||
| """ | ||
|
|
||
| @classmethod | ||
| def list(cls, params: Optional[ListParams] = None) -> ListResponse: | ||
| """ | ||
| Retrieve a list of OAuth grants for the authenticated user. | ||
| see more: https://resend.com/docs/api-reference/oauth/list-grants | ||
|
|
||
| Args: | ||
| params (Optional[ListParams]): Optional pagination parameters | ||
| - limit: Number of OAuth grants to retrieve (max 100, min 1) | ||
| - after: ID after which to retrieve more OAuth grants | ||
| - before: ID before which to retrieve more OAuth grants | ||
|
|
||
| Returns: | ||
| ListResponse: A list of OAuth grant objects | ||
| """ | ||
| base_path = "/oauth/grants" | ||
| query_params = cast(Dict[Any, Any], params) if params else None | ||
| path = PaginationHelper.build_paginated_path(base_path, query_params) | ||
| resp = request.Request[OAuthGrants.ListResponse]( | ||
| path=path, params={}, verb="get" | ||
| ).perform_with_content() | ||
| return resp | ||
|
|
||
| @classmethod | ||
| def revoke(cls, oauth_grant_id: str) -> RevokeOAuthGrantResponse: | ||
| """ | ||
| Revoke an existing OAuth grant. | ||
| see more: https://resend.com/docs/api-reference/oauth/revoke-grant | ||
|
|
||
| Args: | ||
| oauth_grant_id (str): The ID of the OAuth grant to revoke | ||
|
|
||
| Returns: | ||
| RevokeOAuthGrantResponse: The revoked OAuth grant response | ||
| """ | ||
| path = f"/oauth/grants/{oauth_grant_id}" | ||
| resp = request.Request[OAuthGrants.RevokeOAuthGrantResponse]( | ||
| path=path, params={}, verb="delete" | ||
| ).perform_with_content() | ||
| return resp | ||
|
|
||
| @classmethod | ||
| async def list_async(cls, params: Optional[ListParams] = None) -> ListResponse: | ||
| """ | ||
| Retrieve a list of OAuth grants for the authenticated user (async). | ||
| see more: https://resend.com/docs/api-reference/oauth/list-grants | ||
|
|
||
| Args: | ||
| params (Optional[ListParams]): Optional pagination parameters | ||
|
|
||
| Returns: | ||
| ListResponse: A list of OAuth grant objects | ||
| """ | ||
| base_path = "/oauth/grants" | ||
| query_params = cast(Dict[Any, Any], params) if params else None | ||
| path = PaginationHelper.build_paginated_path(base_path, query_params) | ||
| resp = await AsyncRequest[OAuthGrants.ListResponse]( | ||
| path=path, params={}, verb="get" | ||
| ).perform_with_content() | ||
| return resp | ||
|
|
||
| @classmethod | ||
| async def revoke_async(cls, oauth_grant_id: str) -> RevokeOAuthGrantResponse: | ||
| """ | ||
| Revoke an existing OAuth grant (async). | ||
| see more: https://resend.com/docs/api-reference/oauth/revoke-grant | ||
|
|
||
| Args: | ||
| oauth_grant_id (str): The ID of the OAuth grant to revoke | ||
|
|
||
| Returns: | ||
| RevokeOAuthGrantResponse: The revoked OAuth grant response | ||
| """ | ||
| path = f"/oauth/grants/{oauth_grant_id}" | ||
| resp = await AsyncRequest[OAuthGrants.RevokeOAuthGrantResponse]( | ||
| path=path, params={}, verb="delete" | ||
| ).perform_with_content() | ||
| return resp |
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| __version__ = "2.32.2" | ||
| __version__ = "2.33.0" | ||
|
|
||
|
|
||
| def get_version() -> str: | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| import pytest | ||
|
|
||
| import resend | ||
| from resend.exceptions import NoContentError | ||
| from tests.conftest import AsyncResendBaseTest | ||
|
|
||
| # flake8: noqa | ||
|
|
||
| pytestmark = pytest.mark.asyncio | ||
|
|
||
|
|
||
| class TestResendOAuthGrantsAsync(AsyncResendBaseTest): | ||
| async def test_oauth_grants_list_async(self) -> None: | ||
| self.set_mock_json( | ||
| { | ||
| "object": "list", | ||
| "has_more": False, | ||
| "data": [ | ||
| { | ||
| "id": "650e8400-e29b-41d4-a716-446655440001", | ||
| "client_id": "430eed87-632a-4ea6-90db-0aace67ec228", | ||
| "scopes": ["emails:send"], | ||
| "created_at": "2023-06-21T06:10:36.144Z", | ||
| "revoked_at": None, | ||
| "revoked_reason": None, | ||
| "client": { | ||
| "name": "Resend CLI", | ||
| "logo_uri": "https://example.com/logo.png", | ||
| }, | ||
| } | ||
| ], | ||
| } | ||
| ) | ||
|
|
||
| grants: resend.OAuthGrants.ListResponse = await resend.OAuthGrants.list_async() | ||
| for grant in grants["data"]: | ||
| assert grant["id"] == "650e8400-e29b-41d4-a716-446655440001" | ||
| assert grant["client_id"] == "430eed87-632a-4ea6-90db-0aace67ec228" | ||
| assert grant["scopes"] == ["emails:send"] | ||
| assert grant["client"]["name"] == "Resend CLI" | ||
|
|
||
| async def test_list_oauth_grants_async_returns_no_content_error( | ||
| self, | ||
| ) -> None: | ||
| self.set_mock_json(None) | ||
| with pytest.raises(NoContentError): | ||
| _ = await resend.OAuthGrants.list_async() | ||
|
|
||
| async def test_oauth_grants_revoke_async(self) -> None: | ||
| self.set_mock_json( | ||
| { | ||
| "object": "oauth_grant", | ||
| "id": "650e8400-e29b-41d4-a716-446655440001", | ||
| "revoked_at": "2023-06-22T06:10:36.144Z", | ||
| "revoked_reason": "revoked_from_api", | ||
| } | ||
| ) | ||
|
|
||
| revoked: resend.OAuthGrants.RevokeOAuthGrantResponse = ( | ||
| await resend.OAuthGrants.revoke_async( | ||
| oauth_grant_id="650e8400-e29b-41d4-a716-446655440001", | ||
| ) | ||
| ) | ||
| assert revoked["object"] == "oauth_grant" | ||
| assert revoked["id"] == "650e8400-e29b-41d4-a716-446655440001" | ||
| assert revoked["revoked_at"] == "2023-06-22T06:10:36.144Z" | ||
| assert revoked["revoked_reason"] == "revoked_from_api" | ||
|
|
||
| async def test_revoke_oauth_grant_async_returns_no_content_error( | ||
| self, | ||
| ) -> None: | ||
| self.set_mock_json(None) | ||
| with pytest.raises(NoContentError): | ||
| _ = await resend.OAuthGrants.revoke_async(oauth_grant_id="grant-1") |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.