Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions examples/async/oauth_grants_async.py
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())
40 changes: 40 additions & 0 deletions examples/oauth_grants.py
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']}")
5 changes: 5 additions & 0 deletions resend/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
from .http_client_requests import RequestsClient
from .logs._log import Log
from .logs._logs import Logs
from .oauth_grants._oauth_grant import OAuthGrant, OAuthGrantClient
from .oauth_grants._oauth_grants import OAuthGrants
from .request import Request
from .segments._segment import Segment
from .segments._segments import Segments
Expand Down Expand Up @@ -94,6 +96,7 @@
"Webhooks",
"Topics",
"Logs",
"OAuthGrants",
# Types
"Audience",
"Automation",
Expand Down Expand Up @@ -139,6 +142,8 @@
"WebhookStatus",
"VerifyWebhookOptions",
"Topic",
"OAuthGrant",
"OAuthGrantClient",
"BatchValidationError",
"ReceivedEmail",
"EmailAttachment",
Expand Down
4 changes: 4 additions & 0 deletions resend/oauth_grants/__init__.py
Comment thread
dielduarte marked this conversation as resolved.
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"]
45 changes: 45 additions & 0 deletions resend/oauth_grants/_oauth_grant.py
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
"""
165 changes: 165 additions & 0 deletions resend/oauth_grants/_oauth_grants.py
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 added resend/oauth_grants/py.typed
Empty file.
2 changes: 1 addition & 1 deletion resend/version.py
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:
Expand Down
74 changes: 74 additions & 0 deletions tests/oauth_grants_async_test.py
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")
Loading
Loading