7 Nov 2017

Using Python to create a Forge Server/Client Web Application - Part II: Deployment

Py-Forge

    Last week I had a closer look at Using Python to create a Forge Server/Client Web Application. So this time I will go through the steps I took in order to deploy that demo and have it running on the Cloud.

    The first version was really basic, so I enhanced it a little to have a more compelling demo. Let's take a look at some key points I added since last time:

I - 404 Handling:

    My app had two routes /home and /viewer?id=some_mongoDB_id but when passing an incorrect or missing id to the viewer route it would throw an exception, making the web site look pretty unprofessional. I added exception handling to that route redirecting to a new /404 route. Here is how the code looks like and the result:

#////////////////////////////////////////////////////////////////////
# /viewer route handler with redirection for incorrect/missing id
#
#////////////////////////////////////////////////////////////////////
@view_config(route_name='viewer', renderer='templates/viewer.jinja2')
def viewer_view(request):

    try:

        model_id = request.params['id']

        model_info = request.db['gallery.models'].find_one({
            '_id': ObjectId(model_id)
        })

        if model_info is None:
            return HTTPFound(location='/404')

        return {
            'token_url': '/forge/token',
            'model_info': model_info
        }

    except:

        return HTTPFound(location='/404')

404 route

II - Forge Thumbnails:

    The first version was not retrieving thumbnails for each listed model. I added an extra /thumbnail?id route which returns a png image requested from Forge using Model Derivatives API GET :urn/thumbnail endpoint:

#////////////////////////////////////////////////////////////////////
# Get Forge thumbnail
#
#////////////////////////////////////////////////////////////////////
def get_thumbnail(token, urn):

    base_url = 'https://developer.api.autodesk.com'

    url = base_url + '/modelderivative/v2/designdata/{}/thumbnail?{}'

    query = 'width=400&height=400'

    headers = {
        'Authorization': 'Bearer ' + token['access_token']
    }

    r = requests.get(url.format(urn, query), headers=headers)

    if 200 == r.status_code:
        return r.content

    return None

# ////////////////////////////////////////////////////////////////////
# /forge/thumbnail?id route
#
# ////////////////////////////////////////////////////////////////////
@view_config(route_name='forge-thumbnail')
def forge_thumbnail(request):

    try:

        model_id = request.params['id']

        model_info = request.db['gallery.models'].find_one({
            '_id': ObjectId(model_id)
        })

        if model_info is None:
            return HTTPNotFound()

        urn = model_info['model']['urn']

        credentials = getCredentials(request.registry.settings)

        token = get_tokenMemo(credentials['id'], credentials['secret'])

        thumbnail = get_thumbnail(token, urn)

        return Response(thumbnail, content_type='image/png')

    except Exception as ex:

        return HTTPNotFound()

III - Using a memoize function for the Forge token:

    When opening the /home route the list of models will be loaded and for each model the thumbnail is being requested, which was causing multiple requests for a Forge token. Since most of the thumbnails will be requested within a small amount of time, the same token can be reused hence saving as many calls to the Forge server. 

    I looked for a way to implement a memoization function in Python and quickly found this snippet:

import time

#////////////////////////////////////////////////////////////////////
# Memoize With Timeout
#
#////////////////////////////////////////////////////////////////////
class Memo(object):

    _caches = {}
    _timeouts = {}

    def __init__(self,timeout=2):
        self.timeout = timeout

    def collect(self):
        """Clear cache of results which have timed out"""
        for func in self._caches:
            cache = {}
            for key in self._caches[func]:
                if (time.time() - self._caches[func][key][1]) < self._timeouts[func]:
                    cache[key] = self._caches[func][key]
            self._caches[func] = cache

    def __call__(self, f):
        self.cache = self._caches[f] = {}
        self._timeouts[f] = self.timeout

        def func(*args, **kwargs):
            kw = sorted(kwargs.items())
            key = (args, tuple(kw))
            try:
                v = self.cache[key]

                if (time.time() - v[1]) > self.timeout:
                    raise KeyError
            except KeyError:

                v = self.cache[key] = f(*args,**kwargs),time.time()
            return v[0]
        func.func_name = f.__name__

        return func

This allows to conveniently use a decorator to specify how long to memoize a function, super nice:

#////////////////////////////////////////////////////////////////////
# Caches current token for delay specified by timeout (in seconds)
#
#////////////////////////////////////////////////////////////////////
@Memo(timeout=3580)
def get_tokenMemo(client_id, client_secret):

    return get_token(client_id, client_secret)

IV - Deployment:

    There are countless combinations you can use to deploy a Python server, you can take a look at the Pyramid deployment section in their doc. My goal was to use the easiest and quickest way possible, given that our autodesk.io server is running on Ubuntu and uses Nginx, this is the option that naturally came to mind: Nginx + pserve + supervisord

    The first sentence was enough to convince me: "This setup can be accomplished simply and is capable of serving a large amount of traffic. The advantage in deployment is that by using pserve, it is not unlike the basic development environment you're probably using on your local machine"

    Nginx is already configured on autodesk.io, so I just had to add a new entry with an open port and the url of my app: py-forge.autodesk.io. For more details about configuring Nginx, please refer to the tutorial above.

The next step is basically running pserve on the server the same way you run it locally:

    Make sure Python3 is installed on the machine:

~$ python3 -V
Python 3.4.3

    Install venv for that version to create a Python virtual environment:

~$ sudo apt-get install python3.4-venv

    Create a directory for our app and initialise the virtual env:

~$ mkdir py-forge
~$ python3 -m venv ./py-forge

    Copy all files of the Py-Forge project and run the setup:

~$ cd py-forge
~$ ./bin/pip install -e .

    My server runs on https, so I had to add this line in the production.ini file:

[server:main]
url_scheme = https
#other fields there ...

    Finally run pserve against the production.ini.

~$ ./bin/pserve production.ini --reload

   You can optionally use a script that defines or overwrites some system variables used by the server and add a & at the end of the command to keep it running in the background once you close the ssh terminal:

#!/usr/bin/env bash
sudo FORGE_CLIENT_ID=<client-id-here> FORGE_CLIENT_SECRET=<client-secret-here> ./bin/pserve production.ini --reload &

Find out the complete project at: 

https://github.com/leefsmp/py-forge 

Along with the live demo at:

https://py-forge.autodesk.io

 

Related Article