Skip to content

Commit 664af7a

Browse files
committed
feat: add OAuth grants management (list + revoke)
1 parent 0dad9d6 commit 664af7a

8 files changed

Lines changed: 455 additions & 1 deletion

File tree

resend/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@
4747
from .http_client_requests import RequestsClient
4848
from .logs._log import Log
4949
from .logs._logs import Logs
50+
from .oauth_grants._oauth_grant import OAuthGrant, OAuthGrantClient
51+
from .oauth_grants._oauth_grants import OAuthGrants
5052
from .request import Request
5153
from .segments._segment import Segment
5254
from .segments._segments import Segments
@@ -94,6 +96,7 @@
9496
"Webhooks",
9597
"Topics",
9698
"Logs",
99+
"OAuthGrants",
97100
# Types
98101
"Audience",
99102
"Automation",
@@ -139,6 +142,8 @@
139142
"WebhookStatus",
140143
"VerifyWebhookOptions",
141144
"Topic",
145+
"OAuthGrant",
146+
"OAuthGrantClient",
142147
"BatchValidationError",
143148
"ReceivedEmail",
144149
"EmailAttachment",

resend/oauth_grants/__init__.py

Whitespace-only changes.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from typing import List, Optional
2+
3+
from typing_extensions import TypedDict
4+
5+
6+
class OAuthGrantClient(TypedDict):
7+
name: str
8+
"""
9+
The name of the OAuth client the grant was issued to
10+
"""
11+
logo_uri: Optional[str]
12+
"""
13+
The URL of the OAuth client's logo, or None if not set
14+
"""
15+
16+
17+
class OAuthGrant(TypedDict):
18+
id: str
19+
"""
20+
The OAuth grant ID
21+
"""
22+
client_id: str
23+
"""
24+
The ID of the OAuth client the grant was issued to
25+
"""
26+
scopes: List[str]
27+
"""
28+
The scopes granted to the OAuth client
29+
"""
30+
created_at: str
31+
"""
32+
The date and time the grant was created
33+
"""
34+
revoked_at: Optional[str]
35+
"""
36+
The date and time the grant was revoked, or None if it is still active
37+
"""
38+
revoked_reason: Optional[str]
39+
"""
40+
The reason the grant was revoked, or None if it is still active
41+
"""
42+
client: OAuthGrantClient
43+
"""
44+
The OAuth client the grant was issued to
45+
"""
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
from typing import Any, Dict, List, Optional, cast
2+
3+
from typing_extensions import NotRequired, TypedDict
4+
5+
from resend import request
6+
from resend._base_response import BaseResponse
7+
from resend.oauth_grants._oauth_grant import OAuthGrant
8+
from resend.pagination_helper import PaginationHelper
9+
10+
# Async imports (optional - only available with pip install resend[async])
11+
try:
12+
from resend.async_request import AsyncRequest
13+
except ImportError:
14+
pass
15+
16+
17+
class OAuthGrants:
18+
19+
class ListResponse(BaseResponse):
20+
"""
21+
ListResponse type that wraps a list of OAuth grant objects with pagination metadata
22+
23+
Attributes:
24+
object (str): The object type, always "list"
25+
data (List[OAuthGrant]): A list of OAuth grant objects
26+
has_more (bool): Whether there are more results available
27+
"""
28+
29+
object: str
30+
"""
31+
The object type, always "list"
32+
"""
33+
data: List[OAuthGrant]
34+
"""
35+
A list of OAuth grant objects
36+
"""
37+
has_more: bool
38+
"""
39+
Whether there are more results available for pagination
40+
"""
41+
42+
class RevokeOAuthGrantResponse(BaseResponse):
43+
"""
44+
RevokeOAuthGrantResponse is the type that wraps the revoked OAuth grant response.
45+
46+
Attributes:
47+
object (str): The object type, always "oauth_grant"
48+
id (str): The ID of the revoked OAuth grant
49+
revoked_at (str): The date and time the grant was revoked
50+
revoked_reason (str): The reason the grant was revoked
51+
"""
52+
53+
object: str
54+
"""
55+
The object type, always "oauth_grant"
56+
"""
57+
id: str
58+
"""
59+
The ID of the revoked OAuth grant
60+
"""
61+
revoked_at: str
62+
"""
63+
The date and time the grant was revoked
64+
"""
65+
revoked_reason: str
66+
"""
67+
The reason the grant was revoked
68+
"""
69+
70+
class ListParams(TypedDict):
71+
limit: NotRequired[int]
72+
"""
73+
Number of OAuth grants to retrieve. Maximum is 100, and minimum is 1.
74+
"""
75+
after: NotRequired[str]
76+
"""
77+
The ID after which we'll retrieve more OAuth grants (for pagination).
78+
This ID will not be included in the returned list.
79+
Cannot be used with the before parameter.
80+
"""
81+
before: NotRequired[str]
82+
"""
83+
The ID before which we'll retrieve more OAuth grants (for pagination).
84+
This ID will not be included in the returned list.
85+
Cannot be used with the after parameter.
86+
"""
87+
88+
@classmethod
89+
def list(cls, params: Optional[ListParams] = None) -> ListResponse:
90+
"""
91+
Retrieve a list of OAuth grants for the authenticated user.
92+
see more: https://resend.com/docs/api-reference/oauth/list-grants
93+
94+
Args:
95+
params (Optional[ListParams]): Optional pagination parameters
96+
- limit: Number of OAuth grants to retrieve (max 100, min 1)
97+
- after: ID after which to retrieve more OAuth grants
98+
- before: ID before which to retrieve more OAuth grants
99+
100+
Returns:
101+
ListResponse: A list of OAuth grant objects
102+
"""
103+
base_path = "/oauth/grants"
104+
query_params = cast(Dict[Any, Any], params) if params else None
105+
path = PaginationHelper.build_paginated_path(base_path, query_params)
106+
resp = request.Request[OAuthGrants.ListResponse](
107+
path=path, params={}, verb="get"
108+
).perform_with_content()
109+
return resp
110+
111+
@classmethod
112+
def revoke(cls, oauth_grant_id: str) -> RevokeOAuthGrantResponse:
113+
"""
114+
Revoke an existing OAuth grant.
115+
see more: https://resend.com/docs/api-reference/oauth/revoke-grant
116+
117+
Args:
118+
oauth_grant_id (str): The ID of the OAuth grant to revoke
119+
120+
Returns:
121+
RevokeOAuthGrantResponse: The revoked OAuth grant response
122+
"""
123+
path = f"/oauth/grants/{oauth_grant_id}"
124+
resp = request.Request[OAuthGrants.RevokeOAuthGrantResponse](
125+
path=path, params={}, verb="delete"
126+
).perform_with_content()
127+
return resp
128+
129+
@classmethod
130+
async def list_async(cls, params: Optional[ListParams] = None) -> ListResponse:
131+
"""
132+
Retrieve a list of OAuth grants for the authenticated user (async).
133+
see more: https://resend.com/docs/api-reference/oauth/list-grants
134+
135+
Args:
136+
params (Optional[ListParams]): Optional pagination parameters
137+
138+
Returns:
139+
ListResponse: A list of OAuth grant objects
140+
"""
141+
base_path = "/oauth/grants"
142+
query_params = cast(Dict[Any, Any], params) if params else None
143+
path = PaginationHelper.build_paginated_path(base_path, query_params)
144+
resp = await AsyncRequest[OAuthGrants.ListResponse](
145+
path=path, params={}, verb="get"
146+
).perform_with_content()
147+
return resp
148+
149+
@classmethod
150+
async def revoke_async(cls, oauth_grant_id: str) -> RevokeOAuthGrantResponse:
151+
"""
152+
Revoke an existing OAuth grant (async).
153+
see more: https://resend.com/docs/api-reference/oauth/revoke-grant
154+
155+
Args:
156+
oauth_grant_id (str): The ID of the OAuth grant to revoke
157+
158+
Returns:
159+
RevokeOAuthGrantResponse: The revoked OAuth grant response
160+
"""
161+
path = f"/oauth/grants/{oauth_grant_id}"
162+
resp = await AsyncRequest[OAuthGrants.RevokeOAuthGrantResponse](
163+
path=path, params={}, verb="delete"
164+
).perform_with_content()
165+
return resp

resend/oauth_grants/py.typed

Whitespace-only changes.

resend/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "2.32.2"
1+
__version__ = "2.33.0"
22

33

44
def get_version() -> str:

tests/oauth_grants_async_test.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import pytest
2+
3+
import resend
4+
from resend.exceptions import NoContentError
5+
from tests.conftest import AsyncResendBaseTest
6+
7+
# flake8: noqa
8+
9+
pytestmark = pytest.mark.asyncio
10+
11+
12+
class TestResendOAuthGrantsAsync(AsyncResendBaseTest):
13+
async def test_oauth_grants_list_async(self) -> None:
14+
self.set_mock_json(
15+
{
16+
"object": "list",
17+
"has_more": False,
18+
"data": [
19+
{
20+
"id": "650e8400-e29b-41d4-a716-446655440001",
21+
"client_id": "430eed87-632a-4ea6-90db-0aace67ec228",
22+
"scopes": ["emails:send"],
23+
"created_at": "2023-06-21T06:10:36.144Z",
24+
"revoked_at": None,
25+
"revoked_reason": None,
26+
"client": {
27+
"name": "Resend CLI",
28+
"logo_uri": "https://example.com/logo.png",
29+
},
30+
}
31+
],
32+
}
33+
)
34+
35+
grants: resend.OAuthGrants.ListResponse = await resend.OAuthGrants.list_async()
36+
for grant in grants["data"]:
37+
assert grant["id"] == "650e8400-e29b-41d4-a716-446655440001"
38+
assert grant["client_id"] == "430eed87-632a-4ea6-90db-0aace67ec228"
39+
assert grant["scopes"] == ["emails:send"]
40+
assert grant["client"]["name"] == "Resend CLI"
41+
42+
async def test_should_list_oauth_grants_async_raise_exception_when_no_content(
43+
self,
44+
) -> None:
45+
self.set_mock_json(None)
46+
with pytest.raises(NoContentError):
47+
_ = await resend.OAuthGrants.list_async()
48+
49+
async def test_oauth_grants_revoke_async(self) -> None:
50+
self.set_mock_json(
51+
{
52+
"object": "oauth_grant",
53+
"id": "650e8400-e29b-41d4-a716-446655440001",
54+
"revoked_at": "2023-06-22T06:10:36.144Z",
55+
"revoked_reason": "revoked_from_api",
56+
}
57+
)
58+
59+
revoked: resend.OAuthGrants.RevokeOAuthGrantResponse = (
60+
await resend.OAuthGrants.revoke_async(
61+
oauth_grant_id="650e8400-e29b-41d4-a716-446655440001",
62+
)
63+
)
64+
assert revoked["object"] == "oauth_grant"
65+
assert revoked["id"] == "650e8400-e29b-41d4-a716-446655440001"
66+
assert revoked["revoked_at"] == "2023-06-22T06:10:36.144Z"
67+
assert revoked["revoked_reason"] == "revoked_from_api"
68+
69+
async def test_should_revoke_oauth_grant_async_raise_exception_when_no_content(
70+
self,
71+
) -> None:
72+
self.set_mock_json(None)
73+
with pytest.raises(NoContentError):
74+
_ = await resend.OAuthGrants.revoke_async(oauth_grant_id="grant-1")

0 commit comments

Comments
 (0)