88import importlib
99import json
1010import logging
11+ import re
1112import time
1213
1314import requests
2728 'XmlRpcTransport' ,
2829 'RestTransport' ,
2930 'TimingTransport' ,
31+ 'DebugTransport' ,
3032 'FixtureTransport' ,
3133 'SoftLayerListResult' ,
3234]
@@ -100,6 +102,24 @@ def __init__(self):
100102 #: Integer result offset.
101103 self .offset = None
102104
105+ #: Integer call start time
106+ self .start_time = None
107+
108+ #: Integer call end time
109+ self .end_time = None
110+
111+ #: String full url
112+ self .url = None
113+
114+ #: String result of api call
115+ self .result = None
116+
117+ #: String payload to send in
118+ self .payload = None
119+
120+ #: Exception any exceptions that got caught
121+ self .exception = None
122+
103123
104124class SoftLayerListResult (list ):
105125 """A SoftLayer API list result."""
@@ -117,8 +137,7 @@ class XmlRpcTransport(object):
117137 """XML-RPC transport."""
118138 def __init__ (self , endpoint_url = None , timeout = None , proxy = None , user_agent = None , verify = True ):
119139
120- self .endpoint_url = (endpoint_url or
121- consts .API_PUBLIC_ENDPOINT ).rstrip ('/' )
140+ self .endpoint_url = (endpoint_url or consts .API_PUBLIC_ENDPOINT ).rstrip ('/' )
122141 self .timeout = timeout or None
123142 self .proxy = proxy
124143 self .user_agent = user_agent or consts .USER_AGENT
@@ -139,7 +158,6 @@ def __call__(self, request):
139158 :param request request: Request object
140159 """
141160 largs = list (request .args )
142-
143161 headers = request .headers
144162
145163 if request .identifier is not None :
@@ -163,32 +181,26 @@ def __call__(self, request):
163181 request .transport_headers .setdefault ('Content-Type' , 'application/xml' )
164182 request .transport_headers .setdefault ('User-Agent' , self .user_agent )
165183
166- url = '/' .join ([self .endpoint_url , request .service ])
167- payload = utils .xmlrpc_client .dumps (tuple (largs ),
184+ request . url = '/' .join ([self .endpoint_url , request .service ])
185+ request . payload = utils .xmlrpc_client .dumps (tuple (largs ),
168186 methodname = request .method ,
169187 allow_none = True )
170188
171189 # Prefer the request setting, if it's not None
172190 verify = request .verify
173191 if verify is None :
174- verify = self .verify
192+ request . verify = self .verify
175193
176- LOGGER .debug ("=== REQUEST ===" )
177- LOGGER .debug ('POST %s' , url )
178- LOGGER .debug (request .transport_headers )
179- LOGGER .debug (payload )
180194
181195 try :
182- resp = self .client .request ('POST' , url ,
183- data = payload ,
196+ resp = self .client .request ('POST' , request . url ,
197+ data = request . payload ,
184198 headers = request .transport_headers ,
185199 timeout = self .timeout ,
186- verify = verify ,
200+ verify = request . verify ,
187201 cert = request .cert ,
188202 proxies = _proxies_dict (self .proxy ))
189- LOGGER .debug ("=== RESPONSE ===" )
190- LOGGER .debug (resp .headers )
191- LOGGER .debug (resp .content )
203+
192204 resp .raise_for_status ()
193205 result = utils .xmlrpc_client .loads (resp .content )[0 ][0 ]
194206 if isinstance (result , list ):
@@ -218,6 +230,42 @@ def __call__(self, request):
218230 except requests .RequestException as ex :
219231 raise exceptions .TransportError (0 , str (ex ))
220232
233+ def print_reproduceable (self , request ):
234+ """Prints out the minimal python code to reproduce a specific request
235+
236+ The will also automatically replace the API key so its not accidently exposed.
237+
238+ :param request request: Request object
239+ """
240+ from string import Template
241+ output = Template ('''============= testing.py =============
242+ import requests
243+ from requests.adapters import HTTPAdapter
244+ from urllib3.util.retry import Retry
245+ from xml.etree import ElementTree
246+ client = requests.Session()
247+ client.headers.update({'Content-Type': 'application/json', 'User-Agent': 'softlayer-python/testing',})
248+ retry = Retry(connect=3, backoff_factor=3)
249+ adapter = HTTPAdapter(max_retries=retry)
250+ client.mount('https://', adapter)
251+ url = '$url'
252+ payload = """$payload"""
253+ transport_headers = $transport_headers
254+ timeout = $timeout
255+ verify = $verify
256+ cert = $cert
257+ proxy = $proxy
258+ response = client.request('POST', url, data=payload, headers=transport_headers, timeout=timeout,
259+ verify=verify, cert=cert, proxies=proxy)
260+ xml = ElementTree.fromstring(response.content)
261+ ElementTree.dump(xml)
262+ ==========================''' )
263+
264+ safe_payload = re .sub (r'<string>[a-z0-9]{64}</string>' , r'<string>API_KEY_GOES_HERE</string>' , request .payload )
265+ substitutions = dict (url = request .url , payload = safe_payload , transport_headers = request .transport_headers ,
266+ timeout = self .timeout , verify = request .verify , cert = request .cert , proxy = _proxies_dict (self .proxy ))
267+ return output .substitute (substitutions )
268+
221269
222270class RestTransport (object ):
223271 """REST transport.
@@ -334,6 +382,43 @@ def __call__(self, request):
334382 raise exceptions .TransportError (0 , str (ex ))
335383
336384
385+ class DebugTransport (object ):
386+ """Transport that records API call timings."""
387+
388+ def __init__ (self , transport ):
389+ self .transport = transport
390+
391+ #: List All API calls made during a session
392+ self .requests = []
393+
394+ def __call__ (self , call ):
395+ call .start_time = time .time ()
396+
397+ self .pre_transport_log (call )
398+ try :
399+ call .result = self .transport (call )
400+ except (exceptions .SoftLayerAPIError , exceptions .TransportError ) as ex :
401+ call .exception = ex
402+
403+ self .post_transport_log (call )
404+
405+ call .end_time = time .time ()
406+ self .requests .append (call )
407+
408+ if call .exception is not None :
409+ raise call .exception
410+
411+ return call .result
412+
413+ def pre_transport_log (self , call ):
414+ LOGGER .warning ("Calling: {}::{}(id={})" .format (call .service , call .method , call .identifier ))
415+
416+ def post_transport_log (self , call ):
417+ LOGGER .debug (self .transport .print_reproduceable (call ))
418+
419+ def get_last_calls (self ):
420+ return self .requests
421+
337422class TimingTransport (object ):
338423 """Transport that records API call timings."""
339424
0 commit comments