Tutorials

Simple application without database usage

This tutorial goes through a test application line-by-line. This application tries to register the following URLs

  1. Accepts json input data, and returns an empty response if successfull
  2. Returns a html page
  3. Returns a json data
  4. 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 the PKG-INFO file. like

    statics-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 the PKG-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 the PKG-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.