Zagadnienia

  • Komunikacja aplikacji z serwerami HTTP
    • CGI
    • FastCGI
  • Pythonowe serwery aplikacji
    • WSGI
    • wsgiref
    • uWSGI
  • Pythonowe frameworki webowe
    • Model-View-Controller (MVC)
    • Django
    • Pyramid
    • Flask

Materiały

Komunikacja aplikacji z serwerem HTTP

Uruchamianie aplikacji internetowych w języku Python

MVC

Trzy najbardziej popularne frameworki webowe

Najważniejsze fragmenty dokumentacji Pyramid-a

Silnik szablonów

Zadania na zajęcia

Proste aplikacje WSGI (1 pkt.)

Najprostsza aplikacja WSGI wygląda następująco:

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()

Możemy ją uruchomić i obejrzeć efekt na stronie http://localhost:6543. Działającą aplikację można też obejrzeć tutaj. Nasza aplikacja w ogóle nie sprawdza adresu URL, który jest przez aplikację obsługiwany. Dla każdego żądania dostajemy zawsze “Hello world”. Aby obsługiwać różne żądania musimy przyglądać się właściwościom argumentu 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()

Możemy uruchomić tę aplikację lub spojrzeć tutaj. Można sobie wyobrazić, że będziemy sprawdzać jak wygląda żądany URL i będziemy obsługiwać je na różne sposoby, być może implementując kolejne funkcje/obiekty itp. To wszystko szybko stanie się żmudne i powtarzalne. Na scenę wkraczają frameworki webowe.

Frameworki są podobne do bibliotek: fragmenty kodu napisane przez innych, abyśmy my mogli ich używać. Frameworki różnią się od bibliotek: kod biblioteki jest wywoływany przez aplikację, podczas gdy framework działa we własnym zakresie i w ustalonych momentach wywołuje kod napisany przez programistę.

Podstawowa funkcjonalność zapewniania przez wszystkie frameworki (nie tylko w świecie Pythona) to mapowanie kodu do odpowiednich adresów URL. Konkretne metody różnią się między językami i frameworkami. Niektóre używają plików konfiguracyjnych XML/YAML/JSON, inne używają kodu lub położenia plików wykonywalnych w systemie plików.

Pierwszy przykład to bardzo prosta aplikacja napisana we frameworku Flask, która potrafi odczytywać argumenty przekazane poprzez adres URL.

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()

Aplikację można wypróbować tutaj.

Zapisanie pliku i uruchomienie z użyciem programu python3 w większości przypadków zaowocuje błędem ModuleNotFoundError: No module named 'flask'. Ponieważ nie mamy uprawnień administratora, nie możemy instalować pakietów Pythona dla całego systemu. Nawet gdybyśmy mogli, nie zawsze jest to najlepszy pomysł. Będziemy używać tzw. środowisk wirtualnych. Aby stworzyć nowe środowisko wirtualne i zainstalować w nim Flask, wykonujemy:

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

To tworzy nowe środowisk wirtualne w katalogu ~/pyvenv i instaluje w nim Flask. Następnie możemy używać pliku wykonywalnego ~/pyvenv/bin/python o uruchomienia naszej aplikacji (w rzeczywistości ten „plik wykonywalny” jest łączem do systemowego, ale ścieżka dla modułów jest odpowiednio ustawiona). Środowiska wirtualne doskonale się sprawdzają gdy nie możemy instalować pakietów w samym systemie lub chcemy mieć dwie wersje dla różnych aplikacji. W praktyce często używamy wielu środowisk wirtualnych.

Bardzo prosta aplikacja we frameworku Pyramid wygląda następująco:

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()

Uruchamiamy podobnie jak poprzednią aplikację (oczywiście po zainstalowaniu pakietu pyramid). Można ją też zobaczyć tutaj.

Przyjrzyj się różnicom i podobieństwom między tą aplikacją i tą poprzednią napisaną we Flask-u. Uruchom którąkolwiek aplikację pod kontrolą debuggera, np. w programie PyCharm czy pudb. Ustaw pułapkę w procedurze obsługi żądania i odwiedź ją w przeglądarce.

Szablony Jinja2 (1 pkt.)

Większość witryn internetowych ma wspólny wygląd, który jest wypełniany różną treścią w zależności od strony odwiedzanej przez użytkownika. Niektóre elementy, np. nawigacyjne czy stopka strony nie zmieniają się w ogóle, kiedy inne (np. okruszki nawigacyjne – breadcrumbs) zmieniają się tylko nieznacznie. Kod tworzący te elementy powinien być dodany do kodu obsługującego każde żądanie, równocześnie nie chcemy tego robić przez kopiowanie i wklejanie. Równocześnie byłoby skomplikowane pisać metody tworzące fragmenty strony: elementy nawigacyjne zwykle mieszają się zwykle z ogólnym wyglądem. Silniki szablonów są rozwiązaniem tego problemu.

Szablony to pliki tekstowe, które mogą zawierać zmienne i zmienne te są wypełniane konkretną treścią w czasie renderowania szablonu. Rezultatem jest plik tekstowy, który zawiera ustalony wygląd (zdefiniowany przez szablon) i taką zawartość jaką sobie wymarzymy (z reguły przekazywana w czasie renderowania szablonu). To pozwala grafikom tworzącym szablony pracować równolegle z programistami tworzącymi kod. Przedstawimy silnik Jinja2, ale istnieje wiele, wiele innych. Jinja2 jest co do zasady zgodna (a przynajmniej bardzo podobna) z Django Templates.

Utwórz następujący plik layout.jinja2:

<!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 pozwala szablonom rozszerzać inne szablony, co pozwala na mniejsze duplikowanie kodu, kosztem trochę większej złożoności. Mechanizm przypomina nieco dziedziczenie w programowaniu zorientowanym obiektowo. Utwórz poniżysz plik table.jinja2:

{% 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 %}

Utwórz poniższy plik lipsum.jinja2:

{% extends 'layout.jinja2' %}

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


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

Następnie zainstaluj pakiety pyramid-jinja2, pyramid-debugtoolbar i faker w swoim środowisku wirtualnym i uruchom poniższą aplikację:

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()

Uruchom aplikację przedstawioną w tym punkcie. Zmień główny szablon, by na każdej stronie aplikacja wyświetlała Twoje imię i nazwisko. Oryginalna aplikacja działa tutaj.

Obsługa formularzy oraz sesje (1 pkt.)

Większość aplikacji internetowych musi w którymś momencie odebrać dane od użytkownika. Mogą to być hasła, ustawienia itp. Z reguły do obsługi tych danych służą znane nam wszystkim formularze. Nauczymy się jak obsługiwać takie żądania.

Protokół HTTP jest protokołem bezstanowym i bezpołączeniowym, więc na poziomie protokołu nie ma żadnego pojęcia tożsamości klienta. Dane z reguły jednak jakoś łączą się z tożsamością, więc nasze aplikacje muszą mieć możliwość rozróżniania klientów, np. muszą być w stanie stwierdzić kiedy kolejne żądania pochodzą od tego samego klienta. Z reguły służy do tego bezpieczne ciasteczko ustawione w przeglądarce, co pozwala na na tzw. sesje (tymczasowe dane, które są prywatne dla każdego klienta z osobna). Ten mechanizm np. pozwala logować się w aplikacjach internetowych.

Istnieją różne strategie przechowywania danych sesji. Niektóre aplikacje przechowują je w przeglądarce (w ciasteczkach), inne przechowują je na serwerze w plikach bądź w bazie danych. Tutaj pokażemy tylko podstawowy mechanizm, który używa kryptograficznie podpisanych ciasteczek by przechowywać dane sesji. Poważniejsze aplikacje pewnie używałyby bazy danych (rozmiar ciasteczka jest ograniczony, więc nie mamy za dużo miejsca).

Nasza aplikacja ma jeden szablon (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>

Ponadto mamy jeden plik Pythona (form.py). Wymaga on biblioteki marshmallow. Możemy ją zainstalować wykonując 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()

Uruchom aplikację z tego zadania. Działająca aplikacja dostępna jest tutaj.

Uruchamianie aplikacji w wersji produkcyjnej (1 pkt.)

Przychodzi chwila gdy chcemy podzielić się naszą aplikacją ze światem. Aplikacje internetowe działają w internecie, co oznacza, że muszą być zintegrowane z działającym serwerem HTTP. Nauczymy się jak uruchomić aplikację z serwerem aplikacji uWSGI z serwerem nginx działającym jako tzw. reverse-proxy na naszych maszynach wirtualnych. Znaczy to, że przeglądarka łączy się z nginx-em, który przekazuje dane do serwera uWSGI, który poza tym jest dostępny tylko lokalnie.

Użyjemy tzw. trybu cesarza w uWSGI. Pozwala on aby aplikacje (wasalowie) były automatycznie uruchamiane przy starcie i ponownie uruchamiane w razie błędu.

Najpierw stworzymy aplikację do uruchomienia. Zobaczymy jak stworzyć pełnowymiarową aplikację w Pyramidzie i jak ona wygląda. Nasze poprzednie aplikacje były raczej w zasięgu mikroframeworka. Pyramid pozwala automatycznie tworzyć szkielety większych projektów, unikając tworzenia wszystkiego od początku za każdym razem.

Najpierw musimy zainstalować w naszym wirtualnym środowisku pakiet Pythona o nazwie cookiecutter. Następnie uruchom:

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

To stworzy projekt używający SQLAlchemy – bardzo dobrej Pythonowej biblioteki ORM, tzn. biblioteki pośredniczącej między aplikacją a serwerem bazodanowym. To oznacza minimalnie większą komplikację, ale pozwala na znacznie większą elastyczność. Programista nie musi pisać kwerend SQL, robi to za nas biblioteka. Pozwala to np. zmienić serwer z MySQL na PostgreSQL bez potrzeby przepisywania połowy kwerend (serwery bazodanowe używają podobnych, ale różnych dialektów SQL-a).

Cookiecutter wypisuje polecenia do wpisania, które uruchomią nowo utworzony projekt. Stworzą one nowe środowisko wirtualne dla tej jednej aplikacji. Pełnowymiarowe aplikacje we frameworku Pyramid są konfigurowane przez pliki INI, nie przez kod. To pozwala łatwo mieć różne konfiguracje dla tej samej aplikacji. W stworzonej dla nas aplikacji mamy development.ini i production.ini, konfiguracje odpowiednie dla tworzenia aplikacji i dla uruchomienia wersji finalnej.

Nie będziemy tutaj tłumaczyć struktury stworzonej aplikacji, ale warto ją sobie we własnym zakresie obejrzeć.

Spróbujemy uruchomić tę aplikację na maszynie wirtualnej. Pierwszym krokiem będzie zbudowanie wersji dystrybuowalnej i skopiowanie na maszynę wirtualną (zakładamy, że w ssh zdefiniowany jest skrót alias tin):

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

Potrzebujemy na maszynie wirtualnej kilku programów. Jako administrator wykonaj:

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

Zostanie zainstalowanych znacznie więcej pakietów (jako zależności).

Stwórz nowe wirtualne środowisko na maszynie wirtualnej i zainstaluj w nim naszą aplikację wraz z jej zależnościami:

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

Teraz możemy spróbować uruchomić aplikację w linii poleceń:

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

W tym momencie powinniśmy być w stanie połączyć się z naszą aplikacją z poziomu przeglądarki, wystarczy wpisać adres maszyny wirtualnej i port 6543. Pamiętaj, że maszyna wirtualna jest osiągalna tylko z kampusu lub poprzez proxy SSH/VPN. Zadania jest już prawie gotowe, musimy tylko przenieść konfigurację do pliku wasala.

W katalogu /etc/uwsgi-emperor/vassals utwórz plik *.ini (konkretna nazwa nie ma większego znaczenia). Wewnątrz umieść:

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

Pierwsza linijka mówi uWSGI gdzie oczekiwać połączeń (tutaj: gniazdo UNIX), ostatnia gdzie zapisywać plik dziennika.

Przy okazji, gdybyśmy chcieli uruchomić aplikację w stylu mikroframeworka (bez pliku INI), odpowiedni wasal wyglądałby następująco:

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

Parametr callable to nazwa obiektu/funkcji w wsgi-file, który powinien być uruchomiony jako aplikacja WSGI.

Na koniec musimy uruchomić uWSGI w trybie cesarza. Domyślna konfiguracja nie będzie działać z powodu uprawnień. Ominiemy problem przez uruchomienie uWSGI z uprawnieniami administratora. W prawdziwym środowisku produkcyjnym nie należy tak robić. Aby zmienić konfigurację cesarza, edytuj plik /etc/uwsgi-emperor/emperor.ini i zmień uid oraz gid z www-data na root. Zapisz plik. Teraz możemy uruchomić cesarza uWSGI i oznaczyć go jako usługę uruchamianą przy starcie systemu:

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

Już prawie skończyliśmy, musimy tylko skonfigurować nginx tak, by łączył się z naszym wasalem działając jako reverse-proxy. Konfiguracja nginx w Debianie jest podzielona między kilka plików (sites). Edytuj plik /etc/nginx/sites-available/default. Zmień

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

na

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;
}

To nakazuje nginx przekazywać ruch przychodzący na adres / do uWSGI nasłuchującego na wskazanym gnieździe UNIX. To jest tylko fragment pełnego pliku konfiguracyjnego, który znajduje się w pliku /etc/nginx/nginx.conf. Można mieć wiele sekcji server w pliku konfiguracyjnym, np. by hostować wiele domen na jednym serwerze lub obsługiwać różne porty. Każdy server może mieć wiele sekcji location, np. by hostować wiele aplikacji pod różnymi prefiksami. Jest to opisane tutaj.

Jeśli chcesz hostować kilka aplikacji WSGI pod różnymi prefiksami, przydatny będzie ten fragment.

Jeśli chcesz hostować kilka aplikacji konfigurowanych plikami INI z jedną instancją uWSGI pod różnymi prefiksami, możesz albo stworzyć małe skrypty w Pythonie, które uruchomią aplikacje z plików INI , użyć middleware URLMap. To druga opcja może być w praktyce bardziej kłopotliwa, ponieważ aplikacje będą współdzielić jeden interpreter Pythona (więc nie są aż tak od mocno izolowane).

Zadania obowiązkowe

JSON-owe API (3 pkt.)

Stwórz i uruchom na maszynie wirtualnej aplikację mnożącą liczby. Powinna nasłuchiwać żądań HTTP POST pod adresem /product na maszynie wirtualnej na porcie 8080. Aplikacja powinna akceptować plik JSON o następującej strukturze:

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

Wszystkie pola są dodatnimi liczbami całkowitymi bez ograniczenia rozmiaru. Całe żądanie będzie nie większe niż 2 MB.

Aplikacja powinna odpowiedzieć plikiem JSON o następującej strukturze:

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

Pole token powinno być takie samo jak w żądaniu, pole product powinno być równe a*b.

Żądanie inne niż POST powinny zwracać kod błędu 405 Method Not Allowed. Błędy w pliku JSON (e.g., liczby ujemne, pola nie będące liczbami) powinny dawać 400 Bad Request.

Aby zadanie zostało sprawdzone należy wysłać maila pod adres bikol@wmi.amu.edu.pl z tematem „[DTIN] Z8.1 ######” (z wpisanym własnym numerem indeksu, będącym częścią nazwy maszyny wirtualnej).

Wskazówki

  1. Aplikację można testować z użyciem cURL, np. w czasie jej tworzenia można wywołać:

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

    na końcu trzeba tylko zmienić adres.

  2. Aby sprawdzać czy przesłany plik zgadza się z zadanym formatem, możesz użyć plakiety jsonschema. Zapisywanie schematów jest szczegółowo opisane w książce dostępnej tutaj. Jeśli przeraża cię wizja nauki kolejnego narzędzia, sprawdzanie poprawności można też wykonać ręcznie. Wiele rzeczy może pójść nie tak, upewnij się, że wszystkie je wykryjesz. Dla przykładu poniższe wywołania powinny zwrócić Bad Request:

    • {}
    • {"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. Może się zdarzyć, że klient wyśle plik nie będący poprawnym plikiem JSON. Upewnij się, że wykryjesz takie przypadki (i zwrócisz 400 Bad Request).

  4. Python automatycznie obsługuje dowolnie duże liczby całkowite, więc nie powinno być problemu z obsługą poniższego (poprawnego) żądania:

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

Portal z przepisami (7 pkt.)

Stwórz aplikację wyświetlającą przepisy. Przepis obejmuje:

  • nazwę potrawy
  • składniki
  • zdjęcie (opcjonalnie)
  • kolejne kroki

Potrzebujesz metody przechowywania przepisów w aplikacji. Można zrobić to na różne sposoby: zwykłe plik tekstowe, baza danych, plik XML, plik JSON. Wybierz taki, który wydaje ci się najlepszy. Aby uprościć sprawę przechowuj tylko link do obrazka, nie cały obrazek. Przygotuj to tak by nie było limitu na liczbę składników/kroków w pojedynczym przepisie.

  • Przygotuj cztery różne przepisy. Wyświetlaj je (jako estetyczne strony HTML) pod adresami /recipe/{kolejne numery} na porcie 8080 na swojej maszynie wirtualnej (np. /recipe/1, /recipe/2 itd.). Aby zadanie zostało sprawdzone należy wysłać maila pod adres bikol@wmi.amu.edu.pl z tematem „[DTIN] Z8.2a ######” (z wpisanym własnym numerem indeksu, będącym częścią nazwy maszyny wirtualnej). (2 pkt.)

  • Pozwól użytkownikom dodawać nowe przepisy. Wyświetla odpowiedni formularz pod adresem /recipe/new. Dla prostoty nie pozwalaj na wysyłanie obrazków jako pliki, tylko gotowe adresy URL. Obrazki powinny być opcjonalne. Ponadto, dla prostoty przyjmij, że będzie najwyżej 5 składników i 10 kroków (jeśli potrafisz, możesz zrobić dowolnie wiele, ale to jest trudniejsze). Po wysłaniu poprawnego przepisu przenieś użytkownika na stronę nowo utworzonego przepisu. Zadanie zostanie sprawdzone ręcznie, w tym celu aplikacja musi cały czas działać. (3 pkt.)

  • Pozwól dodawać nowe przepisy poprzez wysłanie (POST-em) pliku JSON na adres /recipe/api/new. Plik JSON ma następującą strukturę:

    {
      "name": "Nazwa przepisu",
      "photo": "http://...",
      "ingredients": [
        "składnik 1",
        "składnik 2",
        ...
      ],
      "steps": [
        "krok 1",
        "krok 2",
        ...
      ]
    }

    Odpowiedz wysyłając plik JSON z id utworzonego przepisu, np.:

    {
      "id": 34
    }

    Nowy przepis powinniśmy być w stanie wyświetlić pod adresem np. /recipe/34 (numer powinien oczywiście odpowiadać temu wysłanemu w pliku JSON). Aby zadanie zostało sprawdzone należy wysłać maila pod adres bikol@wmi.amu.edu.pl z tematem „[DTIN] Z8.2c ######” (z wpisanym własnym numerem indeksu, będącym częścią nazwy maszyny wirtualnej). (2 pkt.)

Wskazówki

  1. Prawdopodobnie najprostszym sposobem przechowywania przepisów jest plik JSON na dysku. Moduł json w Pythonie pozwala łatwo odczytywać i zapisywać takie pliki.

  2. Użyj silnika szablonów. Prawdopodobnie trzy szablony powinny wystarczyć: główny oraz dwa rozszerzające (pojedynczy przepis i formularz).

  3. API JSON-owe powinno być bardzo proste gdy stworzysz już formularz i jego obsługę. W prawdziwym projekcie byłoby możliwe wysyłanie danych z formularza przez JSON do naszego API, ale to wymaga JavaScriptu – przeglądarki nie robią tego same z siebie.

  4. W tym zadaniu możesz przyjąć, że dane wysyłane do aplikacji są poprawne. W prawdziwej aplikacji dane otrzymywane z zewnątrz należy gruntownie sprawdzać. Nie należy ufać danym od użytkownikom gdyż bardzo łatwo skonstruować złośliwe żądanie (przez co aplikacja byłaby narażona np. na ataki XSS).

  5. To zadanie i poprzednie może być wykonane w jednej aplikacji. To oszczędza pamięć, która jest najcenniejszym zasobem na twojej maszynie wirtualnej. Jednak w przypadku poważnego błędu żadne z zadań nie będzie działać.

Zadanie dodatkowe

Webhook

Stwórz i uruchom na swojej maszynie wirtualnej aplikację konsumującą tzw webhook z git-a, która potrafi dzięki temu mechanizmowi zrestartować inną aplikację po wysłaniu kolejnej wersji do jej repozytorium git. To powinno pozawalać na automatyczne aktualizacje tej drugiej aplikacji. Webhooki są obsługiwane zarówno przez GitHub jak i nasz wydziałowy serwer git.wmi.amu.edu.pl. Stwórz repozytorium i zintegruj je ze swoją aplikacją by zademonstrować jak działa. Sprawdzaj podpisy HMAC obecne w webhooku.