SOAP (Suds library)

Creating and modifying campaigns, ads and keywords in Python 2.7 using the Suds 0.4 GA library.

The Suds library analyzes the WSDL description and provides an interface based on the SOAP protocol. The library creates input data structures that the application then fills in and passes to the server.

Important. The Python socket subsystem should support SSL (the Python distribution includes the ssl.py module).

Sample application: app-python-oauth.py. A step-by-step tour of the application is provided below.

1. Connect Suds and configure logging

# -*- coding: utf_8 -*-
from suds.client import Client
from suds.cache import DocumentCache
from suds.sax.element import Element
from suds import WebFault

The Client class (suds.client module) analyzes the WSDL description, creates data structures, sends SOAP packets and parses responses. This is the main class for interacting with the API. The DocumentCache class (suds.cache module) provides caching for the WSDL description.

The Element class (suds.sax.element module) is for adding arbitrary headers to SOAP packets. SOAP headers are used for passing metadata and data to the server for OAuth authorization.

The WebFault class is an exception that is generated when errors occur on the side of the API server. Catching this exception allows you to distinguish between server errors and errors on the application side.

Logging parameters are shown below.

import logging
logging.basicConfig(level=logging.INFO)
if __debug__:
    logging.getLogger('suds.client').setLevel(logging.DEBUG)
else:
    logging.getLogger('suds.client').setLevel(logging.CRITICAL)

When running the Python interpreter with the -O flag (optimized mode), only messages about critical errors are output. When running it without this flag, debugging messages about sent and received SOAP packets are output, including the packet texts.

2. Plugin for correcting responses

The technique shown below fixes a known API error that causes the data types in a response to be associated with the namespace http://namespaces.soaplite.com/perl instead of the API namespace. Because of this error, Suds creates incorrect output data structures that cannot be reused in API requests. In the next version of the API, the error will be fixed and this technique will no longer be necessary.

from suds.plugin import *
class NamespaceCorrectionPlugin(MessagePlugin):
    def received(self, context):
        context.reply = context.reply.replace('"http://namespaces.soaplite.com/perl"','"API"')

The received user function corrects the namespace in API responses before Suds analyzes the responses. The response is passed in the context parameter as a Unicode string. The response consists of a SOAP message in the same format as it arrived via HTTP.

3. Instance of the suds.Client class

An instance of the suds.Client class is used for interacting with the API. When creating it, specify the URL of the WSDL description.

api = Client('https://api.direct.yandex.ru/v4/wsdl/', plugins = [NamespaceCorrectionPlugin()])
api.set_options(cache=DocumentCache())

The code shown also connects the plugin that was created in the previous step, and in the last line it includes caching for the WSDL description.

4. Metadata in SOAP packet headers

SOAP packets contain the <Header> element, which is used for passing metadata to the server. The metadata relates to the request in general, not to the particular method invoked. The following example shows how to pass the locale parameter to specify the language for response messages.

locale = Element('locale').setText('en')
api.set_options(soapheaders=(locale))

SOAP headers are automatically included in all SOAP packets.

5. OAuth authorization

For OAuth authorization, you must know the access token (token). It is specified in the SOAP packet headers as metadata.

token = Element('token').setText('e4d3b3d2a74e4fa387a18dda5cd1c8d9')
locale = Element('locale').setText('en')
api.set_options(soapheaders=(token, locale))

6. Function for invoking API methods

The following function calls the specified API method with input parameters.

def directRequest(methodName, params):
    '''
    Calling a Yandex Direct API method:
       api - instance of the suds.Client class
       methodName - method name
       params - input parameters
    If an error occurs the program ends,
    otherwise it returns the result of calling the method
    '''
    try:
        result = api.service['APIPort'][methodName](params)
        return result
    except WebFault, err:
        print unicode(err)
    except:
        err = sys.exc_info()[1]
        print 'Other error: ' + str(err)
    exit(-1)

If the method is executed successfully, the function returns the data received from the server. The data is presented as a hierarchy of objects, which is created by Suds based on an analysis of the SOAP response. Now we will examine techniques for working with output data.

If an error occurs, the function analyzes the exception object. If it belongs to the WebFault class, an error message from the server is output. In all other cases, the error is on the application side, and the error message is output with the “Other error:” prefix. For any error, the application exits with the return code -1.

7. Create a campaign

The following code demonstrates how to use a dictionary to formulate input parameters. The params dictionary contains the minimum required parameters for creating a campaign.

params = {
   'CampaignID': 0,
   'Login': 'agrom',
   'Name': u'Campaign created via the API',
   'FIO': 'Alex Gromov',
   'Strategy':{
      'StrategyName': 'WeeklyBudget',
      'WeeklySumLimit': 400,
      'MaxPrice': 8,
   },
   'EmailNotification':{
      'MoneyWarningValue':20,
      'SendAccNews':'Yes',
      'WarnPlaceInterval':60,
      'SendWarn':'Yes',
      'Email':'agrom@yandex.ru'
   },
}

Shown below is a CreateOrUpdateCampaign method call using the directRequest function that was created earlier. The ID of the created campaign is put in the campaignId output parameter.

campaignId = directRequest('CreateOrUpdateCampaign', params)
print u'Created campaign ID=' + str(campaignId)

8. Edit the campaign

To edit a campaign, it is a good idea to get its parameters, make the necessary changes, then save the parameters in the API. The GetCampaignsParams method accepts an array of campaign IDs and returns their parameters.

params = {'CampaignIDS': [campaignId]}
campaignsParams = directRequest('GetCampaignsParams', params)

Ways to access output data are shown below.

params = campaignsParams[0]
params['EmailNotification']['Email'] = 'new@email.ru'
params.MinusKeywords = [u'car cooler', u'refrigerator truck'] 
params.TimeTarget.DaysHours[0].Hours = [6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]

The campaignsParams array contains CampaignInfo objects. Since we have requested parameters for a single campaign, we will be working with the first element of the array, setting its variable with params.

The second line of code shows that parameters can be accessed in dictionary notation. To do this, specify parameter names in quotation marks inside square brackets. The third and fourth lines demonstrate how to access parameters in object notation, where parameter names are separated by dots.

After making the necessary changes, we save the parameters using theCreateOrUpdateCampaign method.

result = directRequest('CreateOrUpdateCampaign', params)
print u'Modified parameters of the campaign ID=' + str(result)

9. Create an ad and a keyword

To create an ad and a keyword, we will use the Factory class from the suds module. It will help us create an input data structure, then we will populate it with values. This method is an alternative to creating dictionaries.

banner = api.factory.create('BannerInfo')
banner.CampaignID = campaignId
banner.BannerID = 0
banner.Title = Refrigerators'
banner.Text = u'Refrigerator sales and repair'
banner.Href = 'http://www.api.ru/banner{param1}?page={param2}'
banner.Geo = '1,10174'

In the first line, the BannerInfo input data structure is created, which contains all the supported parameters. In the following lines, mandatory parameters are set (the keyword can't be created without them).

The input structure created using Factory should not contain any uninitialized arrays. For this reason, parameters that contain arrays should either be explicitly initialized (for example, by assigning an empty array), or they should be deleted, as shown below.

banner.Sitelinks = []
del(banner.ContactInfo) 
del(banner.MinusKeywords)

For the BannerInfo structure, there is a mandatory Phrases array, which must contain at least one keyword. We will create one and place it in the Phrases array.

phrase = api.factory.create('BannerPhraseInfo')
phrase.PhraseID = 0
phrase.Phrase = u'refrigerator'
phrase.Price = 1.99
banner.Phrases = [phrase]

In the first line, the BannerPhraseInfo structure is created. In the following lines, mandatory parameters are set for the keyword.

The API lets you set variables for the keyword, which can be automatically substituted in the link to the site if the ad was found using this keyword. How to set these variables is shown below.

userParams = api.factory.create('PhraseUserParams')
userParams.Param1 = ''
userParams.Param2 = 'freezer'
banner.Phrases[0].UserParams = userParams

Now, if the ad is found using the keyword [refrigerator], the link to the site http://www.api.ru/banner{param1}?page={param2} will appear as http://www.api.ru/banner?page=freezer.

Save the ad parameters using the CreateOrUpdateBanners method.

params = [banner]
bannerID = directRequest('CreateOrUpdateBanners', params)[0]
print u'Created ad ID=' + str(bannerID)

If successful, the output parameter bannerId contains the ID of the created ad.

10. Add a keyword to the ad

To add keywords, it is a good practice to get the current parameters for the ad, including parameters for keywords, then make the necessary changes and save the parameters in the API.

How to get ad parameters is shown below.

params = {'BannerIDS': [bannerID]}
bannerParams = directRequest('GetBanners', params)[0]

The GetBanners method returns an array of BannerInfo objects, each of which contains information about an ad. In our example, we requested information that is located in the bannerParams variable for a single ad.

Now we create the BannerPhraseInfo object and set the keyword parameters.

phrase = api.factory.create('BannerPhraseInfo')
phrase.PhraseID = 0
phrase.Phrase = u'freezer'
phrase.Price = 1.44

We create user variables for substituting in the link to the site.

phrase.UserParams = api.factory.create('PhraseUserParams')
phrase.UserParams.Param1 = '_2'
phrase.UserParams.Param2 = 'deepfreeze'

Now, if the ad is found using the keyword [freezer], the link to the site http://www.api.ru/banner{param1}?page={param2} will appear as http://www.api.ru/banner_2?page=deepfreeze.

Let's add the keyword to the Phrases array and save the ad parameters using the CreateOrUpdateBanners method.

bannerParams.Phrases.append(phrase)
params = [bannerParams]
bannerIDS = directRequest('CreateOrUpdateBanners', params)
print u'Modified ad ID=' + str(bannerIDS[0])

11. Call finance methods

When calling the finance method, you must additionally specify the finance token and the transaction number (see Accessing finance methods).

As an example, we will call the CreateInvoice method, which creates an invoice. Below you can see how the finance token is formed. The input data are the master token (obtained from the Yandex Direct interface), the transaction number, the method name, and the username.

import hashlib

masterToken  = 'AEgchkX2M3FBL8lU'
operationNum = 119
usedMethod   = 'CreateInvoice'
login        = 'agrom'

financeToken = hashlib.sha256(masterToken + str(operationNum) + usedMethod + login).hexdigest()

This creates a single-use finance token for calling the CreateInvoice method for the user agrom. An example of the token is shown below.

7215f95e84a766971d8ec4eb5a39ae96505b3a5529a91e5a03e5943565a6e6c7

The finance token and transaction number are passed in the header of the SOAP packet. Below you can see how the parameters are set in the header.

finance_token = Element('finance_token').setText(financeToken)
operation_num = Element('operation_num').setText(operationNum)
api.set_options(soapheaders=(finance_token, operation_num))
Attention. When using OAuth authorization, you also must specify the access token (token) in the header of the SOAP package.

Let's create an input data structure and call the CreateInvoice method.

invoice = api.factory.create('PayCampElement')
invoice.CampaignID = campaignId
invoice.Sum = 150
params = api.factory.create('CreateInvoiceInfo')
params.Payments = []
params.Payments.append(invoice)
url = directRequest('CreateInvoice', params)

If successful, the method returns the URL to use to get the printable format of the invoice. The only person who can access the invoice is the authorized Yandex user that the request was made on behalf of (in this example, the user "agrom").