bottle - the right way to use
Vorwort:
Python wird nicht umsonst die Sprache der "Hacker" genannt. Alleine schon mit dem übermächtigen Scapy Modul, welches Paketmanipulation auf allen Netzwerkschichten erlaubt, besteht ein Grund, einen Webserver+Anwendung in Python zu schreiben. Zudem hat Python in seiner ganzen Historie lediglich eine Hand voll Sicherheitslücken zu beklagen - hingegen PHP¹ über alle Versionen hinweg tausende... der zweite Grund auf Python zu setzen.
Da wir uns nun entscheiden haben, Python zu verwenden, nutzen wir auch dessen Stärken - nämlich das Verwenden von Modulen. Das Rad neu zu erfinden ist nie eine gute Idee.
Mit bottle² steht uns ein kleines aber mächtiges Mikro-Rahmenwerk zur Verfügung, welches von Marcel Hellkamp geschrieben wurde. Bottle nimmt einem im Grunde drei Aufgaben ab:
erstens kümmert sich bottle um das Routing, zweitens hat bottle eine Template-Engine mit an Bord und drittens stellt bottle eine Vielzahl an Serverschnittstellen (APIs) zur Verfügung. Zudem ist bottle eine einzige Datei, was dies sozusagen portabel macht, d.h., man müsste es gar nicht installieren sondern nur in den Projektordner einfügen. qpython liefert bottle von Haus aus mit.
Mit bottle kann man also in wenigen Minuten eine vollständige Webanwendung (web application-->app) schreiben.
Diese kleine Anleitung setzt gewisse Python-, HTML/CSS- und Netzwerk-(Grund)Kenntnisse voraus.
Installation:
Bottle setzt keinerlei Abhängigkeiten voraus. Sämtliche Module sind in der Python Standardbibliothek enthalten.
Zusätzlich für später noch das Bootstrap Rahmenwerk sich besorgen unter http://getbootstrap.com
Einrichtung:
Um eine kleine App zu schreiben, richtet man von Beginn an sauber eine Ordner- und Dateistruktur ein, welche wie folgt aussieht:
my_web_app/ #kann beliebig benannt werden
├── htdocs/ #kann beliebig benannt werden, ggf. auch einfach static/
│ ├── css/
└── bootstrap.css
└── bootstrap-theme.css
│ ├── fonts/
│ └── img/
│ └── js/
└── bootstrap.js
├── views/ #muss so benannt werden
├── app.py #kann beliebig benannt werden
Alles anzeigen
Der Ordner ``my_web_app`` kann beliebig liegen; vielleicht hat man bereits einen Projektordner, wo ein Unterordner für das Projekt erstellt werden kann. Die Dateien aus dem Bootstrap fügt
man in die jeweiligen Ordner.
Webanwendung:
Nachdem die Ordnerstruktur und die app.py (<--UTF-8 ohne Byte Order Mark) erstellt wurden, öffnen wir die Datei und fügen folgendes ein:
app.py
#!/usr/bin/env python
# coding: utf-8
from bottle import Bottle, static_file, run, template
STATIC_FOLDER = '/home/pi/projects/my_web_app/htdocs'
app = Bottle()
@app.route('/static/:path#.+#')
def static_files(path):
return static_file(path, root=STATIC_FOLDER)
@app.route('/index')
def index():
return '<b>Hello World!</b>'
run(app, host='localhost', port=8089, reloader=True, debug=True)
Alles anzeigen
Mit der Konstante ``STATIC_FOLDER`` verweisen wir auf unser Ordner ``htdocs``. Somit weiss bottle, wo all unsere .css, .js, Bilddateien etc. liegen werden. Im Verlauf der
Weiterentwicklung unserer App muss nur noch zB. ``/static/img/mein_bild.jpg`` im Template hinterlegt werden.
Für jede Route schreiben wir eine Funktion. Im obigen Beispiel wird einfach nur ein String an den Client/Browser gesendet. Da bottle aber eine Template-Engine hat, wollen
wird dies so nicht tun.
Templating:
Bottle kennt zwei Arten um Templates³ in HTML wiederzugeben (rendering). ``template`` und ``view``. Ich selber nutze die template() Klassenmethode.
Um gleich zu Beginn sauber anzufangen, setzten wir hier das mächtige und meistverwendetste HTML/CSS Rahmenwerk der Welt ein: bootstrap.
Dazu legen wird im Ordner ``views`` eine neue Datei namens ``main.tpl`` an. Da bottle automatisch die Templates im Ordner ``views`` sucht, muss der Ordner auch so lauten.
Die Dateiendung lautet auf ``.tpl``. Auch das weiss bottle und macht uns das Leben bzw. Schreiben im app.py Programm leichter.
Unser Haupttemplate sieht so aus:
main.tpl
<!DOCTYPE html>
<html lang="de">
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta charset="utf-8">
<title>
{{title}}
</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link href="/static/css/bootstrap.css" rel="stylesheet">
<link href="/static/css/bootstrap-theme.css" rel="stylesheet">
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<script src="/static/js/bootstrap.js"></script>
<body>
{{!base}}
</body>
</html>
Alles anzeigen
Wie im obigen Code zu erkennen, gibt es hier eine untypische HTML Schreibweise, nämlich ``{{}}``. Hierbei handelt es sich nicht um HTML sondern um bottles Template Syntax. Sogenannte Platzhalter.
Mit ``{{!base}}`` laden wir im späteren Verlauf die Templates der jeweiligen Routes dynamisch in unser Grundgerüst. Dazu passen wir sogleich unsere app.py Datei wie folgt an:
app.py
Im Order ``views`` erstellen wir eine neue Datei namens ``index.tpl``. Im obigen Code brauchen wir in den ganzen Dateinamen nicht mehr auszuschreiben.
index.tpl
Der Ordner ``views`` muss nun so aussehen:
Layouts können hier http://getbootstrap.com/getting-started/ geladen werden.
Diese Anleitung basiert auf: http://getbootstrap.com/examples/theme/#
Server:
Bottle kommt mit einem eigenen Webserver daher, welcher sich gut eignet, ein wenig zu testen und rumfuhrzuwerken. Wenn das Projekt aber wächst, wird es Zeit, sich für einen kampferprobten Server zu entscheiden.
Ich selber nutze cherrypy und gevent. In diesem Beispiel installieren wir cherrypy (neu ab Version 10.2.2 cheroot) und nutzen dessen Webserver. Für die Dauer der Entwicklung stellt bottle zwei nützliche Methoden zur Verfügung: ``reloader`` und ``debug``.
Wenn wir unsere app.py bearbeiten und speichern, müssen wir den Server bzw. das Programm nicht beenden und wieder starten. Aber Achtung, nach Vollendung des Projekts verzichtet man besser darauf wie es in der Doku auch nachzulesen ist.
ZitatThe Debug Mode is very helpful during early development, but should be switched off for public applications. Keep that in mind.
in der app.py stellen wir nun auf unser neuer Webserver um:
from bottle import Bottle, static_file, run, template, CherootServer
...
run(app, server=CherootServer, host='localhost', port=8089, reloader=True, debug=True)
Cherrypy's Webserver ist ein robuster, multithreaded Server, der auch bei uns in zahlreichen Anwendungen seit Jahren eingesetzt wird.
GPIO:
Im folgenden Beispiel verwende ich das ``gpiozero`` Modul. Selbstverständlich kann jede beliebige Bibliothek verwendet werden. gpiozero ist jedoch anfängerfreundlich und sehr gut dokumentiert.
Es ist nur ein Minimalbeispiel eines PIR-Sensors. Dazu schreiben wir eine neue Route und ein neues Template namens ``gpio.tpl`` sowie eine JavaScript Datei ``gpio.js``, welche in den Ordner htdocs/js/ kommt.
https://gpiozero.readthedocs.io/en/v1.3.1/index.html
Edit:
RPi.GPIO und pigpio hinzugefügt.
app.py
from bottle import Bottle, static_file, run, template, CherootServer, request
from gpiozero import MotionSensor
from RPi import GPIO
import pigpio
@app.route('/gpio')
def get_gpio_status():
return template('gpio', status='')
@app.route('/gpio', method='POST')
def set_gpio_status():
#global pir
global motion
pir_status = int(request.forms.get('pir_status'))
#pir = MotionSensor(20)
#GPIO.setmode(GPIO.BCM)
#GPIO.setup(PIR_PIN, GPIO.IN)
pir = pigpio.pi()
if pir_status:
print('start')
#pir.when_motion = motion_detected
motion = pir.callback(20, pigpio.RISING_EDGE, get_motion)
#GPIO.add_event_detect(PIR_PIN, GPIO.RISING, callback=get_motion)
else:
print('exit')
#pir.close()
#GPIO.cleanup()
motion.cancel()
Alles anzeigen
gpio.tpl
%rebase('main', title='GPIO with gpiozero')
<br>
<h3>GPIO with gpiozero</h3>
<br>
<br>
<form>
<button type="submit" id="pir_status" name="pir_status" class="btn btn-lg btn-success" value="1">PIR ON</button>
<button type="submit" id="pir_status" name="pir_status" class="btn btn-lg btn-danger" value="0">PIR OFF</button>
</form>
gpio.js
$(document).ready(function() {
$('button').on('click', function(event) {
event.preventDefault();
$.ajax({
type: "POST",
cache: false,
url: $(this).parent('form').attr('action'),
data: $(this).parent('form').serialize() + '&' + $(this).attr('name') + '=' + $(this).val(),
});
});
});
Alles anzeigen
RPi.GPIO Version mit Template
Spoiler anzeigen
app.py
#!/usr/bin/env python
# coding: utf-8
from bottle import Bottle, static_file, run, template, CherootServer, request
from RPi import GPIO
import datetime as dt
from itertools import count
from functools import partial
STATIC_FOLDER = '/home/pi/Python/my_web_app/htdocs'
app = Bottle()
PIR_PIN = 20
motion_list = [] # should be written into a DB and NOT as a global variable!
def setup_gpio():
GPIO.setmode(GPIO.BCM)
GPIO.setup(PIR_PIN, GPIO.IN)
@app.route('/static/:path#.+#')
def static_files(path):
return static_file(path, root=STATIC_FOLDER)
def get_motion(counter, PIR_PIN):
motion_list.append('{} {}'.format(counter.next(), dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S')).split())
print motion_list
@app.route('/index')
def index():
return template('index')
@app.route('/gpio')
def get_gpio_status():
return template('gpio', motion_list=motion_list)
@app.route('/gpio', method='POST')
def set_gpio_status():
counter = count(1)
pir_status = int(request.forms.get('pir_status'))
if pir_status:
print('start')
GPIO.add_event_detect(PIR_PIN, GPIO.RISING, callback=partial(get_motion, counter))
else:
print('exit')
GPIO.cleanup()
def main():
setup_gpio()
run(app, server=CherootServer, host='0.0.0.0', port=8080, reloader=True, debug=True)
if __name__ == '__main__':
main()
Alles anzeigen
gpio.tpl
%rebase('main', title='GPIO with gpiozero', active='class="active"')
<br>
<br>
<h3>GPIO with gpiozero</h3>
<br>
<br>
<form>
<button type="submit" id="pir_status" name="pir_status" class="btn btn-lg btn-success" value="1">PIR ON</button>
<button type="submit" id="pir_status" name="pir_status" class="btn btn-lg btn-danger" value="0">PIR OFF</button>
</form>
<table class="table table-striped table-bordered table-condensed" name="my_table">
<thead>
<tr>
<th>Bewegung</th>
<th>Datum</th>
<th>Uhrzeit</th>
</tr>
</thead>
<tbody>
% for row in motion_list:
<tr>
%for col in row:
<td>{{col}}</td>
% end
</tr>
% end
</tbody>
</table>
Alles anzeigen
Aus der jQuery Bibliothek verwenden wir die Ajax (Asynchronous JavaScript and XML) Funktion, um Daten ohne URL-Paramenterübergabe an den Server zu senden.
Datenbank:
wird noch folgen
In den allermeisten Fällen reicht eine SQLite DB. Vorher sollte man sich Gedanken über einen Wechsel des Rahmenwerks machen.
https://bottlepy.org/docs/dev/plugi…le-sqliteplugin
https://github.com/iurisilvio/bottle-sqlalchemy
WebSocket:
wird noch folgen
Sofern man nicht im Hochfrequenzhandel tätig ist und sich keinen Schlüssel für die API der Frankfurter Börse ergaunert hat, sehe ich kein Grund für WebSockets.
WebSocket setzen fundierte Kenntnisse in JS voraus. Für ein wsgi Rahmenwerk sind die Möglichkeiten sowieso sehr limitiert.
Eine Seite clientseitig neu laden zu lassen reicht in den meisten Fällen völlig aus.
Siehe folgendes Beispiel:
Problem mit bottle und ws4py
Fragen, Anregungen, Kritik, Ergänzungen etc. einfach hier stellen bzw. laut Boardregeln ein separates Thema aufmachen.
Edit
Wichtig: Seit dem Update Cherrypy auf die Version 10.2.2 wurde der WSGI Server abgetrennt und als separates Projekt namens Cherrot-Server dem Cherrypy Paket beigefügt
ZitatDeprecationWarning: Warning: Use of deprecated feature or API. (Deprecated in Bottle-0.13)
Cause: The wsgi server part of cherrypy was split into a new project called 'cheroot'.
Fix: Use the 'cheroot' server adapter instead of cherrypy.
.
Bottle hat dies in der aktuellen Version 0.12.13 noch nicht implementiert nur in der aktuellen Developer Version 0.13. Diese muss manuell runtergeladen werden oder man überschreibt die aktuelle Version unter ``/usr/local/lib/python2.7/dist-packages/bottle.py``
Wenn jemand eine ältere Version <0.9 von Cherrypy im Einsatz hat, dann muss anstelle von
neu
verwendet werden.
https://github.com/bottlepy/bottl…bottle.py#L3246
https://github.com/cherrypy/chero…eroot/server.py
Quellen:
¹) http://www.php.net/ChangeLog-5.php
²) http://bottlepy.org/docs/dev/#license
³) https://bottlepy.org/docs/dev/stpl.…template-syntax