1+ import json
12from typing import Any , Dict , Generic , List , Optional , Union , cast
23
3- import requests
44from typing_extensions import Literal , TypeVar
55
66import resend
7- from resend .exceptions import NoContentError , raise_for_code_and_type
7+ from resend .exceptions import (NoContentError , ResendError ,
8+ raise_for_code_and_type )
89from resend .version import get_version
910
1011RequestVerb = Literal ["get" , "post" , "put" , "patch" , "delete" ]
11-
1212T = TypeVar ("T" )
1313
14+ ParamsType = Union [Dict [str , Any ], List [Dict [str , Any ]]]
15+ HeadersType = Dict [str , str ]
16+
1417
15- # This class wraps the HTTP request creation logic
1618class Request (Generic [T ]):
1719 def __init__ (
1820 self ,
1921 path : str ,
20- params : Union [ Dict [ Any , Any ], List [ Dict [ Any , Any ]]] ,
22+ params : ParamsType ,
2123 verb : RequestVerb ,
2224 options : Optional [Dict [str , Any ]] = None ,
2325 ):
@@ -27,94 +29,80 @@ def __init__(
2729 self .options = options
2830
2931 def perform (self ) -> Union [T , None ]:
30- """Is the main function that makes the HTTP request
31- to the Resend API. It uses the path, params, and verb attributes
32- to make the request.
33-
34- Returns:
35- Union[T, None]: A generic type of the Request class or None
36-
37- Raises:
38- requests.HTTPError: If the request fails
39- """
40- resp = self .make_request (url = f"{ resend .api_url } { self .path } " )
32+ data = self .make_request (url = f"{ resend .api_url } { self .path } " )
4133
42- # delete calls do not return a body
43- if resp .text == "" and resp .status_code == 200 :
44- return None
45-
46- # this is a safety net, if we get here it means the Resend API is having issues
47- # and most likely the gateway is returning htmls
48- if "application/json" not in resp .headers ["content-type" ]:
34+ if isinstance (data , dict ) and data .get ("statusCode" ) not in (None , 200 ):
4935 raise_for_code_and_type (
50- code = 500 ,
51- message = "Failed to parse Resend API response. Please try again." ,
52- error_type = " InternalServerError" ,
36+ code = data . get ( "statusCode" ) or 500 ,
37+ message = data . get ( "message" , "Unknown error" ) ,
38+ error_type = data . get ( "name" , " InternalServerError") ,
5339 )
5440
55- # handle error in case there is a statusCode attr present
56- # and status != 200 and response is a json.
57- if resp .status_code != 200 and resp .json ().get ("statusCode" ):
58- error = resp .json ()
59- raise_for_code_and_type (
60- code = error .get ("statusCode" ),
61- message = error .get ("message" ),
62- error_type = error .get ("name" ),
63- )
64- return cast (T , resp .json ())
41+ return cast (T , data )
6542
6643 def perform_with_content (self ) -> T :
67- """
68- Perform an HTTP request and return the response content.
69-
70- Returns:
71- T: The content of the response
72-
73- Raises:
74- NoContentError: If the response content is `None`.
75- """
7644 resp = self .perform ()
7745 if resp is None :
7846 raise NoContentError ()
7947 return resp
8048
81- def __get_headers (self ) -> Dict [Any , Any ]:
82- """get_headers returns the HTTP headers that will be
83- used for every req.
84-
85- Returns:
86- Dict: configured HTTP Headers
87- """
88- headers = {
49+ def __get_headers (self ) -> HeadersType :
50+ headers : HeadersType = {
8951 "Accept" : "application/json" ,
9052 "Authorization" : f"Bearer { resend .api_key } " ,
9153 "User-Agent" : f"resend-python:{ get_version ()} " ,
9254 }
9355
94- # Add the Idempotency-Key header if the verb is POST
95- # and the options dict contains the key
96- if self .verb == "post" and (self .options and "idempotency_key" in self .options ):
97- headers ["Idempotency-Key" ] = self .options ["idempotency_key" ]
56+ if self .verb == "post" and self .options and "idempotency_key" in self .options :
57+ headers ["Idempotency-Key" ] = str (self .options ["idempotency_key" ])
58+
9859 return headers
9960
100- def make_request (self , url : str ) -> requests .Response :
101- """make_request is a helper function that makes the actual
102- HTTP request to the Resend API.
61+ def make_request (self , url : str ) -> Union [Dict [str , Any ], List [Any ]]:
62+ headers = self .__get_headers ()
10363
104- Args:
105- url (str): The URL to make the request to
64+ if isinstance (self .params , dict ):
65+ json_params : Optional [Union [Dict [str , Any ], List [Any ]]] = {
66+ str (k ): v for k , v in self .params .items ()
67+ }
68+ elif isinstance (self .params , list ):
69+ json_params = [dict (item ) for item in self .params ]
70+ else :
71+ json_params = None
10672
107- Returns:
108- requests.Response: The response object from the request
73+ try :
74+ content , _status_code , resp_headers = resend .default_http_client .request (
75+ method = self .verb ,
76+ url = url ,
77+ headers = headers ,
78+ json = json_params ,
79+ )
10980
110- Raises:
111- requests.HTTPError: If the request fails
112- """
113- headers = self .__get_headers ()
114- params = self .params
115- verb = self .verb
81+ # Safety net around the HTTP Client
82+ except Exception as e :
83+ raise ResendError (
84+ code = 500 ,
85+ message = str (e ),
86+ error_type = "HttpClientError" ,
87+ suggested_action = "Request failed, please try again." ,
88+ )
89+
90+ content_type = {k .lower (): v for k , v in resp_headers .items ()}.get (
91+ "content-type" , ""
92+ )
93+
94+ if "application/json" not in content_type :
95+ raise_for_code_and_type (
96+ code = 500 ,
97+ message = f"Expected JSON response but got: { content_type } " ,
98+ error_type = "InternalServerError" ,
99+ )
116100
117101 try :
118- return requests .request (verb , url , json = params , headers = headers )
119- except requests .HTTPError as e :
120- raise e
102+ return cast (Union [Dict [str , Any ], List [Any ]], json .loads (content ))
103+ except json .JSONDecodeError :
104+ raise_for_code_and_type (
105+ code = 500 ,
106+ message = "Failed to decode JSON response" ,
107+ error_type = "InternalServerError" ,
108+ )
0 commit comments