Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
TSTOFFELS
Explorer
This article will describe the SAP Solution Manager's Process Management API, give an example of how it can be used to export the structure of branch to CSV/Excel and concludes with an outlook regarding further potential use-cases.

What is the Process Management API?


The Process Management API offers all the necessary building blocks to read/write to almost any solution documentation elements (Test Steps Test cases are not supported though). Obviously the solution manager’s authorization/change control mechanisms still apply.

Resource Model + Content Model (List of API Calls):

https://wiki.scn.sap.com/wiki/display/SM/Process+Management+API

Description of Exchange Format:

https://wiki.scn.sap.com/wiki/display/SM/Exchange+format

Getting started - Basic example:


We will demonstrate how to obtain a list of solutions using the SolutionSet() API call. To do this, simply (after replacing the <placeholders> according to your solution manager) open the below URL in a browser. You will be asked for your username/password.

URL: http://<hostname>:50000/sap/opu/odata/sap/ProcessManagement/SolutionSet?sap-client=<client>&$format=...

Result:



The resulting JSON is in fact a full list of solutions. It tells us, that several solutions have been created in this solution manager. One of those solutions (the one expanded) goes by the Name “DELETE_ME”.

Using the “SolutionId”, (the solution’s  unique identifier) we could now dig deeper by using the BranchSet() API Call, to obtain a list of branches of the “DELETE_ME” Solution:

URL:
http://<hostname>:50000/sap/opu/odata/sap/ProcessManagement/BranchSet?$filter= SolutionId eq '051MZjd97jUdYfSEOG}k10' &sap-client=<client>&$format=json

Result:


A List of the "DELETE_ME" solution's branches, each with name, type and id. With the BranchID, we will be able to read the full content structure of a branch through the BranchContentSet() API Call.

Exporting a full branch structure


At this point all that is left to do, is to identify the ROOT node of the tree structure that comprises a branches content and then walk through it (the JSON, that is) along the parent/child relationships as follows:

  • Process (output) the current node.

  • Process (output) all children.


This obviously cannot be done by typing requests in a browser anymore, but rather requires a small program.
For this prototype I chose Python 3, as it natively supports JSON, offers a convenient way handle http requests and is widely available / has a large community.

In python:
def map_branch(branch_id, node_id, depth):
#recursively walks through a branch and outputs all elements
#1: output the current node
sep = '\t' #separator
node = get_node_by_id(branch_id, node_id)
description = ''.join(get_node_attribute_values(node, 'DESCRIPTION') + get_node_attribute_values(node, '_DESCRIPTION'))
name = get_node_name_by_id(branch_id, node_id)
print('\t' * depth + node['obj_type'], sep, name)
#2: do the same for all children
for child_id in get_children_ids(branch_id, node_id):
map_branch(branch_id, child_id, depth + 1)

To make this more convenient and reusable I included a set of functions, which encapsulate the API calls such as get_node_by_id() or get_children_ids().

Full source code is available below.

 

Content of the “DELETE_ME” solution’s the “Development” Branch:





Our “DELETE_ME” solution’s development branch contains Process “My Process” (containing a Functional Spec document, an Executable, 3 Test Cases and a Test Configuration) as well as a Process Step Reference “My Step” (containing another Func Spec document, two test case documents and another test case document inherited from the original process step).

We also will have to adjust the hostname, port, sap-Client, username, password, solution and branch variables in the demo code (below). The resulting output…



…can be copy/pasted to excel, to make it more readable:



The exported structure of the solution documentation is clearly recognizable.

We also notice, that there are some additional nodes, grouping sub-elements which are not displayed in the solution documentation. For example the process step references (REF_PROCSTEP) are not immediate sub elements of the process (PROC). Instead there is a node of type PROCSTEPGRP between the two, encapsulating all process step references. Same with nodes of type DOCUGRP, TESTGRP and EXECGRP encapsulating documents, test-resources and executables beneath process(-step reference) nodes.

This makes uploading/creating elements  particularly more difficult, as you will have to check whether or not there already is a fitting grouping node available (and if there is none, create one).

We also see, that the document inherited from the step original is not listed (as it is a child of the step original).

Conclusion & Outlook:


As the API allows not only possible to read / download structure but also documents/content. It also offers write/update-access to virtually all Solution Documentation elements – suggesting it can be used the following use-cases:

  • Mass-downloading documents (not the structure, but content – aka documents themselves)

  • Mass-updating Solution Documentation elements (i.e. change status of all test documents under scenario X in branch Y to “Released”)

  • Mass-uploading documents (i.e. Mass-upload documents to specific processes-nodes based on a CSV containing process-name and file name of the document(s) to upload).
    Uploading a document via the Process Management API would involve three steps:

    • Create an (empty) Knowledge Warehouse Object (“KWOBJ”)

      • API Call: CreateDocument



    • Upload content (the actual document) to the KWOBJ

      • API Call: DocumentContentImporter



    • Create an Entry in a Process Management Branch, which points to the new KWOBJ

      • API Calls: BranchContentImporter, BranchContent (as a prerequisite)






 

Full Source Code:


import json
import requests
import pprint

host = 'SOLMAN.HOSTNAME' #SolMan Hostname or IP
port = '50000' #Port (most likely 5000)
sap_client = '800' #Client

user = 'USERNAME' #Username
passwd = 'PASSWORD' #Password

solution_name = 'DELETE_ME' #Solution Name
branch_name = 'DEVELOPMENT' #Branch Name


def get_solution_by_name (solution_name):
#returns a list of solutions by name
try:
response = requests.get('http://'+ host + ':' + port + '/sap/opu/odata/sap/ProcessManagement/SolutionSet?sap-client=' + sap_client, auth=(user, passwd), headers=headers)
response.raise_for_status()
except requests.exceptions.HTTPError as errh:
print("Http Error:", errh)
except requests.exceptions.ConnectionError as errc:
print("Error Connecting:", errc)
except requests.exceptions.Timeout as errt:
print("Timeout Error:", errt)
except requests.exceptions.RequestException as err:
print("Undefined Request Exception:", err)
return list(filter(lambda s: s['SolutionName'] == solution_name, response.json()['d']['results']))

def get_branch_by_name (solution_id, branch_name):
#returns a list of branches by name and solution_id
response = requests.get('http://'+ host + ':' + port + '/sap/opu/odata/sap/ProcessManagement/BranchSet?sap-client=' + sap_client, auth=(user, passwd), headers=headers)
return list(filter(lambda b: b['BranchName'] == branch_name and b['SolutionId'] == solution_id, response.json()['d']['results']))

def get_node_attribute_values(my_node, attribute_name):
#returns the value of an attribute of a node_id
res = []
for my_attribute in my_node['attributes']:
if my_attribute['attr_type'] == attribute_name: res = my_attribute['values']
return res

def get_node_by_type(branch_id, node_type):
#returns a list of nodes in a branch, having the specified node type
response = client.get('http://'+ host + ':' + port + '/sap/opu/odata/sap/ProcessManagement/BranchContentSet(BranchId=\'' + branch_id + '\',ScopeId=\'SAP_DEFAULT_SCOPE\',SiteId=\'\',SystemRole=\'D\')/$value?sap-client=' + sap_client ,auth=(user, passwd),headers=headers)
nodes_list = json.loads(list(filter(lambda s: s['section-id'] == 'NODES', response.json()['sections']))[0]['section-content'])
return list(filter(lambda n: n['obj_type'] == node_type, nodes_list))

def get_children_ids(branch_id, node_id):
#returns the list of children of a given node in a given branch
response = client.get('http://'+ host + ':' + port + '/sap/opu/odata/sap/ProcessManagement/BranchContentSet(BranchId=\'' + branch_id + '\',ScopeId=\'SAP_DEFAULT_SCOPE\',SiteId=\'\',SystemRole=\'D\')/$value?sap-client=' + sap_client ,auth=(user, passwd),headers=headers)
nodes_structure_list = json.loads(list(filter(lambda s: s['section-id'] == 'NODES-STRUCTURE', response.json()['sections']))[0]['section-content'])
nodes_structure_list_filtered = list(filter(lambda x: x['parent_occ'] == node_id, nodes_structure_list))
if len(nodes_structure_list_filtered) == 0:
return []
else:
return list(filter(lambda x: x['parent_occ'] == node_id, nodes_structure_list_filtered))[0]['children']

def get_node_by_id(branch_id, node_id):
#returns the specified node (by node_id and branch_id)
response = client.get('http://'+ host + ':' + port + '/sap/opu/odata/sap/ProcessManagement/BranchContentSet(BranchId=\'' + branch_id + '\',ScopeId=\'SAP_DEFAULT_SCOPE\',SiteId=\'\',SystemRole=\'D\')/$value?sap-client=' + sap_client ,auth=(user, passwd),headers=headers)
nodes_list = json.loads(list(filter(lambda s: s['section-id'] == 'NODES', response.json()['sections']))[0]['section-content'])
return list(filter(lambda x: x['occ_id'] == node_id, nodes_list))[0]

def get_node_name_by_id(branch_id, node_id):
#returns a node's name (based on node_id and branch_id)
response = client.get('http://'+ host + ':' + port + '/sap/opu/odata/sap/ProcessManagement/BranchContentSet(BranchId=\'' + branch_id + '\',ScopeId=\'SAP_DEFAULT_SCOPE\',SiteId=\'\',SystemRole=\'D\')/$value?sap-client=' + sap_client ,auth=(user, passwd),headers=headers)
names_list = json.loads(list(filter(lambda s: s['section-id'] == 'ELEMENT-NAMES', response.json()['sections']))[0]['section-content'])
names_list_filtered = list(filter(lambda n: n['occ_id'] == node_id, names_list['D']))
if len(names_list_filtered) == 0: return ''
else: return names_list_filtered[0]['name']

def map_branch(branch_id, node_id, depth):
#recursively walks through a branch and outputs all elements
#1: output the current node
sep = '\t'
node = get_node_by_id(branch_id, node_id)
description = ''.join(get_node_attribute_values(node, 'DESCRIPTION') + get_node_attribute_values(node, '_DESCRIPTION'))
name = get_node_name_by_id(branch_id, node_id)
print('\t' * depth + node['obj_type'], sep, name)
#2: do the same for all children
for child_id in get_children_ids(branch_id, node_id):
map_branch(branch_id, child_id, depth + 1)

client = requests.session()
headers = {'X-CSRF-Token': 'Fetch', 'Accept': 'application/json', 'Content-Type': 'application/json'}

#get solution id (by name), branch id (by name) and root node (by type)
solution_id = get_solution_by_name(solution_name)[0]['SolutionId']
branch_id = get_branch_by_name(solution_id, branch_name)[0]['BranchId']
root_node = get_node_by_type(branch_id, 'ROOT')

#map branch starting at root node
map_branch(branch_id, root_node[0]['occ_id'], 0)
7 Comments