Tutorials¶
Simple application without database usage¶
This tutorial goes through a test application line-by-line. This application tries to register the following URLs
- Accepts json input data, and returns an empty response if successfull
- Returns a html page
- Returns a json data
- Accepts some parameters and returns an empty response
Setup¶
Apart from your application source code, you will have to add certain files for packaging. For this example app, we shall have the following directory structure.
/
PKG-INFO
setup.py
resources/
create_form.html
items_access.html
example_app/
__init__.py
app.py
/PKG-INFO
file is the file in which you define some variables about your app which will be used by the packager./setup.py
file is the file the you traditionally write for any python module. You can learn about this file from here. It basically tells the packager about the dependent python packages and the list of modules and other resources needed to be installed/resources
folder has the static files. In this example we use two html pages. The setup.py must have instructions to install the static files. We will have to note the directory in which the static resource are installed in thePKG-INFO
file. likestatics-url: <The URL prefix from which static resource are accessed> statics-path: <The directory in which the resources installed>
Rest of the files contains the app’s source code. This example app has all the code in a single file
example_app/app.py
.
Entry point¶
The application entry accepts one parameter which is the state of the application.
def entry(state):
if state != STATE_VALIDATION:
# do intializations here.
pass
# entry point should return a list of callbacks. Since this is a
# simple application, we just return a callback for routes.
return {CALLBACK_ROUTES: routes_cb}
The above entry point does not have anything to initialize or migrate. The list of callbacks should be returned no matter in what state the application is started.
This entry-point will have to updated in the PKG-INFO file like
entry-point: example_app.app:entry
Route callback¶
Routes callback accepts a mapper object on which the URLs can be
registered. In this example we will register the URLs with the help of a
submapper as we are passing the same handler
for all the URLs.
def routes_cb(mapper):
# We have the same handler for all the below urls. If not for submapper,
# we will have to define 'handler' in each of the mapper.connect().
# The mapper object has to be passed to the BaseHandler.__init__
with mapper.submapper(handler=TestAppHandler(mapper)) as m:
# Below url sends an index page. The static prefix will be
# used inside the html page to access the static resources.
# no permissions attribute means it can be accessed by anyone
m.connect("/aalam/testapp/",
action="send_homepage",
conditions={"method": ['GET']})
# Below url accepts input data, and it can be in xml/json format.
# It allows only valid users with "Items/create" permission
m.connect("/aalam/testapp/items",
action="create_item",
deserializer="create_item_deserializer",
permissions=Permissions.all(["Items/create"]).deny_anon(),
conditions={"method": ['PUT']})
# Below URL will send a json output. We do not have a serializer
# for this, because the framework will by default use the json
# serializer for response data.
# This api is denied for anonymous requests.
m.connect("/aalam/testapp/item/{item_name}",
action="get_item",
permissions=Permissions().deny_anon(),
serializer="get_item_serializer",
conditions={"method": ['GET']})
# Below URL is used to update a setting, but will permit a user
# to update the setting that is created by the same user.
m.connect("/aalam/testapp/item/{item_name}",
action="update_item",
permissions=Permissions().deny_anon(),
conditions={"method": ['POST']})
In the above code, TestAppHandler
is the class which will have all the action
handler methods that are passed in the each m.connect
call.
Route Handler¶
Route Handler should inherit aalam_common.wsgi.BaseHandler
class.
This class should define methods for all the action
, serializer
and
deserializer
arguments used for the URL using this handler.
class TestAppHandler(wsgi.BaseHandler):
def __init__(self, mapper):
# The BaseHandler.__init__ must be called with this mapper object.
# If you do not have anything to do in this method, better not
# define it.
super(TestAppHandler, self).__init__(mapper)
def send_homepage(self, request):
# We are not documenting this, as this API will be more useful
# for the users than the developers.
# We dont to send the index page on a static prefixed URL, hence
# we set the static_file attribute in the request object and let
# the framework handle the response.
# The index file will be placed by the aalam packager only if
# it is mentioned in the setup.py like
# setup(data_files=[("index.html", "resource/index.html"), ...])
request.static_file = {"resource": "index.html",
"path": os.path.join(
cfg.CONF.package_dir, "resources", "index.html")}
# we are not returning any data, as the framework will take care
# of it.
def create_item_deserializer(self, request):
# Input is expected to be either in json or xml format, so check
# content type. json_deserializer() method is defined in the
# BaseHandler, so using it as is.
if request.content_type == "application/json":
# We have an inbuilt json serializer defined in BaseHandler class
return self.json_deserializer(request)
elif request.content_type == "application/xml":
# Parse the input xml data from request.body and return a
# dictionary object
pass
def create_item(self, request, name=None, type=None):
"""
Create Item
Creates a new item of a user chosen name and type
Section:
Items
Input:
type: application/json
description: Input data in json
spec:
{
"name": "Some name",
"type": "Some type",
}
Input:
type: application/xml
description: Input data in xml
spec:
<item>
<name>Some name</name>
<type>Some type</type>
</item
Output:
status_code: 200
description: Successfully created a new item
Output:
status_code: 400
description: Input data is wrong
"""
# We documented this API and the documentation will be automatically
# generated and be available in the public domain
# validate the input data. The kwargs parmaters are the result from
# deserializer, ie - create_item_deserializer()
if not name or not type:
raise webob.exc.HTTPBadRequest(
explanation="Invalid input")
# Get the email id of the user creating this item.
user_email = auth.get_auth_user(request)
# For future use, we store this item in redis.
redis_conn.hmset(name, {"type": type, "owner": user_email})
# If we return a tuple with null data, just the status code will
# be set in the response.
return (201, None)
def get_item_serializer(self, data, response):
# Data will be the data returned by get_item(). If the response output
# is json, json_serializer from the base class will be automatically
# used. This class is defined just for demonstration.
# Set the respone content type
response.content_type = "application/json"
response.body = json.dumps(data)
def get_item(self, request, item_name):
# Document like above if you wish to expose this API in publicly
# Note the parameter, 'item_name'. The name of the parameter is as
# set in the URL for this action handler. The value will is as
# it is in the actual path, example: for /aalam/testapp/some_name,
# 'item_name' variable will have the value 'some_name'
# We get the data back from redis and return a dictionary. This data
# will be passed to get_item_serializer.
return {"type": redis_conn.get(item_name)}
def update_item(self, request, item_name):
# 'item_name' is set in the same way as it is for get_item.
# we expect the new type in the request parameter. If not present
# throw an error
if 'type' not in request.params:
raise webob.exc.HTTPBadRequest(explanation="Invalid usage")
# Check if the owner of this item is updating this item. Else
# throw an error.
(user_email) = auth.get_auth_user(request)
if redis_conn.hget(item_name, "owner") != user_email:
raise webob.exc.HTTPForbidden()
# We can update the item now, as the user is verified.
redis_conn.hset(item_name, "type", request.params['type'])
# This just sends a 200 OK response
Hooks¶
We will present this application to any valid user in the front HTML page of the portal in which this app is running. To do so, we need to hook the base applications’ apps palette API. This is ‘B’ hook, and the hook information should be present in this application’s PKG-INFO. The hook callback will look like the below code. Learn more about hooks(hooks.md).
def hook_entry(request):
# We present different entry points
# For admin user or users with Items/create permisssion
# create form will be shown
# For authenticated users who is not authorized enought to create
# items, can still access the items and items search page will
# shown
# For the anonymous users, a general information will be shown
if role_mgmt.is_user_admin(request) and \
role_mgmt.is_client_authorized(request, "Items", "create"):
return {"entry": "/aalam/testapp/s/create_form.html"}
elif not auth.is_anonymous_user(request):
return {"entry": "/aalam/testapp/s/items_access.html"}
else:
# anonymous users
return {"entry": "/aalam/testapp/"}
PKG-INFO¶
Since for PKG-INFO must have the entry-point
, we have to anyway add it. Apart
from entry-point
, we have to define few more items.
statics-url
Since this test application sends some static pages, it will be efficient to use the frameworks support for service static files.static-url
should contain the URL prefix on which the static files are served. This app uses/aalam/testapp/s/
as the URL prefix for static files.statics-path
The value of this should be the directory in which the static files are installed by this app’s setup.py.permissions
As we have noted above, this app has few permissions. Following is how these permissions will be defined in thePKG-INFO
file.
permissions:
permission-groups:
Items: Permissions to manage this app's items
Items:
create: Enable an user to create items
hooks
This app is hooking the base applications apps palette to get this app to be displayed on the portal’s index page. You can hook any application as long as the application you are hooking allows you to hook. This way your app can be acting as a plugin to other applications. You can define a hook specification in you app to enable other application to act as plugins to your app. Following is how hooks are defined in thePKG-INFO
file.
hooks:
hook:
- app: aalam/base
url: /aalam/base
method: GET
handler: example_app.app:hook_entry
type: B
Testing¶
We have successfully written the test application. But before submitting it to the apps server, it is better we test it. We can use our SDK to do the same.
Create a provider account with your provider code (aalam) in the SDK and create an app with your app’s code (testapp) inside the app’s provider account. Then create a gzipped tar file (.tar.gz) of all the files in the top folder of your app and submit it to the SDK. The SDK starts packaging and if it successfully packages, a new version for the app will be created.
If you have a prior version of this app, you can either migrate your app to this version or you can set the running version of the app to the version that you just created. Since this is the first version we are creating, we can just use the “Set as current version” option.
After you choose the version to be the current version, the app will be started in the SDK. You can browse through your app’s url in a web browser from the same machine.
Downloadables¶
Following are the source for the test application