Bottle: Micro Web Framework

Stellt hier eure Projekte vor.
Internetseiten, Skripte, und alles andere bzgl. Python.
Benutzeravatar
Defnull
User
Beiträge: 778
Registriert: Donnerstag 18. Juni 2009, 22:09
Wohnort: Göttingen
Kontaktdaten:

Nach etwa 4 Tagen und unzähligen Tassen Kaffee kann ich euch endlich mein eigenes Micro Web Framework präsentieren :D

bottle.py

( Der Name hat einen Sinn! Ich muss nur noch herausfinden, welchen ;) )

Ich werde es in Zukunft für meine eigenen Web-Projekte verwenden und auf diese Weise ausgiebig testen und weiter verbessern. Das nächste Update wird also nicht lange auf sich warten lassen. Das ganze steht unter einer MIT Lizenz und ist demnach ziemlich offen. Über Feedback, Betatester und Forks würde ich mich freuen ;)


Und wer zu faul ist, auf den Link da oben zu klicken, kann sich eine kleine Beispiel-Applikation auch gleich hier ansehen:

Code: Alles auswählen

from bottle import route, run, request, response, send_file, abort

@route('/')
def hello_world():
    return 'Hello World!'

@route('/hello/:name')
def hello_name(name):
    return 'Hello %s!' % name

@route('/hello', method='POST')
def hello_post():
    name = request.POST['name']
    return 'Hello %s!' % name

@route('/static/:filename#.*#')
def static_file(filename):
    send_file(filename, root='/path/to/static/files/')

@route('/counter')
def counter():
    old = request.COOKIE.get('counter',0)
    new = int(old) + 1
    response.COOKIE['counter'] = new
    return "You viewed this page %d times!" % new

@route('/private')
def private():
    if request.POST.get('password','') != 'secret':
        abort(401, 'Go away!')
    return "Welcome!"

run(host='localhost', port=80)
PS: Doppelpost zu diesem Beitrag. Sorry dafür, aber ich denke das passt besser in einen neuen thread.
stuhlbein
User
Beiträge: 89
Registriert: Freitag 9. Januar 2009, 16:08

gefällt mir gut das projekt!

da ich bei dem thema WSGI ebenfalls neu bin, werd ich mir das die tage auf jedenfall mal genauer anschauen, das gebrachte beispiel sieht jedenfalls gut aus ;)
audax
User
Beiträge: 830
Registriert: Mittwoch 19. Dezember 2007, 10:38

Mag es sein, dass das Ding nicht Threadsafe ist?
Kommt mir so vor, weil der Request nicht direkt den Funktionen übergeben wird und dadurich ne Race-Condition entsteht.
Benutzeravatar
snafu
User
Beiträge: 6731
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Ich glaube, es wäre einfacher, wenn man das OPTIMIZER-Flag per Keyword-Argument ändern könnte anstatt es im Quelltext umstellen zu müssen.

Und außerdem sollte es bei den Errorseiten IMHO möglich sein, den vorgegebenen HTML-Code bei Bedarf ändern zu können.

Aber an sich finde ich die API schon ziemlich cool. :) Leider nur 2 Buchstaben zu viel, um als echtes Microframework gelten zu können. ;P
Benutzeravatar
Defnull
User
Beiträge: 778
Registriert: Donnerstag 18. Juni 2009, 22:09
Wohnort: Göttingen
Kontaktdaten:

@Bitfish: Danke :) Bei Fragen, frag.

@audax: Die Modul-Variablen request und response sind beides Instanzen von Request bzw. Response die wiederum von threading.local erben und bei jeder Anfrage mit den gerade aktuellen Daten gefüttert werden. Das hat sich als performanter und auch angenehmer erwiesen, als die Objekte bei jeder Anfrage frisch zu erschaffen und an die Handler zu übergeben. Ist also alles Thread-Save :)
snafu hat geschrieben:Ich glaube, es wäre einfacher, wenn man das OPTIMIZER-Flag per Keyword-Argument ändern könnte anstatt es im Quelltext umstellen zu müssen.
Jup, wäre es. Ist in der aktuellen Version (0.3.3) geändert. Außerdem ist es nun per default aus geschaltet, da das umsortieren von Routen zu komischen Ergebnissen führen kann, wenn sich Routen überschneiden (/hello/world und /hello/:name) und der Benutzer nichts vom Optimierer weis.
snafu hat geschrieben: Und außerdem sollte es bei den Errorseiten IMHO möglich sein, den vorgegebenen HTML-Code bei Bedarf ändern zu können.
Du hast mit eigenen error-handlern volle Kontrolle über den output, also das, was an den Browser zurück gesendet wird. Alle Nicht-HTTP-Exceptions resultieren in einem 500er Fehler. Wenn man sein eigenes Debugging-Framework an die 500 bindet, kann man eigentlich alles machen, was man will :)
snafu hat geschrieben: Aber an sich finde ich die API schon ziemlich cool. :) Leider nur 2 Buchstaben zu viel, um als echtes Microframework gelten zu können. ;P
botl.py ;)
Bottle: Micro Web Framework + Development Blog
Benutzeravatar
snafu
User
Beiträge: 6731
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Oder BTTL. ;)
Benutzeravatar
Defnull
User
Beiträge: 778
Registriert: Donnerstag 18. Juni 2009, 22:09
Wohnort: Göttingen
Kontaktdaten:

Buffered Transistor Transistor Logic? ;)

Version 0.3.4 enthält nun auch ein etwas umfassenderes Beispiel
Version 0.3.5 behebt einen Fehler im POST Daten Parser und verfügt über etwas aussagekräftigere Fehlermeldungen im DEBUG Modus.
Bottle: Micro Web Framework + Development Blog
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

Nicht schlecht. Wäre dein Ziel Minimalismus, würde ich die ##-Syntax bei @route weglassen. Ich sehe nicht, warum ich noch eine Alternative zu regulären Ausdrücken brauche. Den Namen muss man sowieso als Funktionsparameter in der nächste Zeile wiederholen. Nett fände ich einen Weg, bei einem Parameter gleich anzugeben, ob das z.B. ein int sein muss. Andernfalls soll das Rahmenwerk gleich ein 400 werfen. Allgemein fände ich es hilfreich, wenn Parameter von Formularen (ähnlich wie es Rails automatisch macht) vorverarbeitet werden. Den threadlokalen Request finde ich gut. Das hält die Funktionsparameterliste übersichtlich. Das Tauschen von Matchern in der Liste der URLs mit einer Wahrscheinlichkeit von 1:1000 finde ich nach wie vor komisch.

Hast du dir auch eine Anbindung an Template-Engines überlegt oder willst du eine Micro-Templating-Engine integrieren?
Benutzeravatar
Defnull
User
Beiträge: 778
Registriert: Donnerstag 18. Juni 2009, 22:09
Wohnort: Göttingen
Kontaktdaten:

sma hat geschrieben:Wäre dein Ziel Minimalismus, würde ich die ##-Syntax bei @route weglassen.
Im Quelltext von Bottle ist das eine einzige Zeile und ich persönlich nutze es eigentlich recht gerne. Ich mag den "(?P<name>...)" Symtax nicht und nicht jeder kennt ihn überhaupt. Optional ist er natürlich trotzdem erlaubt.
sma hat geschrieben:Nett fände ich einen Weg, bei einem Parameter gleich anzugeben, ob das z.B. ein int sein muss. Andernfalls soll das Rahmenwerk gleich ein 400 werfen.
Das ist einfacher mit einem neuen Dekorator zu lösen, aber ne gute Idee.
sma hat geschrieben:Allgemein fände ich es hilfreich, wenn Parameter von Formularen (ähnlich wie es Rails automatisch macht) vorverarbeitet werden.
Autocast zu int oder float für alles, was wie ne Zahl aus sieht, mache ich absichtlich nicht. Das gehört in die Kategorie 'magic features' die eher nicht in so ein Framework gehören. Man muss eh prüfen, ob man vom Framework nen int oder nen string bekommt. Dann kann man auch gleich das Casting selbst machen.
sma hat geschrieben:Den threadlokalen Request finde ich gut. Das hält die Funktionsparameterliste übersichtlich.
Das war der Hauptgrund für diese Entscheidung :)
sma hat geschrieben:Das Tauschen von Matchern in der Liste der URLs mit einer Wahrscheinlichkeit von 1:1000 finde ich nach wie vor komisch.
Und per default aus geschaltet. Ich hab aber Tests gemacht: Schon bei 20 Routen macht die Position der Route in der Prüfliste einen Unterschied von bis zu 100 Requests/Sekunde (bei 400-500#/s im Durchschnitt, also 20-25%). Schließlich ist das Routing mit das rechenintensivste, das überhaupt im Framework passiert.
sma hat geschrieben:Hast du dir auch eine Anbindung an Template-Engines überlegt oder willst du eine Micro-Templating-Engine integrieren?
Ich wollte genau wie bei den WSGIServer auch für die verschiedenen Template Engines einheitliche Adapter basteln, damit man sie jederzeit nach belieben austauschen kann, ohne den App-Code zu ändern. Der erste Adapter wird wohl mako, weil ich das selbst sehr gerne verwende.
Bottle: Micro Web Framework + Development Blog
Dauerbaustelle
User
Beiträge: 996
Registriert: Mittwoch 9. Januar 2008, 13:48

Defnull hat geschrieben:
sma hat geschrieben:Nett fände ich einen Weg, bei einem Parameter gleich anzugeben, ob das z.B. ein int sein muss. Andernfalls soll das Rahmenwerk gleich ein 400 werfen.
Das ist einfacher mit einem neuen Dekorator zu lösen
Nein, wie soll denn der Dekorator aussehen? Bedenke, dass du innerhalb einer URL mehrere verschiedene Typen haben möchtest.
Benutzeravatar
Defnull
User
Beiträge: 778
Registriert: Donnerstag 18. Juni 2009, 22:09
Wohnort: Göttingen
Kontaktdaten:

Dauerbaustelle hat geschrieben:
Defnull hat geschrieben:
sma hat geschrieben:Nett fände ich einen Weg, bei einem Parameter gleich anzugeben, ob das z.B. ein int sein muss. Andernfalls soll das Rahmenwerk gleich ein 400 werfen.
Das ist einfacher mit einem neuen Dekorator zu lösen
Nein, wie soll denn der Dekorator aussehen? Bedenke, dass du innerhalb einer URL mehrere verschiedene Typen haben möchtest.
Ungetestetes Beispiel

Code: Alles auswählen

def validate(**vkargs):
    def decorator(func):
        def wrapper(**kargs):
            for key in vkargs:
                if key not in kargs:
                    abort(400, 'Missing parameter: %s' % key)
                try:
                    kargs[key] = vkargs[key](kargs[key])
                except ValueError, e:
                    abort(400, 'Wrong parameter format for: %s' % key)
            return func(**kargs)
        return wrapper
    return decorator

@route('/move/:id/:x/:y')
@validate(id=int, x=float, y=float)
def move(id, x, y):
    pass
Erklärung: Man übergibt validate() für jeden Parameter, den man testen möchte, eine Callable. Diese sollte die Eingabe wie gewünscht umwandeln und zurück geben oder ValueError werfen, wenn die Umwandlung nicht möglich ist. Alternativ kann man im Callable auch gleich HTTPError werfen oder (besser) abort() benutzen um dem User ne individuelle Fehlermeldung um die Ohren zu hauen :)

Konvertierung in alle Python Standard-Typen (int, float, ...) sind so direkt möglich. Mit eigenen Callables kann man aber noch viel mehr tun, zum Beispiel vollwertige Form-Validation. Für def-faule gibts ja auch noch lambda ;)

Code: Alles auswählen

@route('/add_list/:csv')
@validate(cvs=lambda x: map(int, x.strip().split(',')))
def add_list(cvs):
    ''' Adds a list of IDs (separated by ',') '''
    pass
Hmm ich glaub den Dekorator bau ich genau so ein ;)
Zuletzt geändert von Defnull am Samstag 4. Juli 2009, 17:24, insgesamt 1-mal geändert.
Bottle: Micro Web Framework + Development Blog
Dauerbaustelle
User
Beiträge: 996
Registriert: Mittwoch 9. Januar 2008, 13:48

Warum ein Extra-Validation-Element, wenn man die Validation auch gleich in die URL-Parameter reinmachen könnte?
Benutzeravatar
Defnull
User
Beiträge: 778
Registriert: Donnerstag 18. Juni 2009, 22:09
Wohnort: Göttingen
Kontaktdaten:

Dauerbaustelle hat geschrieben:Warum ein Extra-Validation-Element, wenn man die Validation auch gleich in die URL-Parameter reinmachen könnte?
Weil das den URL-Syntax zu kompliziert macht und mit nem Dekorator um einiges übersichtlicher ist. (Meine Meinung) Außerdem ist es (rein prinzipiell) nicht Aufgabe der Routen, die Parameter zu verändern.

Edit: Version 0.3.7 enthält den neuen decorator :)
Bottle: Micro Web Framework + Development Blog
Benutzeravatar
snafu
User
Beiträge: 6731
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Defnull hat geschrieben:
sma hat geschrieben:Wäre dein Ziel Minimalismus, würde ich die ##-Syntax bei @route weglassen.
Im Quelltext von Bottle ist das eine einzige Zeile und ich persönlich nutze es eigentlich recht gerne. Ich mag den "(?P<name>...)" Symtax nicht und nicht jeder kennt ihn überhaupt. Optional ist er natürlich trotzdem erlaubt.
Der Meinung bin ich übrigens auch. Gerade dieser Punkt ist mir sehr positiv ins Auge gesprungen. Es gibt bestimmt einige Leute (ich bin einer davon), die soetwas tausendmal lieber benutzen als Regexes.
Defnull hat geschrieben:
Dauerbaustelle hat geschrieben:Warum ein Extra-Validation-Element, wenn man die Validation auch gleich in die URL-Parameter reinmachen könnte?
Weil das den URL-Syntax zu kompliziert macht und mit nem Dekorator um einiges übersichtlicher ist.
Man könnte zusätzlich zum Dekorator noch `validate` als Keywordargument anbieten, welches ein Dictionary erwartet:

Code: Alles auswählen

@route('/move/:id/:x/:y', validate=dict(id=int, x=float, y=float))
def move(id, x, y):
    pass
EDIT: Oder die Typen in einer Liste übergeben, ähnlich wie ctypes das bei den Argumenten macht. Ist vielleicht noch etwas besser.
Zuletzt geändert von snafu am Sonntag 5. Juli 2009, 06:54, insgesamt 2-mal geändert.
nemomuk
User
Beiträge: 862
Registriert: Dienstag 6. November 2007, 21:49

das mit dem validate Dekorator gefällt mir irgendwie nicht... finde es besser und einfacher direkt in der URL-Rule.
Benutzeravatar
Defnull
User
Beiträge: 778
Registriert: Donnerstag 18. Juni 2009, 22:09
Wohnort: Göttingen
Kontaktdaten:

snafu hat geschrieben: Man könnte zusätzlich zum Dekorator noch `validate` als Keywordargument anbieten, welches ein Dictionary erwartet:

Code: Alles auswählen

@route('/move/:id/:x/:y', validate=dict(id=int, x=float, y=float))
def move(id, x, y):
    pass
Da finde ich den Decorator übersichtlicher. Aber wenn gewünscht, kann ich mir was direkt für den route() decorator überlegen. Direkt im Syntax ist das wie gesagt nicht so einfach, da re.match() nun mal keine Typen-Umwandlung macht.
snafu hat geschrieben:Oder die Typen in einer Liste übergeben, ähnlich wie ctypes das bei den Argumenten macht. Ist vielleicht noch etwas besser.
Das wiederum geht nicht, da die Platzhalter keine wirklich eindeutige Ordnung haben: "/archive/(:year/:month/:day|/:month/:year)"
Es muss schon nen dict sein.
Bottle: Micro Web Framework + Development Blog
Benutzeravatar
snafu
User
Beiträge: 6731
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Defnull hat geschrieben:"/archive/(:year/:month/:day|/:month/:year)"
Wie wäre es dann mit einer eigenen Syntax?

Code: Alles auswählen

"/archive/(:year/:month/:day|/:month/:year){int}"

"/move/:id{int}/:x{float}/:y{float}"
Will man nicht auf Typen prüfen, lässt man es bei dem entsprechenden Argument weg (also keine Klammern).

Keine Ahnung, inwiefern sich das im regelmäßigen Gebrauch als praktikabel erweisen würde und man muss natürlich aufpassen, dass sich Regex-Syntax und eigene Syntax nicht beißen.
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

Vieleicht ist es doch Zeit für Python 3.x:

Code: Alles auswählen

#!/usr/bin/env python3.1

def typecheck(f):
    def wrapper(*args):
        args = [f.__annotations__[n](a)
            if n in f.__annotations__ else a
            for a, n in zip(args, f.__code__.co_varnames)]
        return f(*args)
    return wrapper

@typecheck
def test(a: int):
    print(a)

test(42)
test("Hallo, Welt")
Stefan
lunar

Man kann sich das Routing auch von Werkzeug abschauen ... dessen Routing-Syntax erlaubt Typangaben in Regeln, ist aber trotzdem nicht komplex.

Die Validierung vom Routing zu trennen, mag zwar konzeptionell elegant sein, hat aber praktische Nachteile. Man kann beispielsweise nicht nach Typen routen. Außerdem zwingt es zu imho unnötigen Duplizierung von Informationen. Die Argumente eines Views müssen so an drei verschiedenen Stellen angegeben werden: In der Funktionssignatur, in der Route und im Validierungsdekorator.
Benutzeravatar
Defnull
User
Beiträge: 778
Registriert: Donnerstag 18. Juni 2009, 22:09
Wohnort: Göttingen
Kontaktdaten:

lunar hat geschrieben:Man kann sich das Routing auch von Werkzeug abschauen ... dessen Routing-Syntax erlaubt Typangaben in Regeln, ist aber trotzdem nicht komplex.
Die Routing-Komponente von Werkzeug ist aber auch 2 mal so groß wie mein ganzes Framework ;)

Ich hab länger drüber nach gedacht und werde den Routing-Syntax voraussichtlich nicht um Typen erweitern. Aus folgenden Gründen:

Aus Prinzip: Routen routen. Das ist ihre Aufgabe. Was nicht zu ihren Aufgaben gehört ist das Verändern und Prüfen von Objekten. In URLs gibt es nämlich keine Objekte, sondern lediglich Strings. Für mich gehört das Validieren und Umwandeln von Parametern nicht in die Routen, sondern in dien Logik-Code. Da, wo man auch sonst die Verarbeitung von Parametern erwarten würde.

Aus technischen Gründen: Momentan basiert das komplette Routing auf re.match() und match.groupdict(). Beide sind sehr schnell und unkompliziert, unterstützen aber nativ keine Typen-Umwandlung. Ein eigener Syntax für Typen würde den Routing-Teil des Frameworks daher um nicht wenige Zeilen Code erweitern, ohne einen wirklichen Vorteil zu bringen.

Weil es überflüssig wäre: Alles, was man mit dem erweiterten Syntax erreichen könnte, ist jetzt schon mit dem validiate() dekorator möglich. Dieser ist bereits enthaltenen und löst das Problem eigentlich sehr einfach und übersichtlich. Außerdem erlaubt er sogar noch das Prüfen und Verändern komplexer Parameter durch lambda oder eigene callbacks. Lambda in den Routing-Syntax einzubauen wäre sicher nicht mehr ganz so einfach.


Aber ich mach folgende Kompromisse:
Man wird dem validator sagen können, das er, statt einen 400er zu werfen, einfach die entsprechende Route ignorieren soll. Außerdem wird der route() dekorator um as validate-Argument erweitert, wenn man zu faul ist, die dekoratoren selbst aneinander zu ketten.

Beides in der nächsten Version (voraussichtlich noch heute)
Bottle: Micro Web Framework + Development Blog
Antworten