summaryrefslogtreecommitdiffstats
path: root/aw_api/WebService.py
blob: ef1c356d144af66541f7c21199769f4ce8b309c5 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
#!/usr/bin/python
#
# Copyright 2009 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

"""Methods for sending and recieving SOAP XML requests."""

__author__ = 'api.sgrinberg@gmail.com (Stan Grinberg)'

import httplib
import sys
import time

from aw_api import AUTH_TOKEN_EXPIRE
from aw_api import LIB_SHORT_NAME
from aw_api import LIB_URL
from aw_api import LIB_VERSION
from aw_api import SOAPPY
from aw_api import ZSI
from aw_api import SanityCheck
from aw_api import Utils
from aw_api.Errors import ERRORS
from aw_api.Errors import ApiError
from aw_api.Errors import AuthTokenError
from aw_api.Errors import Error
from aw_api.Errors import ValidationError
from aw_api.Logger import Logger
from aw_api.SoapBuffer import SoapBuffer


class WebService(object):

  """Implements WebService.

  Responsible for sending and recieving SOAP XML requests.
  """

  def __init__(self, headers, config, op_config, url, lock, logger=None):
    """Inits WebService.

    Args:
      headers: dict dictionary object with populated authentication
               credentials.
      config: dict dictionary object with populated configuration values.
      op_config: dict dictionary object with additional configuration values for
                 this operation.
      url: str url of the web service to call.
      lock: thread.lock the thread lock.
      logger: Logger the instance of Logger
    """
    self.__headers = headers
    self.__config = config
    self.__op_config = op_config
    self.__url = url
    self.__lock = lock
    self.__logger = logger

    if self.__logger is None:
      self.__logger = Logger(self.__config['log_home'])

    if (self.__config['soap_lib'] == SOAPPY and
        self.__headers.values().count(None) < 2 and
        (len(self.__headers.values()) > len(set(self.__headers.values())) and
         not self.__headers.has_key('useragent'))):
      msg = ('Two (or more) values in \'headers\' dict can not be the same. '
             'See, %s/issues/detail?id=27.' % LIB_URL)
      raise ValidationError(msg)

  def __ManageSoap(self, buf, start_time, stop_time, error={}):
    """Manage SOAP XML message.

    Args:
      buf: SoapBuffer SOAP buffer.
      start_time: str time before service call was invoked.
      stop_time: str time after service call was invoked.
      [optional]
      error: dict error, if any.
    """
    # Update the number of units and operations consumed by API call.
    if buf.GetCallUnits() and buf.GetCallOperations():
      self.__config['units'][0] += int(buf.GetCallUnits())
      self.__config['operations'][0] += int(buf.GetCallOperations())
      self.__config['last_units'][0] = int(buf.GetCallUnits())
      self.__config['last_operations'][0] = int(buf.GetCallOperations())

    # Load trace errors, if any.
    if error and 'trace' in error:
      error_msg = error['trace']
    else:
      error_msg = ''

    # Check if response was successful or not.
    if error and 'data' in error:
      is_fault = True
    else:
      is_fault = False

    # Forward SOAP XML, errors, and other debugging data to console, external
    # file, both, or ignore. Each handler supports the following elements,
    #   tag: Config value for this handler. If left empty, will never write
    #        data to file.
    #   target: Target/destination represented by this handler (i.e. FILE,
    #           CONSOLE, etc.). Initially, it should be set to Logger.NONE.
    #   name: Name of the log file to use.
    #   data: Data to write.
    handlers = [
        {'tag': 'xml_log',
         'target': Logger.NONE,
         'name': 'soap_xml',
         'data': str('StartTime: %s\n%s\n%s\n%s\n%s\nEndTime: %s'
                     % (start_time, buf.GetHeadersOut(), buf.GetSOAPOut(),
                        buf.GetHeadersIn(), buf.GetSOAPIn(), stop_time))},
        {'tag': 'request_log',
         'target': Logger.NONE,
         'name': 'request_info',
         'data': str('host=%s service=%s method=%s operator=%s responseTime=%s '
                     'operations=%s units=%s requestId=%s isFault=%s'
                     % (Utils.GetNetLocFromUrl(self.__url),
                        buf.GetServiceName(), buf.GetCallName(),
                        buf.GetOperatorName(), buf.GetCallResponseTime(),
                        buf.GetCallOperations(), buf.GetCallUnits(),
                        buf.GetCallRequestId(), is_fault))},
        {'tag': '',
         'target': Logger.NONE,
         'name': 'aw_api_lib',
         'data': 'DEBUG: %s' % error_msg}
    ]
    for handler in handlers:
      if (handler['tag'] and
          Utils.BoolTypeConvert(self.__config[handler['tag']])):
        handler['target'] = Logger.FILE
      # If debugging is On, raise handler's target two levels,
      #   NONE -> CONSOLE
      #   FILE -> FILE_AND_CONSOLE.
      if Utils.BoolTypeConvert(self.__config['debug']):
        handler['target'] += 2

      if (handler['target'] != Logger.NONE and handler['data'] and
          handler['data'] != 'None' and handler['data'] != 'DEBUG: '):
        self.__logger.Log(handler['name'], handler['data'],
                          log_level=Logger.DEBUG, log_handler=handler['target'])

    # If raw response is requested, no need to validate and throw appropriate
    # error. Up to the end user to handle successful or failed request.
    if Utils.BoolTypeConvert(self.__config['raw_response']):
      return

    # Report SOAP fault.
    if is_fault:
      try:
        fault = buf.GetFaultAsDict()
        if not fault:
          msg = error['data']
      except:
        fault = None
        # An error is not a SOAP fault, but check if some other error.
        if error_msg:
          msg = error_msg
        else:
          msg = ('Unable to parse incoming SOAP XML. Please, file '
                 'a bug at %s/issues/list.' % LIB_URL)
      # Release thread lock.
      if self.__lock.locked():
        self.__lock.release()
      if not fault and msg:
        raise Error(msg)

      # Raise a specific error, subclass of ApiError.
      if 'detail' in fault:
        if 'code' in fault['detail']:
          code = int(fault['detail']['code'])
          if code in ERRORS:
            raise ERRORS[code](fault)
        elif 'errors' in fault['detail']:
          type = fault['detail']['errors'][0]['type']
          if type in ERRORS:
            raise ERRORS[str(type)](fault)
      raise ApiError(fault)

  def CallMethod(self, method_name, params, service_name=None, loc=None,
                 request=None):
    """Make an API call to specified method.

    Args:
      method_name: str API method name.
      params: list list of parameters to send to the API method.
      [optional]
      service_name: str API service name.
      loc: service locator.
      request: instance holder of the SOAP request.

    Returns:
      tuple/str response from the API method. If 'raw_response' flag enabled a
                string is returned, tuple otherwise.
    """
    # Acquire thread lock.
    self.__lock.acquire()

    try:
      headers = self.__headers
      config = self.__config

      # Temporarily redirect HTTP headers and SOAP from STDOUT into a buffer.
      buf = SoapBuffer(
          xml_parser=config['xml_parser'],
          pretty_xml=Utils.BoolTypeConvert(config['use_pretty_xml']))
      old_stdout = sys.stdout
      sys.stdout = buf

      start_time = time.strftime('%Y-%m-%d %H:%M:%S')
      response = ()
      raw_response = ''
      error = {}
      try:
        if Utils.BoolTypeConvert(config['use_strict']):
          SanityCheck.ValidateHeadersForServer(headers,
                                               self.__op_config['server'])

        # Load/unload version specific authentication and configuration data.
        if SanityCheck.IsNewApi(self.__op_config['version']):
          # Set boolean to the format expected by the server, True => true.
          if 'validateOnly' in headers:
            headers['validateOnly'] = headers['validateOnly'].lower()

          # Load/set authentication token. If authentication token has expired,
          # regenerate it.
          now = time.time()
          if (Utils.BoolTypeConvert(config['use_auth_token']) and
              (('authToken' not in headers and
                'auth_token_epoch' not in config) or
               int(now - config['auth_token_epoch']) >= AUTH_TOKEN_EXPIRE)):
            headers['authToken'] = Utils.GetAuthToken(headers['email'],
                                                      headers['password'])
            config['auth_token_epoch'] = time.time()
            self.__headers = headers
            self.__config = config
          elif not Utils.BoolTypeConvert(config['use_auth_token']):
            msg = ('Requests via %s require use of authentication token.'
                   % self.__op_config['version'])
            raise ValidationError(msg)

          headers = Utils.UnLoadDictKeys(Utils.CleanUpDict(headers),
                                         ['email', 'password'])
          name_space = '/'.join(['https://adwords.google.com/api/adwords',
                                 self.__op_config['group'],
                                 self.__op_config['version']])
          config['ns_target'] = (name_space, 'RequestHeader')
        else:
          headers['useragent'] = headers['userAgent']
          headers = Utils.UnLoadDictKeys(headers, ['authToken', 'userAgent'])
          config = Utils.UnLoadDictKeys(config, ['ns_target',
                                                 'auth_token_epoch'])

        # Fire off API request and handle the response.
        if config['soap_lib'] == SOAPPY:
          from aw_api.soappy_toolkit import MessageHandler
          service = MessageHandler.GetServiceConnection(
              headers, config, self.__url, self.__op_config['http_proxy'],
              self.__op_config['version'])

          if not SanityCheck.IsNewApi(self.__op_config['version']):
            response = MessageHandler.UnpackResponseAsDict(
                service.invoke(method_name, params))
          else:
            response = MessageHandler.UnpackResponseAsDict(
                service._callWithBody(MessageHandler.SetRequestParams(
                    config, method_name, params)))
        elif config['soap_lib'] == ZSI:
          from aw_api.zsi_toolkit import MessageHandler
          service = MessageHandler.GetServiceConnection(
              headers, config, self.__url, self.__op_config['http_proxy'],
              service_name, loc)
          request = MessageHandler.SetRequestParams(self.__op_config, request,
                                                    params)

          response = MessageHandler.UnpackResponseAsTuple(
              eval('service.%s(request)' % method_name))

          # The response should always be tuple. If it's not, there must be
          # something wrong with MessageHandler.UnpackResponseAsTuple().
          if len(response) == 1 and isinstance(response[0], list):
            response = tuple(response[0])

        if isinstance(response, list):
          response = tuple(response)
        elif isinstance(response, tuple):
          pass
        else:
          if response:
            response = (response,)
          else:
            response = ()
      except Exception, e:
        error['data'] = e
      stop_time = time.strftime('%Y-%m-%d %H:%M:%S')

      # Restore STDOUT.
      sys.stdout = old_stdout

      # When debugging mode is ON, fetch last traceback.
      if Utils.BoolTypeConvert(self.__config['debug']):
        error['trace'] = Utils.LastStackTrace()

      # Catch local errors prior to going down to the SOAP layer, which may not
      # exist for this error instance.
      if 'data' in error and not buf.IsHandshakeComplete():
        # Check if buffer contains non-XML data, most likely an HTML page. This
        # happens in the case of 502 errors (and similar). Otherwise, this is a
        # local error and API request was never made.
        html_error = Utils.GetErrorFromHtml(buf.GetBufferAsStr())
        if html_error:
          msg = '%s' % html_error
        else:
          msg = str(error['data'])
          if Utils.BoolTypeConvert(self.__config['debug']):
            msg += '\n%s' % error['trace']

        # When debugging mode is ON, store the raw content of the buffer.
        if Utils.BoolTypeConvert(self.__config['debug']):
          error['raw_data'] = buf.GetBufferAsStr()

        # Catch errors from AuthToken and ValidationError levels, raised during
        # try/except above.
        if isinstance(error['data'], AuthTokenError):
          raise AuthTokenError(msg)
        elif isinstance(error['data'], ValidationError):
          raise ValidationError(error['data'])
        if 'raw_data' in error:
          msg = '%s [RAW DATA: %s]' % (msg, error['raw_data'])
        raise Error(msg)

      if Utils.BoolTypeConvert(self.__config['raw_response']):
        raw_response = buf.GetRawSOAPIn()

      self.__ManageSoap(buf, start_time, stop_time, error)
    finally:
      # Release thread lock.
      if self.__lock.locked():
        self.__lock.release()

    if Utils.BoolTypeConvert(self.__config['raw_response']):
      return raw_response

    return response

  def CallRawMethod(self, soap_message):
    """Make an API call by posting raw SOAP XML message.

    Args:
      soap_message: str SOAP XML message.

    Returns:
      tuple response from the API method.
    """
    # Acquire thread lock.
    self.__lock.acquire()

    try:
      buf = SoapBuffer(
          xml_parser=self.__config['xml_parser'],
          pretty_xml=Utils.BoolTypeConvert(self.__config['use_pretty_xml']))
      http_header = {
          'post': '%s' % self.__url,
          'host': 'sandbox.google.com',
          'user_agent': '%s v%s; WebService.py' % (LIB_SHORT_NAME, LIB_VERSION),
          'content_type': 'text/xml; charset=\"UTF-8\"',
          'content_length': '%d' % len(soap_message),
          'soap_action': ''
      }

      version = self.__url.split('/')[-2]
      if SanityCheck.IsNewApi(version):
        http_header['host'] = 'adwords-%s' % http_header['host']

      index = self.__url.find('adwords.google.com')
      if index > -1:
        http_header['host'] = 'adwords.google.com'

      self.__url = ''.join(['https://', http_header['host'], self.__url])

      start_time = time.strftime('%Y-%m-%d %H:%M:%S')
      buf.write(
          ('%s Outgoing HTTP headers %s\nPOST %s\nHost: %s\nUser-Agent: '
           '%s\nContent-type: %s\nContent-length: %s\nSOAPAction: %s\n%s\n%s '
           'Outgoing SOAP %s\n%s\n%s\n' % ('*'*3, '*'*46, http_header['post'],
                                           http_header['host'],
                                           http_header['user_agent'],
                                           http_header['content_type'],
                                           http_header['content_length'],
                                           http_header['soap_action'], '*'*72,
                                           '*'*3, '*'*54, soap_message,
                                           '*'*72)))

      # Construct header and send SOAP message.
      web_service = httplib.HTTPS(http_header['host'])
      web_service.putrequest('POST', http_header['post'])
      web_service.putheader('Host', http_header['host'])
      web_service.putheader('User-Agent', http_header['user_agent'])
      web_service.putheader('Content-type', http_header['content_type'])
      web_service.putheader('Content-length', http_header['content_length'])
      web_service.putheader('SOAPAction', http_header['soap_action'])
      web_service.endheaders()
      web_service.send(soap_message)

      # Get response.
      status_code, status_message, header = web_service.getreply()
      response = web_service.getfile().read()

      header = str(header).replace('\r', '')
      buf.write(('%s Incoming HTTP headers %s\n%s %s\n%s\n%s\n%s Incoming SOAP'
                 ' %s\n%s\n%s\n' % ('*'*3, '*'*46, status_code, status_message,
                                    header, '*'*72, '*'*3, '*'*54, response,
                                    '*'*72)))
      stop_time = time.strftime('%Y-%m-%d %H:%M:%S')

      # Catch local errors prior to going down to the SOAP layer, which may not
      # exist for this error instance.
      if not buf.IsHandshakeComplete() or not buf.IsSoap():
        # The buffer contains non-XML data, most likely an HTML page. This
        # happens in the case of 502 errors.
        html_error = Utils.GetErrorFromHtml(buf.GetBufferAsStr())
        if html_error:
          msg = '%s' % html_error
        else:
          msg = 'Unknown error.'
        raise Error(msg)

      self.__ManageSoap(buf, start_time, stop_time,
                        {'data': buf.GetBufferAsStr()})
    finally:
      # Release thread lock.
      if self.__lock.locked():
        self.__lock.release()

    return (response,)