Topics for today

  • Interfacing web servers
    • CGI
    • FastCGI
  • Python application servers
    • WSGI
    • wsgiref
    • uWSGI
  • Python web frameworks
    • Model-View-Controller
    • Django
    • Pyramid
    • Flask

Links

Integrating web servers with applications

Running web applications with Python

MVC

Most important parts of Pyramid documentation

Template engine

Tasks to do during classes

Simple WSGI applications (1 pt.)

You may run this application in many different ways:

  1. On Debian-based (including Ubuntu) local computer. If you work from faculty computers please remember to run programs from outside of ~/ directory (I recommend /tmp or /dev/shm).

  2. On Windows 10 using WSL (tested to work on WSL v1 and Debian image).

  3. On your tin VM. Then remember to replace localhost in your URLs with your tin VM host name. You need to work from the faculty or be connected to VPN to be able to access the webpages in the browser. Moreover, if you configured UFW during class 03, remember to reopen required ports with

    # ufw insert 1 allow 6543

Whatever option you choose, if you work on computer that you administer (not faculty one) remember to install python3 with

$ sudo apt install python3 python3-venv

The class cannot be completed on LTS or Windows terminal (term), due to restricted permissions and port conflicts (only one aplication, and hence user, may listen on given port).

The simplest possible WSGI application looks like this:

def app(environ, start_response):
    start_response('200 OK', [('Content-type', 'text/plain')])
    return ['Hello world!'.encode('utf-8')]

if __name__ == '__main__':
    from wsgiref.simple_server import make_server
    server = make_server('', 6543, app)
    server.serve_forever()

We can run it with

$ python3 task08a.py

After that we can see the result in any browser at http://localhost:6543. You can also see the application in action here. Our application does not look at the specific URL that is being handled. For every request we get just “Hello world”. To handle different URLs we need to look into the specific elements inside environ:

def app(environ, start_response):
    start_response('200 OK', [('Content-type', 'text/plain')])
    return [b'REQUEST_METHOD: %s\nSCRIPT_NAME: %s\nPATH_INFO: %s' %
            (environ.get('REQUEST_METHOD', '').encode('utf-8'),
             environ.get('SCRIPT_NAME', '').encode('utf-8'),
             environ.get('PATH_INFO', '').encode('utf-8'))]

if __name__ == '__main__':
    from wsgiref.simple_server import make_server
    server = make_server('', 6543, app)
    server.serve_forever()

You can run it again or look here. One can imagine now, that we can test for specific URLs and handle them in different ways, maybe by implementing other functions/objects etc. This quickly get very tedious and repetitive. Enter the web frameworks.

Frameworks are like libraries: bits of code that someone else wrote for us to use. Frameworks differ from libraries: code from the library is called by the application, whereas framework runs on its own and at some point calls the code written by the programmer.

Core functionality supported by all the web frameworks (not only in the Python world) is mapping bits of code to URLs. Specific methods to do that vary across languages and frameworks. Some use XML/YAML/JSON configurations files, others use some specific code or placement of files in the filesystem.

First example is a very simple application in Flask that is able to receive some input through URLs.

from flask import Flask

app = Flask(__name__)

@app.route('/')
def index():
    return 'Welcome to the index!'

@app.route('/hello')
def hello():
    return 'Hello World!'

@app.route('/hello/<name>')
def hello_name(name):
    return 'Hello ' + name + '!'

if __name__ == '__main__':
    from wsgiref.simple_server import make_server
    server = make_server('', 6543, app)
    server.serve_forever()

You can interact with the application here.

Saving the file and running with python3 will in most cases result in the error ModuleNotFoundError: No module named 'flask'. As we do not have a root account, we are unable to install Python modules system-wide. Even if we could, this is not the best idea. We will use the so-called virtual environments. To create a virtual environment type:

$ python3 -m venv ./pyvenv
$ ./pyvenv/bin/pip install flask

This creates a new virtual environment in the directory ./pyvenv and installs Flask there. Then we can use the ./pyvenv/bin/python t08-flask.py executable to run our application (in fact this “executable” is a link to the system one, but the module path is suitably arranged for us). Virtual environments are great when you need a library that is not installed system-wide or you need different version of the same library for some reason. One usually has many virtual environments.

A very simple application built with Pyramid follows:

from pyramid.config import Configurator
from pyramid.view import view_config
from pyramid.response import Response

@view_config(route_name='index')
def index(request):
    return Response('Welcome to the index!')

@view_config(route_name='hello', renderer='string')
def hello(request):
    return 'Hello World!'

@view_config(route_name='hello_name', renderer='string')
def hello_name(request):
    return 'Hello ' + request.matchdict['name'] + '!'

config = Configurator()
config.add_route('index', '/')
config.add_route('hello', '/hello')
config.add_route('hello_name', '/hello/{name}')
config.scan()
app = config.make_wsgi_app()

if __name__ == '__main__':
    from wsgiref.simple_server import make_server
    server = make_server('', 6543, app)
    server.serve_forever()

It can be run similarly to the application written in Flask (after installing the package pyramid that is). You can also see it here.

Observe the differences and similarities between this application and the Flask application above. Run any of the applications here under a debugger, e.g., in PyCharm or pudb. Set breakpoints inside requests handlers and visit them in browser.

Jinja2 templates (1 pt.)

Most websites have single layout that is filled with different content depending on the current page. Some elements, like navigation bars or footers stay the same throughout while others, i.e., navigation breadcrumbs, change only superficially. The code that renders such elements should be added to every request handler but shouldn’t be done by copying and pasting. At the same time this would be also cumbersome to do with specialized methods that create fragments of page: code that creates actual content tends to mix with general layout. Template engines are a solution to this problem.

Templates are text files that can contain variables and these variables are filled by actual content during the template rendering. This results in a text file that has consistent layout (defined in the template) and any content we wish (usually provided during rendering of the template). This allows graphics designers creating templates to work in parallel to programmers writing code. We will showcase here the Jinja2 engine, but there are many, many, many more. Jinja2 is mostly compatible (or at least very similar) with Django Templates.

Create the following layout.jinja2 file:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>{% block title %}{{ title }}{% endblock %} ~ Jinja2 demo</title>
  <script src="https://code.jquery.com/jquery-3.3.1.slim.js"></script>
  <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4" crossorigin="anonymous">
  <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/js/bootstrap.min.js" integrity="sha384-uefMccjFJAIv6A+rW+L4AHf99KvxDjWSu1z9VI8SKNVmz4sk7buKt/6v9KI65qnm" crossorigin="anonymous"></script>
  <!--[if lt IE 9]>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.js"></script>
  <![endif]-->
  {% block head %}{% endblock %}
</head>

<body>
  <nav class="navbar navbar-expand-md navbar-light bg-light">
    <a class="navbar-brand" href="{{ request.route_url('index') }}">Jinja2 demo</a>
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
      <span class="navbar-toggler-icon"></span>
    </button>

    <div class="collapse navbar-collapse" id="navbarSupportedContent">
      <ul class="navbar-nav mr-auto">
      <li class="nav-item dropdown">
        <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Pages</a>
          <div class="dropdown-menu" aria-labelledby="navbarDropdown">
              <a class="dropdown-item" href="{{ request.route_url('table', seed=''|rand_string) }}">Random table</a>
              <a class="dropdown-item" href="{{ request.route_url('lipsum') }}">Lorem ipsum</a>
          </div>
        </li>
      </ul>
    </div>
  </nav>

  <div class="container">
      <div class="row">
        <div class="col-md-8 offset-md-2">
        {% block content %}<p>Pick a subpage from the navigation bar above</p>{% endblock %}
        </div>
    </div>
  </div>
</body>
</html>

Jinja2 allows for templates to extend other templates, which allows for even less duplication, at the cost of little more complexity. This is somewhat similar to inheritance in object-oriented programming. Create the following table.jinja2 file:

{% extends 'layout.jinja2' %}

{% block title %}Table{% endblock %}

{% block head %}
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/v/bs4/dt-1.10.16/datatables.min.css"/>
<script type="text/javascript" src="https://cdn.datatables.net/v/bs4/dt-1.10.16/datatables.min.js"></script>
<script>
$(document).ready( function () {
    $('#table').DataTable();
} );
</script>
<style>
  #table {
    width: 100%;
  }
</style>
{% endblock %}

{% block content %}
<table id="table">
    <thead>
        <tr>
            {% for column in columns %}
            <th>{{ column }}</th>
            {% endfor %}
        </tr>
    </thead>
    <tbody>
        {% for row in rows %}
        <tr>
            {% for datum in row %}
            <td>{{ datum }}</td>
            {% endfor %}
        </tr>
        {% endfor %}
    </tbody>
</table>
{% endblock %}

Also create the following lipsum.jinja2 file:

{% extends 'layout.jinja2' %}

{% block title %}Lorem ipsum{% endblock %}


{% block content %}
    {% for p in paragraphs %}
      <p>{{ p }}</p>
  {% endfor %}
{% endblock %}

Now install the packages pyramid-jinja2, pyramid-debugtoolbar and faker into your virtual environment and run the following application:

from pyramid.config import Configurator
from pyramid.view import view_config
import string
from faker import Faker
from jinja2 import contextfilter
import os

fake = Faker()

# A hack that allows to automatically generate random URLs in the template.
# The '@contextfilter' decorator is necessary, otherwise the result would be cached
@contextfilter
def randString(context, dummy):
    return ''.join([fake.random.choice(string.ascii_letters + string.digits) for n in range(16)])

@view_config(route_name='index', renderer='layout.jinja2')
def index(request):
    return {'title': 'Index'}

@view_config(route_name='lipsum', renderer='lipsum.jinja2')
def lipsum(request):
    return {'paragraphs': fake.paragraphs(nb=fake.random.randint(5,10))}

@view_config(route_name='table', renderer='table.jinja2')
def random_table(request):
    faker_state = fake.random.getstate()
    seed = request.matchdict['seed']
    fake.random.seed(seed)
    number_of_columns = fake.random.randint(5,10)
    columns = fake.words(nb=number_of_columns)
    rows = []
    for i in range(0, fake.random.randint(30,50)):
        rows.append([fake.random.randint(0,1000) for i in range(0, number_of_columns)])
    fake.random.setstate(faker_state)
    return {'columns': columns,
            'rows': rows}

config = Configurator(settings={'debugtoolbar.hosts': '0.0.0.0/0'})
config.add_route('index', '/')
config.add_route('lipsum', '/lipsum')
config.add_route('table', '/table/{seed}')
config.include('pyramid_jinja2')
thisDirectory = os.path.dirname(os.path.realpath(__file__))
config.add_jinja2_search_path(thisDirectory)
config.include('pyramid_debugtoolbar')
config.commit()
jinja2_env = config.get_jinja2_environment()
jinja2_env.filters['rand_string'] = randString
jinja2_env.autoescape = False
config.scan()

app = config.make_wsgi_app()

if __name__ == '__main__':
    from wsgiref.simple_server import make_server
    server = make_server('', 6543, app)
    server.serve_forever()

Run the application presented in this section. Change the main layout, so that any page displayed by the application shows your name. The original application runs here.

Handling forms and sessions (1 pt.)

Most web applications at some point need to get input from users. Be it passwords, settings etc. This is usually handled through forms everyone is accustomed to. We will learn how to handle such input.

At the same time HTTP is a stateless and connectionless protocol, so at the protocol level there is no notion of client identity. But data is usually connected with identity, so our application needs a way to discriminate between clients, i.e., tell when subsequent requests originate from the same client. This is usually done through a special secure cookie set in the browser and allows for so-called sessions (transient data that is private for a given client). This is the mechanism that allows, e.g., to log into web applications.

There are different strategies as to where store the data in the session. Some applications store them in the browser (in cookies), some store it on the server in files or databases. We will show here only a basic mechanism that uses cryptographically signed cookies to store data in the client. A more serious application would probably use a structured database (cookie size is limited, so we do not have much space).

Our application has only one template (form.jinja2):

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Form handling demo</title>
  <script src="http://code.jquery.com/jquery-3.3.1.slim.js"></script>
  <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4" crossorigin="anonymous">
  <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/js/bootstrap.min.js" integrity="sha384-uefMccjFJAIv6A+rW+L4AHf99KvxDjWSu1z9VI8SKNVmz4sk7buKt/6v9KI65qnm" crossorigin="anonymous"></script>
  <!--[if lt IE 9]>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.js"></script>
  <![endif]-->
</head>

<body>
  <div class="container">
    <div class="row">
      <div class="col-md-8 offset-md-2">
          {% for key in request.session.keys() %}
              <p>{{ key }}: {{ request.session[key] }} <a href="{{ request.route_url('delete', key=key)}}">Delete</a></p>
          {% endfor %}
        <form action="{{ request.route_url('form') }}" method="POST" name="form" id="form">
          <div class="form-group">
            <label for="key">Key:</label>
            <input type="text" class="form-control{% if error and 'key' in error %} is-invalid{% endif %}" id="key" name="key" placeholder="Enter key" required {% if values and 'key' in values %}value="{{ values['key'] }}"{% endif %}>
            {% if error and 'key' in error %}<div class="invalid-feedback">{{ error['key'][0] }}</div>{% endif %}
            <small id="keyHelp" class="form-text text-muted">The key you want to add to the store.</small>
          </div>
          <div class="form-group">
            <label for="payload">Payload:</label>
            <input type="text" class="form-control{% if error and 'payload' in error %} is-invalid{% endif %}" id="payload" name="payload" placeholder="Payload" required {% if values and 'key' in values %}value="{{ values['payload'] }}"{% endif %}>
            {% if error and 'payload' in error %}<div class="invalid-feedback">{{ error['payload'][0] }}</div>{% endif %}
            <small id="keyHelp" class="form-text text-muted">Corresponding value.</small>
          </div>
          {% if error and '_schema' in error %}<div class="alert alert-danger">{{ error['_schema'][0] }}</div>{% endif %}
          <button type="submit" class="btn btn-primary" value="submit">Submit</button>
        </form>
      </div>
    </div>
  </div>
</body>
</html>

There is also one Python file (form.py). It requires the marshmallow library. It can be installed by running pip install marshmallow:

from pyramid.config import Configurator
from pyramid.session import SignedCookieSessionFactory
from pyramid.view import view_config
import re
from marshmallow import Schema, fields, validates, validates_schema, ValidationError
import os

uppercaseAndDigits = re.compile(r'^[A-Z0-9]*$')

class KeySchema(Schema):
    key = fields.String(required=True)
    payload = fields.String(required=True)

    @validates('key')
    def valid_key(self, value):
        if not value or len(value) > 10:
            raise ValidationError('Key must be nonempty an at most 10 characters long.')
        if not uppercaseAndDigits.match(value):
            raise ValidationError('Key must contain only uppercase letters and digits.')

    @validates('payload')
    def valid_dat(self, value):
        if not value or len(value) > 30:
            raise ValidationError('Payload must be nonempty and at most 30 characters long.')

    @validates_schema
    def valid_schema(self, data, **kwargs):
        if not data['key'][0] == data['payload'][0]:
            raise ValidationError('First letters of key and payload must be the same.')

@view_config(route_name='form', renderer='form.jinja2', request_method='GET')
def form(request):
    return {}

@view_config(route_name='form', renderer='form.jinja2', request_method='POST')
def handle(request):
    # Data that comes from the client must always be thoroughly checked
    # as they can be easily spoofed
    try:
        schema = KeySchema()
        pair = schema.load(request.POST)
        request.session[pair['key']] = pair['payload']
        return {}
    except ValidationError as err:
        return { 'error': err.messages,
                 'values': request.POST }

@view_config(route_name='delete', renderer='form.jinja2')
def delete(request):
    key = request.matchdict['key']
    if key in request.session:
        del request.session[key]
    return {}

config = Configurator()
config.add_route('form', '/')
config.add_route('delete', '/delete/{key}')
config.include('pyramid_jinja2')
thisDirectory = os.path.dirname(os.path.realpath(__file__))
config.add_jinja2_search_path(thisDirectory)
config.include('pyramid_debugtoolbar')
session_factory = SignedCookieSessionFactory('tajneHaslo')
config.set_session_factory(session_factory)
config.scan()

app = config.make_wsgi_app()

if __name__ == '__main__':
    from wsgiref.simple_server import make_server
    server = make_server('', 6543, app)
    server.serve_forever()

Run the application from this section. You can see it also here.

Running an application in production (1 pt.)

There comes a moment when we wish to share our creation with the world. Web applications run, well, on the web, which means they have to be integrated with a working HTTP server. We will learn now to setup a Pyramid application with the uWSGI application server with the web server nginx as reverse-proxy on your VM. This means that a browser connects with nginx, which forwards traffic to a suitable instance of uWSGI that is reachable only locally.

We will use the so-called Emperor mode of uWSGI. It allows for web applications (vassals) to be automatically spawned at boot and respawned if they die.

We will now create an application to run. We will see how a full-sized Pyramid application is created and how it looks like. All our previous applications were more in the microframework ballpark. Pyramid can automatically create scaffolds of larger project, avoiding the tedious recreating of everything from scratch.

We will first need to install into our virtual environment the Python package cookiecuter. Afterwards just run:

$ ./pyvenv/bin/cookiecutter 'https://github.com/Pylons/pyramid-cookiecutter-alchemy'

This creates a project that uses SQLAlchemy – a very good Python ORM library, i.e., a library that sits between the application and the database server. This adds some complexity but allows for greater flexibility, as the programmer does not need to write SQL queries, the library does that. This allows, e.g, to change the database server from MySQL to PostgreSQL without rewriting half of the queries (database servers speak different dialects of SQL).

Cookiecutter emits commands to enter to run the newly created project. They will create a new virtual environment just for this application. Full-sized Pyramid applications are configured through INI files, not through code. This allows us to have many different configurations for the same application. In the newly created project we have development.ini and production.ini, suitable for development and production respectively.

We will not explain here how the application is organized, but you are welcome to look around.

As advertised, we will now run this application on our VM. To do that first create a redistributable archive and copy it to the VM (assuming you have configured the shortcut tin for the VM):

$ env/bin/python3 setup.py sdist
$ scp dist/*tar.gz production.ini *.sqlite tin:

We need to install some packages on the VM. As root on your VM do:

# apt-get update
# apt-get install nginx uwsgi uwsgi-emperor uwsgi-plugin-python3 python3 python3-venv libsqlite3-dev

More packages might be installed as dependencies.

Create a virtual environment on the VM and install our application into it together with its dependencies:

# python3 -m venv pyramidvenv
# pyramidvenv/bin/pip install --upgrade pip setuptools wheel pysqlite3
# pyramidvenv/bin/pip install *tar.gz
# pyramidvenv/bin/initialize*db production.ini

Now we can try and run our application on the command line:

# uwsgi --plugins=python3 --venv pyramidvenv --ini-paste production.ini --http-socket 0.0.0.0:6543

You should be able to connect with your application from a browser, just point it at your VM, port 6543 (Remember that your VM is not reachable from the internet, you need to be on the campus or use an SSH tunnel/VPN). We are almost there, now just to move the configuration to the vassal.

Create an *.ini file in /etc/uwsgi-emperor/vassals (specific name does not matter much, name it as you wish). Inside put:

[uwsgi]
socket = /tmp/uwsgi-tin08.sock
plugins = python3, logfile
venv = /root/pyramidvenv
ini-paste = /root/production.ini
logger = file:/root/myFirstProductionApp.log

The first line tell uWSGI where to listen for connections (here – a UNIX socket), the last line saves logs to disk.

By the way, if you wish to run an application in the microframework style (so no INI file), the corresponding vassal would look like this:

[uwsgi]
socket = ****
plugin = python3, logfile
virtualenv = /root/pyramidvenv
wsgi-file = /root/SingleFileApp.py
callable = application
logger = file:/root/SigleFileApp.log

Here, the callable is the name of the object/function in wsgi-file that should be run as a WSGI application.

To finish the job, we just need to run the uWSGI emperor. Default configuration would not work because of permission reasons. We will skip this problem by running the emperor as root. In a true production server this should not be the case. To change the configuration of the emperor, edit the file /etc/uwsgi-emperor/emperor.ini and change uid and gid from www-data to root. Save the file. Now you can run the emperor and mark it for running at boot:

# systemctl start uwsgi-emperor
# systemctl enable uwsgi-emperor

We are almost done, we just need to tell nginx to connect to our vassal and act as a reverse-proxy for it. The configuration of nginx in Debian is split between many files, so called sites. Edit the file /etc/nginx/sites-available/default. Change

location / {
    # First attempt to serve request as file, then
    # as directory, then fall back to displaying a 404.
    try_files $uri $uri/ =404;
}

to

location / {
    # First attempt to serve request as file, then
    # as directory, then fall back to displaying a 404.
    # try_files $uri $uri/ =404;
    include uwsgi_params;
    uwsgi_pass unix:///tmp/uwsgi-tin08.sock;
}

Now we need to restart nginx:

# systemctl restart nginx

This tells nginx to forward the traffic coming to / to uWSGI listening on the specified UNIX socket. Note that this is only a fragment of the full configuration file, which lives at /etc/nginx/nginx.conf. You can have many server sections in the configuration file, e.g., to host multiple domains on the same server or handle multiple ports. Each server section can have multiple location sections, e.g. to host multiple applications on different prefixes. This is described here.

If you want to host an application with uWSGI that is mounted under a prefix, take a look at this snippet.

If you want to host multiple applications configured with INI files with one uWSGI instance under different prefixes, you can either create small Python scripts that bootstrap WSGI applications from INI files , or use URLMap middleware. This last option may be more problematic, as applications necessarily share the same Python instance (so the are not that isolated).

Mandatory tasks (at home)

JSON API (3 pt.)

Create and deploy on your VM an application that multiplies numbers. It should listen for HTTP POST requests at /product on port 8080 on your VM. It should accept a JSON file of the following form:

{
  "token": 1234567890,
  "a": 4718923648912376,
  "b": 4710943190713794
}

Here all fields are positive integers with no limits on their size. The whole body of the request will be at most 2 megabytes.

It should respond with a JSON file of the following form:

{
  "token": 1234567890,
  "product": 22230581231342048010971200514544
}

The field token should match the corresponding field in the request and product should be equal to a*b.

Other requests than POST should return 405 Method Not Allowed. Any errors in the request JSON (e.g., non-numbers, negative numbers) should return 400 Bad Request.

In order for this task to be checked, send an email to bikol@wmi.amu.edu.pl with the subject “[DTIN] Z8.1 ######” (with your student ID number, which is a part of your VM address).

Hints

  1. Recall that you can test your application using cURL, e.g., during development you might do:

    $ curl -X POST -d '{"token": 1, "a": 1, "b": 1}' http://localhost:6543/product

    In the end just change to the appropriate URL on your VM.

  2. To check whether the JSON file sent to the application conforms to the format indicated above, you may want to use the jsonschema package. The format of the schema is explained in details in the book available here. If you feel overwhelmed by having to learn another package for one task, the validation can also be done by hand. There are many edge cases, make sure to cover them all. For example the following JSON files should all give a Bad Request error:

    • {}
    • {"token": 1}
    • {"token": 1, "a": 1}
    • {"token": 1, "a": 1, "c": 1}
    • {"a": 1, "b": 1}
    • {"token": "a", "a": 1, "b": 1}
    • {"token": 0, "a": 1, "b": 1}
    • {"token": 1, "a": 0, "b": 1}
    • {"token": 1, "a": 1, "b": 0}
    • {"token": 1, "a": 1, "b": -1}
    • {"token": 1, "a": 1, "b": 1, "c": 1}
  3. It may happen that a client sends a file that is not a valid JSON file. Make sure to catch such cases (and return 400 Bad Request).

  4. Python automatically handles arbitrarily large integers, so there should be no problem in handling the following (valid) submission:

    {
      "token": 1498723,
      "a": 11111111111111111111111111111111111111111111111111111111111111111111111111111111,
      "b": 11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
    }

Recipes portal (7 pt.)

Create an application that presents to the user cooking recipes. Recipes need:

  • name
  • ingredients
  • photo (optional)
  • preparation steps

You need a way to store the recipes in the application. You may do that in different ways: plain text files, database, XML files, JSON files. Chooses one that you find suitable. To make things simple store only a link to the image, not the whole file. Make sure there are no limits to the number of ingredients/steps a single recipe might have.

  • Make sure to have at least 4 different recipes of your own defined. Present them (as nicely formatted webpages) at the addresses /recipe/{consectutive number} on port 8080 on your VM (i.e., /recipe/1, /recipe/2 etc). In order for this task to be checked, send an email to bikol@wmi.amu.edu.pl with the subject “[DTIN] Z8.2a ######” (with your student ID number, which is a part of your VM address). (2 pt.)

  • Allow the user to add new recipes. Present a suitable form at: /recipe/new (on port 8080). To make things simple do not allow to submit images as files, only URLs. Images should be optional. Also, for simplicity assume that we have at most 5 ingredients and at most 10 steps (if you can, you can allow for arbitrarily many, but this is more difficult). After submitting a valid recipe redirect the user to the newly created page. This will be checked by hand. Make sure your application is always running. (3 pt.)

  • Allow the user to add new recipes by sending a JSON file (by HTTP POST) to /recipe/api/new (on port 8080). The JSON file has the following structure (no limit on number of ingredients or steps):

    {
      "name": "Name of the recipe",
      "photo": "http://...",
      "ingredients": [
        "ingredient 1",
        "ingredient 2",
        ...
      ],
      "steps": [
        "step 1",
        "step 2",
        ...
      ]
    }

    Reply by sending a simple JSON file with the id given to the new recipe, e.g.:

    {
      "id": 34
    }

    One should be able to display the new recipe at e.g. /recipe/34 (of course the number should correspond to the one in the JSON reply). In order for this task to be checked, send an email to bikol@wmi.amu.edu.pl with the subject “[DTIN] Z8.2c ######” (with your student ID number, which is a part of your VM address). (2 pt.)

Hints

  1. Probably the easiest way to store recipes is in a JSON file on disk. The json module in Python is able to load and save data from/to such files easily.

  2. Use a templating engine. Probably 3 templates should be enough: one main template and two templates extending it: one showing a single recipe, another showing the form for a new recipe.

  3. The JSON api should be very easy once you create the form and its handler. In the real life it would be possible for the form to be submitted by JSON through the api, but this requires JavaScript – browsers do not do it on their own.

  4. In this task you can assume that data sent to the application is correct. In the real you have to check (and double check) everything that is received by the server. Data coming from users should not be trusted as a malicious request can be easily crafted (opening your application to XSS attacks etc.).

  5. This task and the previous one can be done by one application. This conserves memory (which is the most precious resource you have on your VM), but if you have serious error, both tasks will be affected.

Extra task (at home)

Webhook

Create and deploy an application that consumes a git webhook and through this mechanism is able to restart another application after a push to its GIT repository. This should give automatic updates of the application. Webhooks are supported by both GitHub and our git.wmi.amu.edu.pl. Create a repository and integrate with your application to show that it works. Make sure to check HMAC signature that is present in the webhook.