55import random
66import re
77import time
8+ import ssl
89
9- import requests
10+ import httpx
1011
1112from chargebee import (
1213 APIError ,
1314 PaymentError ,
1415 InvalidRequestError ,
1516 OperationFailedError ,
1617)
17- from chargebee import compat , util
18+ from chargebee import compat , util , environment
1819from chargebee .main import Chargebee
1920from chargebee .version import VERSION
2021
@@ -30,31 +31,32 @@ def _basic_auth_str(username):
3031def request (
3132 method ,
3233 url ,
33- env ,
34+ env : environment . Environment ,
3435 params = None ,
3536 headers = None ,
3637 subDomain = None ,
3738 isJsonRequest = False ,
38- options = None ,
39+ options = {},
40+ use_async_client = False ,
3941):
4042 if not env :
4143 raise Exception ("No environment configured." )
42- if headers is None :
43- headers = {}
44+
45+ headers = headers or {}
46+ request_args = {"method" : method .upper ()}
4447
4548 retry_config = env .get_retry_config () if hasattr (env , "get_retry_config" ) else None
4649 url = env .api_url (url , subDomain )
4750
48- if method .lower () in ("get" , "head" , "delete" ):
49- url = "%s?%s" % (url , compat .urlencode (params ))
50- payload = None
51- else :
52- if isJsonRequest :
53- payload = params
54- headers ["Content-type" ] = "application/json;charset=UTF-8"
55- else :
56- payload = compat .urlencode (params )
57- headers ["Content-type" ] = "application/x-www-form-urlencoded"
51+ match method .lower (), isJsonRequest :
52+ case "get" | "head" | "delete" , _:
53+ request_args ["params" ] = params
54+ case _, True :
55+ headers ["Content-Type" ] = "application/json;charset=UTF-8"
56+ request_args ["json" ] = params
57+ case _, False :
58+ headers ["Content-Type" ] = "application/x-www-form-urlencoded"
59+ request_args ["data" ] = params
5860
5961 headers .update (
6062 {
@@ -71,28 +73,42 @@ def request(
7173 idempotency_key is None
7274 and retry_config .is_enabled ()
7375 and method .lower () == "post"
74- and options [ "isIdempotent" ]
76+ and options . get ( "isIdempotent" )
7577 ):
7678 headers [Chargebee .idempotency_header ] = util .generate_uuid_v4 ()
7779
7880 meta = compat .urlparse (url )
7981 scheme = "https" if Chargebee .verify_ca_certs or env .protocol == "https" else "http"
8082 full_url = f"{ scheme } ://{ meta .netloc + meta .path + '?' + meta .query } "
8183
84+ timeout = httpx .Timeout (
85+ None ,
86+ connect = env .connect_timeout ,
87+ read = env .read_timeout ,
88+ )
89+
8290 request_args = {
83- "method" : method .upper (),
84- "timeout" : (env .connect_timeout , env .read_timeout ),
85- "data" : payload ,
91+ ** request_args ,
92+ "timeout" : timeout ,
8693 "headers" : headers ,
8794 "url" : full_url ,
8895 }
96+
8997 if Chargebee .verify_ca_certs :
90- request_args ["verify" ] = Chargebee .ca_cert_path
98+ ctx = ssl .create_default_context (cafile = Chargebee .ca_cert_path )
99+ request_args ["verify" ] = ctx
91100
92- return process_response (full_url , request_args , retry_config , env .enable_debug_logs )
101+ if use_async_client :
102+ return _process_response_async (
103+ full_url , request_args , retry_config , env .enable_debug_logs
104+ )
105+ else :
106+ return _process_response (
107+ full_url , request_args , retry_config , env .enable_debug_logs
108+ )
93109
94110
95- def process_response (url , request_args , retry_config , enable_debug_logs ):
111+ def _process_response (url , request_args , retry_config , enable_debug_logs ):
96112 retry_count = 0
97113
98114 while True :
@@ -107,30 +123,75 @@ def process_response(url, request_args, retry_config, enable_debug_logs):
107123 }
108124 )
109125 )
110- if request_args [ " data"] :
111- _logger .debug ("PAYLOAD: {0}" .format (request_args [ "data" ] ))
126+ if payload := request_args . get ( "json" , request_args . get ( " data")) :
127+ _logger .debug ("PAYLOAD: {0}" .format (payload ))
112128
113129 if retry_count > 0 :
114130 headers = request_args .get ("headers" , {})
115131 headers ["X-CB-Retry-Attempt" ] = str (retry_count )
116132 request_args ["headers" ] = headers
117133
118- response = requests .request (** request_args )
119- _logger .debug (
120- f"{ request_args ['method' ]} Response: { response .status_code } - { response .text } "
134+ return _make_request (request_args )
135+
136+ except Exception as err :
137+ status_code = extract_status_code (err )
138+
139+ if not retry_config or not retry_config .is_enabled ():
140+ raise err
141+
142+ if status_code == 429 :
143+ delay_ms = parse_retry_after (err ) or retry_config .get_delay_ms ()
144+ log (
145+ f"Rate limit hit. Retrying in { delay_ms } ms" ,
146+ "INFO" ,
147+ enable_debug_logs ,
148+ )
149+ sleep (delay_ms )
150+ retry_count += 1
151+ continue
152+
153+ if not should_retry (status_code , retry_count , retry_config ):
154+ log (
155+ f"Request failed after { retry_count } retries: { str (err )} " ,
156+ "ERROR" ,
157+ enable_debug_logs ,
158+ )
159+ raise err
160+
161+ delay_ms = calculate_backoff_delay (retry_count , retry_config .get_delay_ms ())
162+ log (
163+ f"Retrying [{ retry_count + 1 } /{ retry_config .get_max_retries ()} ] in { delay_ms } ms due to status { status_code } " ,
164+ "INFO" ,
165+ enable_debug_logs ,
121166 )
167+ sleep (delay_ms )
168+ retry_count += 1
122169
123- try :
124- resp_json = compat .json .loads (response .text )
125- except Exception :
126- raise map_plaintext_to_error (response )
127170
128- if response .status_code < 200 or response .status_code > 299 :
129- handle_api_resp_error (
130- url , response .status_code , resp_json , response .headers
171+ async def _process_response_async (url , request_args , retry_config , enable_debug_logs ):
172+ retry_count = 0
173+
174+ while True :
175+ try :
176+ _logger .debug (f"{ request_args ['method' ]} Request: { url } " )
177+ _logger .debug (
178+ "HEADERS: {0}" .format (
179+ {
180+ k : v
181+ for k , v in request_args ["headers" ].items ()
182+ if k .lower () != "authorization"
183+ }
131184 )
185+ )
186+ if payload := request_args .get ("json" , request_args .get ("data" )):
187+ _logger .debug ("PAYLOAD: {0}" .format (payload ))
188+
189+ if retry_count > 0 :
190+ headers = request_args .get ("headers" , {})
191+ headers ["X-CB-Retry-Attempt" ] = str (retry_count )
192+ request_args ["headers" ] = headers
132193
133- return resp_json , response . headers , response . status_code
194+ return await _make_request_async ( request_args )
134195
135196 except Exception as err :
136197 status_code = extract_status_code (err )
@@ -145,7 +206,7 @@ def process_response(url, request_args, retry_config, enable_debug_logs):
145206 "INFO" ,
146207 enable_debug_logs ,
147208 )
148- sleep (delay_ms )
209+ await sleep_async (delay_ms )
149210 retry_count += 1
150211 continue
151212
@@ -163,10 +224,44 @@ def process_response(url, request_args, retry_config, enable_debug_logs):
163224 "INFO" ,
164225 enable_debug_logs ,
165226 )
166- sleep (delay_ms )
227+ await sleep_async (delay_ms )
167228 retry_count += 1
168229
169230
231+ def _handle_response (request_args : dict , response : httpx .Response ):
232+ _logger .debug (
233+ f"{ request_args ['method' ]} Response: { response .status_code } - { response .text } "
234+ )
235+
236+ try :
237+ resp_json = compat .json .loads (response .text )
238+ except Exception :
239+ raise map_plaintext_to_error (response )
240+
241+ if response .status_code < 200 or response .status_code > 299 :
242+ handle_api_resp_error (
243+ request_args ["url" ], response .status_code , resp_json , response .headers
244+ )
245+
246+ return resp_json , response .headers , response .status_code
247+
248+
249+ def _make_request (request_args ):
250+ """Make a synchronous HTTP request using httpx"""
251+ verify = request_args .pop ("verify" , True )
252+ with httpx .Client (verify = verify ) as client :
253+ response = client .request (** request_args )
254+ return _handle_response (request_args , response )
255+
256+
257+ async def _make_request_async (request_args ):
258+ """Make an asynchronous HTTP request using httpx"""
259+ verify = request_args .pop ("verify" , True )
260+ async with httpx .AsyncClient (verify = verify ) as client :
261+ response = await client .request (** request_args )
262+ return _handle_response (request_args , response )
263+
264+
170265def map_plaintext_to_error (response ):
171266 text = response .text
172267 if "503" in text :
@@ -246,6 +341,10 @@ def sleep(milliseconds):
246341 time .sleep (milliseconds / 1000.0 )
247342
248343
344+ async def sleep_async (milliseconds ):
345+ await compat .event_loop .sleep (milliseconds / 1000.0 )
346+
347+
249348def log (message , level = "INFO" , enable_debug_logs = False ):
250349 if enable_debug_logs :
251350 print (f"[{ level } ] { message } " )
0 commit comments