Skip to Content
Author's profile photo William Ayd

Communicating with RESTful Web Services via Python

Introduction

SAP introduced a RESTful Web Service SDK in 4.X environments to interact with the BI platform and its components. In previous 3.X versions, you were tied into using the Java and .NET SDKs which were tedious to set up and correctly configure an environment for. By moving to a RESTful Web Service, SAP has unlocked the potential to communicate with your BI platform via any language of your choice, assuming it supports making HTTP requests. The SAP BusinessObjects RESTful Web Service SDK User Guide for Web Intelligence and the BI Semantic Layer does a fantastic job of documenting the particular URLs that can be accessed as well as the responses that they return.

While the documentation is thorough, it does not help you figure out how to leverage any one particular language to interact with your semantic layer. This document will show you how to do that using the Python programming language, while providing a theoretical web service implementation that will not only help you understand how to make requests, but should also give you a rough idea of how this could be implemented server side.

With that said, the web service implementation I’ve provided is for demonstration purposes only and will (almost assuredly) vary in some aspects from what you will encounter on the BOE platform. If there are any major differences please leave a comment or open an issue on the GitHub repository and I will do whatever I can to accommodate.

Requirements and Installations

You will need to have the latest version of Python 3.X installed on your system. You’ll also need to install the Bottle, lxml and request packages, which can be done using pip:

pip install bottle lxml requests

Pip comes pre-installed on Python versions 3.4 and up. If you are running on an older version you will need to install pip separately.

You’ll also want to download the code samples from my BOESDKParser GitHub repository. If you have git installed, you can clone the repository:

git clone git@github.com:WillAyd/BOESDKParser.git

Otherwise simply go to the link and download as HTML to your local system.

Optionally if you want easy export to commonly reportable file formats like CSV and Excel you should install the pandas library. Be sure to reference their installation instructions for details on how to do that.

Understanding the Application

Using the Sample Web Framework

The BOESDKParser provides two modules:

  • sample_framework.py
  • sdk_parser.py

The sample_framework module is a very lightweight web framework implemented using the Bottle package. Here’s some abbreviated code from that module:

from bottle import route, run, request, response

@route('/biprws/logon/long')
def login():
    return ('<attrs xmlns="http://www.sap.com/rws/bip">'
            '<attr name="userName" type="string"></attr>'
            '<attr name="password" type="string"></attr>'
            '<attr name="auth" type="string" possibilities="secEnterprise,secLDAP,secWinAD,secSAPR3">secEnterprise</attr>'
            '</attrs>')

run(host='localhost', port=8080)

So what exactly is this doing? When the module is loaded, it runs the “run” method from the bottle package, which is responsible for starting a web server with the provided host and port. In this case, once this module is loaded it will fire up a web server at http://localhost:8080/.

We’ve also defined a login function with the “@route(‘/biprws/logon/long’)” decorator. If you visit http://localhost:8080/biprws/logon/long while the server is running, Bottle will execute the login method, returning the XML in the function body. For more information on how that framework operates, be sure to check out the Bottle documentation!

You’ll notice there are two methods called login, with the other one looking as follows:

@route('/biprws/logon/long', method='POST')
def login():
    ...

There difference is the “POST” parameter being specified in the decorator. By default the decorator maps the “GET” method to the function underneath it. By specifying “POST” above, we can have two requests going to http://localhost:8080/biprws/logon/long mapped to two different functions, based purely on the type of HTTP method being used. You’ll see that this is exactly what the “To Log on to the BI platform” section in the SAP BusinessObjects RESTful Web Service SDK User Guide for Web Intelligence and the BI Semantic Layer asks you to do to get a logon token.

We’ll dive further into that in the next section as we review the sdk_parser module. For now, after you’ve looked at the sample_framework module go ahead and fire up your server from the command line by navigating to the module and running:

python sample_framework.py

If successful, you should see the following:

Bottle v0.12.12 server starting up (using WSGIRefServer())...
Listening on http://localhost:8080/
Hit Ctrl-C to quit.

Interacting with the Sample Web Framework

Importing and Initializing a BOESDKParser Instance

With a web server running, we’ll shift our focus to the sdk_parser module. Within that you’ll see a class definition for an object named BOESDKParser.

class BOESDKParser():
    def __init__(self, protocol='http', host='localhost', port='8080',
                 content_type='application/xml'):
        base_url = protocol + '://' + host + ':' + port
        self.bip_url = base_url + '/biprws'
        self.webi_url = self.bip_url + '/raylight/v1'
        self.sl_url = self.bip_url + '/sl/v1'
        self.headers = {
            'accept' : content_type
            }

The default arguments for the parser initialization match the defaults specified in the sample_framework. If you modified anything in that module before starting, be sure to account for that when initializing a BOESDKParser instance. You’ll also notice that the class builds the URLs described in the Default Base URLs section of the BOE REST API, with one for the BI Platform, one for the Web Intelligence Layer and one for the BI Semantic Layer.

Assuming you can stick with the defaults, let’s go ahead and import this module and create an instance of the BOESDKParser. Launch a Python shell in the same directory as this module and run:

from sdk_parser import BOESDKParser

parser = BOESDKParser()

Understanding the Logon Methods

If you’ve read through the SAP RESTful API documentation, you’ll notice that the first step you need to do to use the services is get a logon token from the server. The BOESDKParser implements this as follows:

    def _get_auth_info(self):
        return requests.get(self.bip_url + '/logon/long',
                            headers=self.headers)

    def _send_auth_info(self, username, password):
        '''Helper function to retrieve a log in token'''
        root = etree.fromstring(self._get_auth_info().text)
        root[0].text = username
        root[1].text = password

        return requests.post(self.bip_url + '/logon/long',
                             headers=self.headers,
                             data=etree.tostring(root))

    def set_logon_token(self, username, password):
        resp = self._send_auth_info(username, password)
        if resp.status_code == 200:
            root = etree.fromstring(resp.text)
            # Set logon token in headers
            self.headers['X-SAP-LogonToken'] = root[0].text
        else:
            # Crude exception handling
            raise Exception("Could not log on and set the logon token!")

The underscore preceding the first two methods is an indication that these methods are “private” and should not be called directly (although nothing in Python prevents you from doing so). The main entry point here is the set_logon_token method. When called with a username and password (we’ll be using “myUserName” and “myPassword”), it passes that information to _send_auth_info which itself calls _get_auth_info.

That method makes the first request going to http://localhost:8080/biprws/logon/long (again, assuming you’ve stuck with the default host / port). If you check back to the sample_framework module, you’ll notice that when called with GET that method returns XML, which looks as follows:

<attrs xmlns="http://www.sap.com/rws/bip">
    <attr name="userName" type="string"/></attr>
    <attr name="password" type="string"></attr>
    <attr name="auth" type="string" possibilities="secEnterprise,secLDAP,secWinAD,secSAPR3">secEnterprise</attr>
</attrs>

The result of _get_auth_info is sent back to the first line in _send_auth_info as a response object:

root = etree.fromstring(self._get_auth_info().text)

Here we access the “.text” attribute of the response object to focus on the XML contents (the request object comes with a lot of other information that we don’t need for this demo). We then use the etree class from the lxml package, calling it’s “fromstring” method to take that XML and convert into an object that we can more easily interact with.

        root[0].text = username
        root[1].text = password

The above two lines of code access the first and second child nodes of that XML, respectively, and sets their text to the username and password that are provided, which would yield something as follows:

<attrs xmlns="http://www.sap.com/rws/bip">
    <attr name="userName" type="string"/>myUserName</attr>
    <attr name="password" type="string">myPassword</attr>
    <attr name="auth" type="string" possibilities="secEnterprise,secLDAP,secWinAD,secSAPR3">secEnterprise</attr>
</attrs>

We need to send that information to get a logon token back. By POSTing to ‘/biprws/logon/long’ instead of using the GET method, we can access the second login function defined in sample_framework.py. That reads the request body and checks that the provided username is “myUserName” while the provided password is “myPassword.” If that is the case, it will return a logon token. Here’s how that all works (from sample_framework.py):

@route('/biprws/logon/long', method='POST')
def login():
    body = request.body.read()
    root = etree.fromstring(body)
    user, pw = root[0].text, root[1].text
    ...
    if user == 'myUserName' and pw == 'myPassword':
        return ('<attrs xmlns="http://www.sap.com/rws/bip">'
                '<attr name="LogonToken" type="string">COMMANDCOM-LCM:'
                '6400@{3&amp;2=5595,U3&amp;p=40674.9596541551,Y7&amp;4F=12,U3&amp;63=secEnterprise,0P&amp;66=60,03&amp;68='
                'secEnterprise:Administrator,'
                '0P&amp;qe=100,U3&amp;vz=SFY6agrLPxpfQBK1ZKYCwoBZKCbfsQm7VgWZFiH.RhM,UP</attr>'
                '</attrs>')

    # Fall back to a 401
    response.status = 401
    return 'Could not authenticate user with provided credentials'

This response makes its way back to the set_logon_token method, where it checks that the response returned successfully (i.e., with a 200 status code). Assuming so, our BOESDParser instance parsers out the logon token and stores it in its headers dict.

If the above seemed very complicated, don’t worry! The parser abstracts all of that for you, so after instantiating your object all you really need to do is call

parser.set_logon_token("myUserName", "myPassword")

and go on your merry way to the next section.

Retrieving a List of Universes

After logging on, you can use the get_universes method to pull a listing of sample universes from the sample web server. Here’s how that method is implemented:

    def get_universes(self):
        resp = requests.get(self.webi_url + '/universes', headers=self.headers)
        if resp.status_code == 200:
            root = etree.fromstring(resp.text)
            # Iterate over the children elements and convert them into a dict
            # of dicts
            univs = dict()
            for index, univ in enumerate(root):
                univs[index] = dict()
                for child in univ:
                    univs[index][child.tag] = child.text

            return univs
        else:
            # Crude Exception handling
            raise Exception(('Could not retrieve universes - have you set a '
                             'valid logon token?'))

What’s going on here? Well, first we make a call to self.webi_url + ‘/universes’, which expands out to http://localhost:8080/biprws/raylight/v1/universes. We also need to send our headers, which after having logged on should now look something as follows:

{
    'accept' : content_type
    'X-SAP-LogonToken' : 'COMMANDCOMLCM:6400@{3&2=5595,U3&p=40674.9596541551,Y7&4F=12,U3&63=secEnterprise,0P&66=60,03&68=secEnterprise:Administrator,0P&qe=100,U3&vz=SFY6agrLPxpfQBK1ZKYCwoBZKCbfsQm7VgWZFiH.RhM,UP'
}

The X-SAP-LogonToken is what the server will use to validate our session, so without it you will not be allowed to retrieve the universes that you want. With that however, we should get a response object that looks as follows:

<universes>
    <universe>
        <id>6773</id>
        <cuid>AXyRzvmRrJxLqUm6_Jbf7lE</cuid>
        <name>efashion.unv</name>
        <type>unv</type>
        <folderId>6771</folderId>
    </universe>
    <universe>
        <id>5808</id>
        <cuid>AUW2qRdU0IdPkyhlpZWrxvo</cuid>
        <name>Warehouse.unx</name>
        <type>unx</type>
        <folderId>5807</folderId>
    </universe>
</universes>

Just like before, we convert that XML response into an etree and iterate over the nodes. We are going to build out a dictionary to store that XML information, and to do such we enumerate the children of the root note (i.e. the two universe tags). We set our initial key in the dict to that enumerated value, and set its value to another dict mapping all the id, cuid, name, type and folderId information for that universe. Ultimately, that will yield something as follows:

{0: {'cuid': 'AXyRzvmRrJxLqUm6_Jbf7lE',
     'folderId': '6771',
     'id': '6773',
     'name': 'efashion.unv',
     'type': 'unv'},
 1: {'cuid': 'AUW2qRdU0IdPkyhlpZWrxvo',
     'folderId': '5807',
     'id': '5808',
     'name': 'Warehouse.unx',
     'type': 'unx'}}

With the explanation aside, let’s go ahead and just call the get_universes method and store its output in a local variable called univs. That alone will be a dict that you can use to access information, but if you’ve installed the optional pandas dependency you can easily convert that dict into a DataFrame.

univs = parser.get_universes()

import pandas as pd
df = pd.DataFrame.from_dict(univs, orient='index')

The DataFrame looks something as follows:

     id                     cuid           name type folderId
0  6773  AXyRzvmRrJxLqUm6_Jbf7lE   efashion.unv  unv     6771
1  5808  AUW2qRdU0IdPkyhlpZWrxvo  Warehouse.unx  unx     5807

Which we can easily export to CSV as such:

df.to_csv('some_file.csv')

Bringing it All Back Home

The above section has gone over in some detail how all of the methods here have been implemented. To sum it all up however, with the web server running we can “log on,” get a list of universes and optionally drop that list of universes to CSV all with the following lines of code.

from sdk_parser import BOESDKParser
parser = BOESDKParser()

parser.set_logon_token("myUserName", "myPassword")
univs = parser.get_universes()

# Optionally write out to a pandas DataFrame
import pandas as pd
df = pd.DataFrame.from_dict(univs, orient='index')
df.to_csv('some_file.csv')

Conclusion

Thanks for taking the time to read through this document. As mentioned, the items provided here may not perfectly replicate what you will see when communicating with a real BOE environment, but I hope that you find value in understanding at a high level how to make requests to a server via Python and also how the framework roughly interprets and responds to the requests that you make.

Should you have any ideas or thoughts on how to improve this document, or if there are any particular code samples you’d like to see spelled out (knowing this document only focused on pulling Universe information) please either leave a comment below or open an issue on the BOESDKParser GitHub repository page.

Assigned Tags

      18 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Srdjan Boskovic
      Srdjan Boskovic

      Nice example

      Author's profile photo Satya Vara Kalla
      Satya Vara Kalla

      hi,

      while trying to install  "pip install bottle lxml requests", we are getting this error for just lxml package saying "unable to find vcvarsall.bat".

      Would you know how to avert this error or any other way to access these packages without pip?

       

      Author's profile photo William Ayd
      William Ayd
      Blog Post Author

      I am not a Windows expert but I think that error is a result of not having a compiler on your system. In that case it won't matter whether you install via pip, another package manager or even try installing from source.

      Here's a SO article for that issue - check it out and see if that helps:

      https://stackoverflow.com/questions/2817869/error-unable-to-find-vcvarsall-bat

       

      Author's profile photo Jon Fortner
      Jon Fortner

      When I use

      parser.set_logon_token("myUserName", "myPassword")

      it says I'm missing 1 positional argument 'password'. No matter what I put in for self, it doesn't like it. What should be used for self?

      Author's profile photo William Ayd
      William Ayd
      Blog Post Author

      Did you instantiate the parser first via

      parser = BOESDKParser()

       

      self is implicitly provided as the first argument when dealing with class instances in Python

      Author's profile photo Mhamed Deghaies
      Mhamed Deghaies

      Hi William,

      I'd appreciate if u can help me to figure out why I get the output below although I've literally copied the code:

      from sdk_parser import BOESDKParser
      parser = BOESDKParser()
      
      parser.set_logon_token("myUserName", "myPassword")
      univs = parser.get_universes()
      
      # Output
      
      127.0.0.1 - - [28/Oct/2019 16:08:20] "GET / HTTP/1.1" 404 720

      Thank you

      Author's profile photo William Ayd
      William Ayd
      Blog Post Author

      A 404 would be an authentication error so check the username and password you are providing 

      Author's profile photo Rohit Pawar
      Rohit Pawar

      Hi William,

      I have started learning RESTful Web Service SDK and found this very useful , I have two question,

      1) Is there any dependency between sample_framework.py & sdk_parser.py because in final section you have only called sdk_parser

      2) Do we need to update local host parameter in script in order to work it for my BO environment

       

      Author's profile photo William Ayd
      William Ayd
      Blog Post Author
      1. The sample_framework is an actual web server run locally and the sdk_parser is what will parse the response provided by that server
      2. Correct - modify to suit your environment
      Author's profile photo Justin Skinner
      Justin Skinner

      William, great article.  Very helpful!  I am getting errors when I attempt to adjust the localhost to my enterprise server.  I know that I can successfully send GET or POST requests to this server, as I have performed tests using the Talend API Tester.  Is there anything else required other than:

      • Update host in sdk_parser.py from localhost to my remote enterprise server
        • (tried with and without the ".MyDomain.com")
        • def __init__(self, protocol='http', host='MyBOServer.MyDomain.com', port='8080',
      • Update the parser.set_logon_token values to my username and password for this server
        • parser.set_logon_token("MyUserName", "MyPassword")
      • Error
        • parser.set_logon_token("MyUserName", "MyPassword")
          ---------------------------------------------------------------------------
          ConnectionRefusedError                    Traceback (most recent call last)
          ~\.........urllib3\connection.py in _new_conn(self)
              155         try:
          --> 156             conn = connection.create_connection(
              157                 (self._dns_host, self.port), self.timeout, **extra_kw
          
          ~\.........\urllib3\util\connection.py in create_connection(address, timeout, source_address, socket_options)
               83     if err is not None:
          ---> 84         raise err
               85 
          
          ~\.........\urllib3\util\connection.py in create_connection(address, timeout, source_address, socket_options)
               73                 sock.bind(source_address)
          ---> 74             sock.connect(sa)
               75             return sock
          
          ConnectionRefusedError: [WinError 10061] No connection could be made because the target machine actively refused it
          
          During handling of the above exception, another exception occurred:
      Author's profile photo William Ayd
      William Ayd
      Blog Post Author

      What happens if you just try requests.get("some_url") ? It seems like something is off with how your server expects you to communicate

      Author's profile photo Justin Skinner
      Justin Skinner

      If i run this against my enterprise server I get a 200 response:

      requests.get('http://servername.domainname.com:8080/biprws/logon/long')

       

      I can also run this and get a 200:

      requests.get('http://www.finnhub.io')

       

      Is that what you mean?

       

      Justin

       

      Author's profile photo Justin Skinner
      Justin Skinner

      Is it possible that I need to add an attribute to one of the functions in the sdk_parser script to handle the auth part, which should carry a value of "secEnterprise"?  If i ping my server using requests, this is the _content.

      r = requests.get('http://MyServer.MyDomain.com:8080/biprws/logon/long')...
      b'<attrs xmlns="http://www.sap.com/rws/bip"><attr name="password" type="string"></attr><attr name="clientType" type="string"></attr><attr name="auth" type="string" possibilities="secEnterprise,secLDAP,secWinAD,secSAPR3">secEnterprise</attr><attr name="userName" type="string"></attr></attrs>'
      Author's profile photo William Ayd
      William Ayd
      Blog Post Author

      From what I’ve read this might be an issue on the server instead of the client

      https://stackoverflow.com/questions/12993276/errno-10061-no-connection-could-be-made-because-the-target-machine-actively-re#12993494

      I am unfortunately not well versed on Windows servers so I don’t have anything super insightful to offer, but if you can figure it out would definitely be interested in hearing what needed to change.

      Author's profile photo Eric Hughes
      Eric Hughes

      I learned Python last week, so bare with me in that I barely know what I’m talking about.  Feel free to ignore this.  I am using python 3.7 in Pycharm 2019.3.  The following are things I changed in sdk_parser.py to get it to work when connecting to my Business Objects server (running windows):

      under “def __init__” I changed ‘access’ to ‘Content-Type’ and called it self.headers_auth. Then I blanked out the value of self.headers. See below.


      self.headers_auth = { 'Content-Type' : content_type } self.headers = { } print("Begin __init__")

       

      Under “def _send_auth_info”, I changed it to use the new headers_auth  that has the Content-Type. Also the root[X].text parsing was incorrect for the XML that I was getting back and had to change the number.  This fixed the secEnterprise not showing up as well. (not real sure how that works – read up on it here https://docs.python.org/2/library/xml.etree.elementtree.html).  The code I downloaded expects the XML to have the nodes username, password, and auth in that order, but my server returns those values in the order of password, auth, username.

      def _send_auth_info(self, username, password):
          '''Helper function to retrieve a log in token'''
          root = etree.fromstring(self._get_auth_info().text)
          root[2].text = username
          root[0].text = password
          return requests.post(self.bip_url + '/logon/long',
                               headers=self.headers_auth,
                               data=etree.tostring(root))
      

       

      Under “set_logon_token”, the root/etree thing was incorrect for me based on the XML I received from my server. The token is the 1st node (0-attr), and is a child of the 1st node (0-attrs), and is a grandchild of the 5th Node (4-content) of the root So I changed self.headers to the following that that it would pull the token correctly from the XML

      self.headers["X-SAP-LogonToken"] = root[4][0][0].text

      --SAMPLE XML returned
      <entry xmlns="http://www.w3.org/2005/Atom">
      <author>
      <name>@BOGUSNAMEHERE</name>
      </author>
      <id>tag:sap.com,2010:bip-rs/logon/long</id>
      <title type="text">Logon Result</title>
      <updated>2029-02-14T05:13:29.989Z</updated>
      <content type="application/xml">
      <attrs xmlns="http://www.sap.com/rws/bip">
      <attr name="logonToken" type="string">BOGUSSSERVER.BOGUSDOMAIN.com:1234@{3&amp;2=48812486,U3&amp;2v=BOGUSSSERVER.BOGUSDOMAIN.com:1234:1234,UP&amp;66=60,U3&amp;68=secEnterprise:Administrator,UP&amp;S9=12,U3&amp;qe=100,U3&amp;vz=AeSacv_BOGUSTOKENVALUE_DaDEt3f,UP}</attr>
      </attrs>
      </content>
      </entry>

       

      Under “get_universes”. I removed “headers=” in the “headers=self.headers” statement to make it that line look like the following.

      resp = requests.get(self.webi_url + '/universes', self.headers)

      I did get an error with get_universes regarding Unicode strings with encoding declaration are not supported. But I can do a print(resp.text) to see the result in that module.

      I think that is all I changed. 

      My 2 cents as a total newb (take with grain of salt).  It was confusing figuring out the purpose of sample_framework.  I wasn’t expecting that someone would create program to spoof a web service API of a business objects server.  Its cool that it can be done, but its not super useful.   I’m over generalizing, but if you didn’t have a Business Objects server to hit, its not likely you would care anything about the code here.

      I do appreciate the work done on this as it has given me a head start to accomplish what I need.

      Author's profile photo Justin Skinner
      Justin Skinner

      I ended up having to make similar updates as Eric, which gave me a great opportunity to learn how to manipulate XML using etree, as well as become more comfortable with Python altogether.

      Thanks to you both for helping me get this working!

      JS

       

       

      Author's profile photo Justin Skinner
      Justin Skinner

      William, thanks again for producing great content.  I have completed this exercise in my environment, as well as your post on How to Measure Report Similarity Using Python, which was equally informative.  I've also read your post on How to List Out Dimensions / Measures of Reports in BOE 3.X and am wondering if you could direct me to any existing documentation around how to List out Dimensions and Measures of Reports for BO 4.2 using Python 3 instead of Java?  I know one person that would benefit if you were to update that 3.X info to 4.2 using Python!

      JFS

       

      Author's profile photo Matthew Moore
      Matthew Moore

      Hi Will,

      Thanks for the write up. I'm trying to create a new schedule for a crystal report on 4.3 but I keep getting a 415 unsupported media type error no matter what I try. Any idea what I'm doing wrong? I also created a post you can review here for more detail: https://answers.sap.com/questions/13691354/restful-api-bo-43-415-unsupported-media-error-from.html

      url = 'http://redacted:8080/biprws/v1/documents/3127507/schedules'
      
      headers = {
          "Accept":"application/json",
          "X-SAP-LOGONTOKEN":f"{token}",
          "Content-Type": "application/xml"
      }
      
      body = """
      <schedule>
          <name>RevintHB-Account</name>
          <format type="csv"/>
          <status id="9">Recurring</status>
          <destination keepInstanceInHistory="true">
              <useSpecificName fileExtension="false">%SI_NAME%_%SI_STARTTIME%.txt</useSpecificName>
              <mail>
                  <from>redacted@myorg.org</from>
                  <to>redacted@myorg.org</to>
                  <cc>redacted@myorg.org</cc>
                  <subject>%SI_NAME%</subject>
                  <message>hello this is the message</message>
                  <addAttachment>true</addAttachment>
              </mail>
          </destination>
          <hourly retriesAllowed="0" retryIntervalInSeconds="1800">
              <startdate>2022-04-30T16:00:00.000Z</startdate>
              <enddate>2031-03-10T18:38:00.000Z</enddate>
              <hour>1</hour>
              <minute>0</minute>
          </hourly>
      </schedule>
      """
      resp = requests.post(url, body, headers=headers)
      print(resp)