Skip to Content
Technical Articles

Serving Fiori Applications with Python Flask, part 1

Many of us may know Python as a programming language with data science libraries such as Tensorflow for building Deep Neural Networks, Pandas for data preparation and agile analytics, or Jupyter for putting it all together in a digital notebook. Some of us may have already coded several microservices for SAP Data Hub using Python operators within a pipeline. But did you know that Python can serve Fiori Applications?

Python Flask is a lightweight web application framework which supports RESTful API and MVC (Model, View, Controller) paradigms required by many projects where scalable integration is required. With a few adjustments to existing Fiori templates, you can build applications locally and deploy globally. Although the Flask web server is not production quality, when it is placed within an Apache framework then it can provide a robust platform as seen in use today with massive digital first companies like Netflix, Lyft, and Reddit. In addition it is also used in SAP Extended Services Advanced (XSA) and SAP Cloud Platform (SCP).

Why would you be interested in this series?

  • You are struggling to locally develop Fiori apps
  • You don’t have connections to ABAP or HANA
  • You want to try something new and unopinionated.
  • You really like SAP Data Hub and want to see what else Python can do

What you should know to complete this series?

  • Basic Python, Javascript, HTML/XML
  • Familiarity with Fiori resources, specifically hana.ondemand.com.
  • Access to Github to access sample code.

In this first of a multi-part series, we will focus on the basic project structure and changes to Fiori application files required to serve a basic template application. By the last part in the series, we will show how SAP HANA, OrientDB, and innovative use cases are integrated into a fully productive web application.

The sample application contains a simple launchpage with tile:

…and Master-Detail app with dummy date:

DISCLAIMER: This is not a guide on how to code Fiori applications nor is it an end-to-end tutorial on setting up web applications with Python Flask. There are plenty of resources that cover this far better than I can and I am happy to point any interested parties to them. To run the application and code, installing Python and Flask is required. This is rather an integration topic with pinpoint solutions to issues that I have not found elsewhere.

What is Flask?

Flask is a BSD licensed Python minimalistic web application framework based on Werkzeug and Jinja 2. A Flask project can consist of a single python file that initializes the app server and contains routes, models, and any supporting logic as shown in the Flask open source documentation. However, this is not scalable for production and therefore we break these functions into 3 different files.

#basic_template/run.py
from flaskr import app

if __name__ == "__main__":
    app.run(debug=True)

At the application parent directory is run.py which imports the app class from the Flask resources directory (flaskr) and runs default on port 5000. We set the debug configuration to true so that the server resets when changes are made to the code base. Other variables to serve on HTTP or HTTPS are available but won’t be covered at this stage.

#basic_template/flaskr/__init__.py
from flask import Flask

app = Flask(__name__)

import flaskr.routes

Within flaskr we have __init__.py which initializes the application and additionally makes the flaskr directory into a Python module which can be imported into other files. Also at this level is routes.py which provides the interface between the Fiori application and any back end resources we may have in the future such as SAP HANA or OrientDB.

#basic_template/flaskr/routes.py
from flask import render_template, jsonify
import time
from datetime import datetime
from . import app

@app.route('/')
def index():

    return render_template("index.html")

@app.route('/DashboardAnalytics', methods=["GET"])
def DashboardAnalytics():
    """
    Return a simple odata container with date time information
    :return:
    """
    odata = {
        'd': {
            'results': []
        }
    }
    i = 0
    names = 'abcdefghijklmnopqrxtu'
    while i < 20:
        odata['d']['results'].append({
            "id": i,
            "name": names[i],
            "datetime": datetime.fromtimestamp(time.time()).strftime('%Y-%m-%d %H:%M:%S'),

        })
        i+=1
    return jsonify(odata)

That’s it for the python required to run this demo and in less than 50 lines which includes a small function to generate dummy data in JSON format thanks to Flask’s internal “jsonify” function. To start the application, navigate to the parent directory containing run.py and execute it with Python as seen below. If you have all dependencies installed you should see the server running on the local host.

> python run.py

In summary we have the following structure with 3 files distributed in 2 folders.

  • FioriFlask\run.py (Execution to start serving the application on port 5000)
  • FioriFlask\flaskr\ (resources for Flask)
    • __init__.py (initialize the app and directory as a python module)
    • routes.py (interface between python back end services and javascript based front end)

A Fiori project on the other hand requires several folders and hierarchies to manage user interactions. We won’t be exhausting the functionality at each level but instead focus on what needs to be changed from the traditional Fiori project structure for serving with Flask. In this case it is a single step that requires understanding 2 additional folders in our Flask project structure, templates and static.

  • FioriFlask\flaskr\templates\index.html
  • FioriFlask\flaskr\static\(all other Fiori Files)

The application will serve HTML files from the routes.py file and Flask’s  render_template function which points at the templates folder by default. Therefore, we need to move the index.html file into the templates folder. This is the typical case for single-page apps not only common in Fiori’s framework but also seen in other popular examples like React and Angular. In any index file case we modify it with pointers to the rest of the application content including controllers, views, fragments, css, and other customizing files. For Fiori this means changing only one line using Jinja.

Flask uses a templating engine called Jinja which allows Python logic to be implemented directly in static web files like HTML and is what we will be using to create the pointers within the index file. A Fiori index file is the container for the application content and is initialized with javascript that sets the content source, theme, resource roots and other options such as dynamic loading. The key line to change here is the value for data-sap-ui-resourceroots.

<!--basic_template/flaskr/templates/index.html-->
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>Basic Template</title>

	<script id="sap-ui-bootstrap"
		src="https://sapui5.hana.ondemand.com/resources/sap-ui-core.js"
		data-sap-ui-theme="sap_belize"
		data-sap-ui-resourceroots='{
			"sap.ui.demo.basicTemplate": "{{ url_for('static', filename='.')}}"}'
		}'
		data-sap-ui-oninit="module:sap/ui/core/ComponentSupport"
		data-sap-ui-compatVersion="edge"
		data-sap-ui-async="true"
		data-sap-ui-frameOptions="trusted">
	</script>
</head>

<body class="sapUiBody">
	<div data-sap-ui-component data-name="sap.ui.demo.basicTemplate" data-id="container" data-settings='{"id" : "basicTemplate"}'></div>
</body>
</html>

This line can contain a json format to identify several roots. However, for this example we want to ensure that there is a value identifying the namespace of the application and that it is set to the application’s static folder using Jinja syntax, specifically url_for and setting it to the static directory. You will notice the Jinja specific syntax through the double curly braces {{ }}. In our case this will be:

"sap.ui.demo.basicTemplate": "{{ url_for('static', filename='.')}}"}'

This will ensure that when the application server runs, it will set up its static directory so that all Fiori resources are accessible and integrated. That’s it for the initial setup and the only line of code necessary to ensure the application can run. The remaining files can be dropped directly as is into the static folder.

The next step is to ensure the application can make calls to the back end. For this we will use the press function of the tile within the App.controller.js. The press function takes the name of the tile pressed and uses that as the url to call to the back end. In the routes.py file we have defined an endpoint to match the name of that tile, DashboardAnalytics and it returns a simple JSON with some values to show it populated the application.

//basic_template/flaskr/static/controllers/App.controller.js
sap.ui.define([
	"sap/ui/core/mvc/Controller",
	"../model/formatter",
	"sap/ui/core/routing/History"
], function(Controller, formatter, History) {
	"use strict";

	return Controller.extend("sap.ui.demo.basicTemplate.controller.App", {

		formatter: formatter,

		onInit: function () {

		    self = this;

		},
		press: function(tile) {

			var selectedData = {};
			sap.ui.core.BusyIndicator.show(0);
			this.getData(tile).done(function(result) {
				var oModel = new sap.ui.model.json.JSONModel(result.d);
				sap.ui.getCore().setModel(oModel, "DashboardAnalyticsModel");
				self.routeToApp(tile);

			}).fail(function(result) {
				console.log(result);
			});

		},
		onNavBack: function() {

			var sPreviousHash = History.getInstance().getPreviousHash();

			if (sPreviousHash !== undefined) {
				history.go(-1);
			} else {
				this.getRouter().navTo("home", {}, true);
			}

		},

		getData: function(url){
			return jQuery.ajax({
				url: url,
				type: "GET"
			});
		},

		getRouter : function () {
			return sap.ui.core.UIComponent.getRouterFor(this);
		},

		routeToApp: function(tile) {
			self.getRouter().navTo(tile, {});

		},
	});
});

Instead of returning the index file as before with render_template, we just want to update the application without an entire re-load. Fiori has JQuery and Ajax built in to ensure we can do this, and Flask provides the pre-packaged function called, jsonify mentioned earlier that transforms Python dictionaries into a JSON that are then updated into the Fiori models. Therefore, once the button is pressed, it makes a GET call to the python routes and receives a JSON to populate the UI elements.

<!--basic_template/flaskr/static/view/App.view.xml-->
<mvc:View
	displayBlock="true"
	xmlns="sap.m"
	xmlns:mvc="sap.ui.core.mvc">
	<Shell>
		<App id="app" class="appBackground">
			<pages>
				<!--<Page title="{i18n>title}">-->
				<!--	<content></content>-->
				<!--</Page>-->
			</pages>
		</App>
	</Shell>
</mvc:View>

In Fiori and MVC in general, you will have separate controllers for each view. Above is the simple App view which is a container for all pages including the launchpad view below.

<!--basic_template/flaskr/static/view/Home.view.xml-->
<core:View controllerName="sap.ui.demo.basicTemplate.controller.App"
		   xmlns:core="sap.ui.core"
		   xmlns="sap.uxap"
		   xmlns:m="sap.m"
		   height="100%">
	<m:Page class="firstPage" title="Launchpad">
    	<m:customHeader class="NavToolbar">
    	    <m:Bar class="audienceSensorHeader">

    			<m:contentLeft>

    			</m:contentLeft>

    			<m:contentMiddle>
    				<m:Text id="titleText" class="titleTextHeader" text="Launchpad"/>
    			</m:contentMiddle>

    			<m:contentRight>
    			</m:contentRight>

    		</m:Bar>
	    </m:customHeader>

		<m:content>

		    <m:HBox class="mainPage" renderType="Bare">
				<ObjectPageLayout id="ObjectPageLayout">
					<sections>
						<ObjectPageSection id="section1" title="Section 1">
							<subSections>
								<ObjectPageSubSection id="OpsSection1" title="Operations">
									<!--<blocks>-->
										<core:Fragment fragmentName="sap.ui.demo.basicTemplate.view.Fragments.Operations" type="XML"/>
									<!--</blocks>-->
								</ObjectPageSubSection>
							</subSections>
						</ObjectPageSection>
					</sections>
				</ObjectPageLayout>
			</m:HBox>

		</m:content>
	</m:Page>
</core:View>

Here we see an example with the FlexibleColumnLayout which provides 3 main frames for navigation, header, and objective detail and its controller below. Together they form the basis for the Master-Detail Fiori pattern for apps.

<!--basic_template/flaskr/static/view/FlexibleColumnLayout.view.xml-->
<mvc:View id="Main" controllerName="sap.ui.demo.basicTemplate.controller.FlexibleColumnLayout"
          xmlns:mvc="sap.ui.core.mvc"
          displayBlock="true"
          xmlns="sap.f"
          xmlns:core="sap.ui.core"
          xmlns:m="sap.m">
    <m:Page class="secondPage" title="{i18n>title}">
        <m:customHeader class="NavToolbar">
            <core:Fragment fragmentName="sap.ui.demo.basicTemplate.view.Fragments.Header" type="XML"/>
        </m:customHeader>
            <m:content>
                <FlexibleColumnLayout id="fcl" initialMidColumnPage="start" layout="TwoColumnsMidExpanded">
                    <beginColumnPages>
                        <m:Page showHeader="false">
                            <m:content>
                            	<core:Fragment fragmentName="sap.ui.demo.basicTemplate.view.Fragments.Master" type="XML"/>
                            </m:content>
                        </m:Page>
                    </beginColumnPages>
                    <midColumnPages>
                        <m:Page id="start" showHeader="false">
                            <m:content>
                                <core:Fragment fragmentName="sap.ui.demo.basicTemplate.view.Fragments.Details" type="XML"/>
                            </m:content>
                        </m:Page>
                        <m:Page id="charts" showHeader="false">
                            <m:content>
                               <m:Text text="Two" />
                            </m:content>
                        </m:Page>
                    </midColumnPages>
                </FlexibleColumnLayout>
            </m:content>
        </m:Page>
</mvc:View>
//basic_template/flaskr/static/controller/FlexibleColumnLayout.controller.js
sap.ui.define([
	"sap/ui/demo/basicTemplate/controller/App.controller",
	"sap/ui/model/Filter",
	"sap/ui/model/FilterOperator",
	"sap/ui/model/json/JSONModel",
	"sap/m/MessageToast"
], function(AppController, Filter, FilterOperator, JSONModel, MessageToast) {
	"use strict";

	var self;
	return AppController.extend("sap.ui.demo.basicTemplate.controller.FlexibleColumnLayout", {

		onInit: function() {

			this.oModelSettings = new JSONModel({
				maxIterations: 200,
				maxTime: 500,
				initialTemperature: 200,
				coolDownStep: 1
			});
			this.getView().setModel(this.oModelSettings, "settings");
			this.getView().setModel(sap.ui.getCore().getModel("DashboardAnalyticsModel"), "DashboardAnalyticsModel");
			sap.ui.core.BusyIndicator.hide(0);
		}
	});
});

Fiori apps are highly configurable and scalable due their implementation through small files that are called into the mobile stack as needed. An example of this is the concept of fragments which are individual pieces of a page that can be re-used across the application. Below are the fragments required to render the basic objects in the Master-Detail app pattern.

<!--basic_template/flaskr/static/view/Fragments/Details.fragment.xml-->
<core:FragmentDefinition
        xmlns="sap.m"
        xmlns:f="sap.f"
        xmlns:core="sap.ui.core"
        xmlns:u="sap.ui.unified"
        xmlns:t="sap.ui.table"
        xmlns:layout="sap.ui.layout">
	<f:DynamicPage id="dynamicPageId" preserveHeaderStateOnScroll="true" headerExpanded="{/headerExpanded}">
		<!-- DynamicPage Title -->
		<f:title>
			<f:DynamicPageTitle>
				<f:heading>
					<Title text="Entity name: {SELECTED_ATTACHMENT>/pname}"/>
				</f:heading>
				<f:expandedContent>
					<Label text="Age: {SELECTED_ATTACHMENT>/age}"/>
				</f:expandedContent>

				<f:actions>
					<ToolbarSpacer/>
				</f:actions>
			</f:DynamicPageTitle>
		</f:title>
		<!-- DynamicPage Header -->
		<f:header>
			<f:DynamicPageHeader pinnable="true">
				<f:content>
					<HBox alignItems="Start" justifyContent="SpaceBetween">
					<!--<layout:HorizontalLayout allowWrapping="true" >-->
						<layout:VerticalLayout class="sapUiMediumMarginEnd">
							<ObjectAttribute title="Name" text="{DashboardAnalyticsModel>/results/0/name}"/>
							<ObjectAttribute title="Date" text="{DashboardAnalyticsModel>/results/0/datetime}"/>
						</layout:VerticalLayout>
					</HBox>
					<!--</layout:HorizontalLayout>-->
				</f:content>
			</f:DynamicPageHeader>
		</f:header>

		<f:content>
			<VBox>
				<!--
				<core:Fragment fragmentName="sap.ui.demo.basicTemplate.view.Fragments.TabbedControl" type="XML" />
				!-->
			</VBox>
		</f:content>
	</f:DynamicPage>
</core:FragmentDefinition>
<!--basic_template/flaskr/static/view/Fragments/Master.fragment.xml-->
<core:FragmentDefinition
        xmlns="sap.m"
        xmlns:f="sap.f"
        xmlns:core="sap.ui.core">
	<f:DynamicPage toggleHeaderOnTitleClick="false" id="masterListAttachments">
		<!-- DynamicPage Title -->
		<f:title>
			<f:DynamicPageTitle>
				<f:heading>
					<Title text="User profiles ({= ${DashboardAnalyticsModel>/results}.length })"/>
				</f:heading>
			</f:DynamicPageTitle>
		</f:title>
		<!-- DynamicPage Content -->
		<f:content>
			<VBox>
				<SearchField liveChange="onSearchChange" class="searchBar sapUiTinyMarginTop sapUiSmallMarginBottom" width="90%" search="onSearch" />
				<HBox alignItems="Start" justifyContent="End">
					<Button press="onSort" icon="sap-icon://sort"/>
				</HBox>
				<List
					id="attachmentsList"
					itemPress="masterListItemSelected"
					items="{DashboardAnalyticsModel>/results}"
					growing="true"
					growingThreshold="50"
					growingScrollToLoad="true">

					<CustomListItem type="Active">
						<HBox alignItems="Start" justifyContent="SpaceBetween">
							<HBox>
								<VBox class="sapUiTinyMarginTop sapUiTinyMarginBottom">

									<Label text="Name:" />
									<Text class="titleText" text="{DashboardAnalyticsModel>name}" />

									<Label class="sapUiTinyMarginTop" text="Date:" />
									<Text text="{DashboardAnalyticsModel>datetime}" />

								</VBox>
							</HBox>
						</HBox>
					</CustomListItem>
				</List>

			</VBox>
		</f:content>
	</f:DynamicPage>
</core:FragmentDefinition>
<!--basic_template/flaskr/static/view/Fragments/Header.fragment.xml-->
<core:FragmentDefinition
        xmlns="sap.m"
        xmlns:core="sap.ui.core"
>
    <Bar>
        <contentLeft>
            <Button class="sapUiMediumMarginBegin sapUiMediumMarginEnd" icon="sap-icon://nav-back" press="onNavBack" />
            <Select>
            </Select>
        </contentLeft>
        <contentMiddle>
        </contentMiddle>
        <contentRight>
        </contentRight>
    </Bar>
</core:FragmentDefinition>

And even the individual tile rows.

<!--basic_template/flaskr/static/view/Fragments/Operations.fragment.xml-->
<core:FragmentDefinition
		xmlns="sap.m"
		xmlns:core="sap.ui.core">
	<VBox renderType="Bare">
		<HBox alignItems="Start" justifyContent="SpaceBetween" wrap="Wrap">
			<VBox wrap="Wrap" >
				<HBox wrap="Wrap" width="100%">
					<VBox class="sapUiSmallMarginEnd">
						<HBox>
							<GenericTile class="sapUiTinyMarginBegin sapUiTinyMarginTop tileLayout" header="Dashboards" subheader="Analytics"
								press="press('DashboardAnalytics')">
								<TileContent>
									<NumericContent value="1" icon="sap-icon://multiple-line-chart" />
								</TileContent>
							</GenericTile>
						</HBox>
					</VBox>
				</HBox>
				
			</VBox>
				
		</HBox>
	</VBox>
</core:FragmentDefinition>

Hopefully this has provided you with an overview of how to serve a simple Fiori application from your local client and update data models with calls to the Flask back end. In the next part of this series we will aim at productizing the base using Docker and other Flask tools so we can add more components down the road. For example, we will add OrientDB, an Open Source multi-modal database that provides a Document Store, Graph Engine and full text indexing with Lucene.

 

6 Comments
You must be Logged on to comment or reply to a post.