[bottle] Schnittstellen-Design für Multi-App Unterstützung

Django, Flask, Bottle, WSGI, CGI…
Antworten
Benutzeravatar
Defnull
User
Beiträge: 778
Registriert: Donnerstag 18. Juni 2009, 22:09
Wohnort: Göttingen
Kontaktdaten:

In seiner aktuellen Form ist Bottle ziemlich ungeeignet, mehr als eine Applikation zur gleichen Zeit zu verarbeiten. Bottle wurde für kleine Projekte entwickelt und geht davon aus, das alles auf Modulebene passiert. Es kommt aber immer häufiger der Wunsch, Bottle Applikationen austausch-, kombinier- und wiederverwendbar zu entwickeln. Daher möchte ich die Unterstützung für komplexere Szenarien ausbauen; natürlich ohne dadurch den einfachen Fall komplizierter zu machen.

Ziel ist es, a) in einem Modul oder Packet mehr als eine Bottle Applikationen verwalten zu können und b) mehrere Bottle Apps konfliktfrei in einem Serverprozess laufen zu lassen.

Punkt a) habe ich schon seit einiger Zeit vorbereitet. app() oder default_app() kennt ihr ja. Diese Funktion liefert ein WSGI-Funktionsobjekt zurück.

In Wirklichkeit ist app() allerdings ein Stack, der mehrere Bottle() Instanzen halten kann und der Rückgabewert ist der jeweils oberste Eintrag auf dem Stack. Mit app.push() und app.pop() kann man den Stack manipulieren.

Eine Bottle() Instanz dient übrigens nicht nur als WSGI Schnittstelle, sondern hat z.B. auch sämtliche Routen in sich gespeichert.

Es ist also jetzt schon möglich, die ganzen Aufrufe von @route() in eine separate Bottle Applikation um zu leiten, da @route(), genau wie die meisten anderen Module-Level Funktionen, intern auf app() zurück greift um die jeweils aktuelle Bottle() Instanz zu bekommen. Man kann also Module schreiben, die App-Objekte erzeugen, ohne mit anderen Modulen in Konflikt zu treten.

Code: Alles auswählen

from bottle import route, app
app.push()
@route('/')
def index()
    return 'MyApp'
myapp = app.pop()
Häufiges Problem: Wenn ich myapp irgendwo anders einbinden und verwenden will, soll es nicht auf '/' hören, sondern auf '/myapp/' oder '/some/other/path/'. Damit kommen wir zu b)

Punkt b) ist etwas schwieriger. Ich erzähle einfach mal, was mir vor schwebt:

Die neue Funktion bottle.mount(app, path) soll ermöglichen, eine separate Bottle Applikation (eventuell auch eine normale WSGI app) unter einem bestimmten Pfad einzubinden.

Code: Alles auswählen

bottle.mount(admin_app, '/admin')
Dies würde z.B. eine Admin Applikation immer dann aufrufen, wenn request.path mit '/admin' beginnt. Dabei wird '/admin' von request['PATH_INFO'] abgeschnitten und an request['SCRIPT_PATH'] angefügt. Der restliche Pfad wird dann von den Routen der Admin-App erneut aufgelöst, diesmal ohne das '/admin'. Für die Admin-App ist dieser Vorgang also absolut transparent.

Die Funktion bottle.merge(newapp, path) funktioniert ähnlich, nur dass die beiden Module miteinander verschmolzen werden. Alle Routen von newapp werden so verändert, das sie mit dem neuen Pfad beginnen. Dieser kann auch leer sein, was die beiden Apps komplett vermixen würde. PATH_INFO oder SCRIPT_PATH werden dabei nicht verändert. So ein Merge ist später schneller, da nur ein Router verwendet wird, allerdings auch nicht mehr rückgängig zu machen, wohingegen Mounts auch dynamisch wieder ausgehängt werden können.

Beide Funktionen könne außerdem einen Modulnamen als String im ersten Parameter verarbeiten. Dann wird mit app.push() und app.pop() gearbeitet und das benannte Modul mit __import__ geladen.


Soweit der Plan. Gibt es Szenarien, die damit nicht lösbar sind? Habt ihr andere Vorschläge oder Ideen?
Bottle: Micro Web Framework + Development Blog
Benutzeravatar
Defnull
User
Beiträge: 778
Registriert: Donnerstag 18. Juni 2009, 22:09
Wohnort: Göttingen
Kontaktdaten:

bottle.mount(app, path) ist implementiert. War leichter, als ich dachte.
Bottle: Micro Web Framework + Development Blog
Benutzeravatar
jens
Python-Forum Veteran
Beiträge: 8502
Registriert: Dienstag 10. August 2004, 09:40
Wohnort: duisburg
Kontaktdaten:

Hört sich nach Django's url include an: http://docs.djangoproject.com/en/dev/to ... r-urlconfs

Ich frage mich allerdings, wohin willst du mit bottle???

GitHub | Open HUB | Xing | Linked in
Bitcoins to: 1JEgSQepxGjdprNedC9tXQWLpS424AL8cd
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

Sinatra und Bottle sind sich sehr ähnlich. Bei Sinatra sind sie Leute den Weg gegangen, dass es eine Basisklasse für eine Sinatra-App gibt, die man einsetzen sollte, wenn man mehr machen will als eine kleine Ad-hoc-Anwendung im klasischen Stil. Rack (das WSGI von Ruby) erlaubt es dann, Sinatra-Apps zu kombinieren und z.B. überall hin zu mounten.

Das push/pop finde ich komisch. Bestenfalls könnte ich akzeptieren, wenn man sagt, dass ein Modul (wie auch immer) automatisch zu einer App wird.

Da explizit aber besser als implizit ist, fände ich es so am besten:

Code: Alles auswählen

class App(bottle.App):
    @route("/")
    @route("/", method="POST")
    def index(self):
        ...
    
    @route("/info/:name")
    def info(self, name):
        ...
Stefan
ms4py
User
Beiträge: 1178
Registriert: Montag 19. Januar 2009, 09:37

sma hat geschrieben:Da explizit aber besser als implizit ist, fände ich es so am besten:

Code: Alles auswählen

class App(bottle.App):
    @route("/")
    @route("/", method="POST")
    def index(self):
        ...
    
    @route("/info/:name")
    def info(self, name):
        ...
Stefan
Da wäre dann wieder meine gewünschte OO im Spiel :-) Finde ich gut so!
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

Eine Klasse ist in Python nun mal der einzige zur Verfügung stehende Container. Dies wäre wieder ein bisschen magisch:

Code: Alles auswählen

with bottle.App:
    @route("/info/:name")
    def info(self, name):
        ...
Nachteil in Python ist dann leider nur, dass (aufgrund der Explizitheit) der Code jetzt mit `self`s gesprenkelt wird. Special vars a la Lisp (also thread-lokale globale Variablen) sind in geringer Dosis daher eine gute Alternative. IMHO.

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

jens hat geschrieben:Hört sich nach Django's url include an: http://docs.djangoproject.com/en/dev/to ... r-urlconfs

Ich frage mich allerdings, wohin willst du mit bottle???
Bottle AppStore ;) Nee, im ernst: Bottle war von Anfang an auf vergleichsweise kleine Projekte ausgelegt und wird es auch bleiben. Bottle hat aber auch versucht, Projekten, die größer werden, keine Steine in den Weg zu legen. Darum gibt es die Server- und TemplateAdapter und darum ist Bottle 100% WSGI konform.

Die neue Funktionalität ist ein reiner Gewinn. Für kleine Projekte hat sie überhaupt keinen Einfluss, nicht einmal die API ändert sich dadurch. Man kann sie einfach ignorieren. Für größere Projekte die modularisieren wollen ist sie Gold Wert und ermöglicht Dinge, die vorher unmöglich waren. Außerdem funktioniert das Feature hervorragend und ohne wenn und aber. Es ist ein heiles, sauberes, sinnvolles und optionales Feature. Ich persönlich werde es definitiv nutzen, und das ist Grund allein :)
Bottle: Micro Web Framework + Development Blog
Benutzeravatar
Defnull
User
Beiträge: 778
Registriert: Donnerstag 18. Juni 2009, 22:09
Wohnort: Göttingen
Kontaktdaten:

sma hat geschrieben:Sinatra und Bottle sind sich sehr ähnlich. Bei Sinatra sind sie Leute den Weg gegangen, dass es eine Basisklasse für eine Sinatra-App gibt, die man einsetzen sollte, wenn man mehr machen will als eine kleine Ad-hoc-Anwendung im klasischen Stil. Rack (das WSGI von Ruby) erlaubt es dann, Sinatra-Apps zu kombinieren und z.B. überall hin zu mounten.
Es widerspricht dem one-file Prinzip von Bottle aber komplett, wenn man ein Drittanbieter-Paket oder ne extra WSGI Bastelei braucht, nur um mehr als ein Modul in Bottle zum laufen zu bringen. Vor allem da Bottle doch ein hervorragendes Routing-System mit sich bringt und es eigentlich hervorragend selbst kann.

Das Problem ist, das die normalen Bottle Apps als eine Abfolge von @route() Dekoratoren geschrieben werden. Das ist so und soll auch so bleiben, schließlich ist das eines der beliebtesten Eigenschaften von Bottle. Importiert man sie aber, installieren sie sich in die root-Applikation. DAS wiederum kann ein Problem sein, wenn man ein fremdes Modul einbinden will (Wiederverwendbarkeit von Modulen) oder einfach nur selbst entscheiden will, unter welcher URL die Funktionen des Moduls erreichbar sein sollen, ohne das ganze Modul zu editieren.

Die Stack-Funktion von bottle.app macht relativ einfach möglich, was vorher unmöglich war. Die with- oder Klassenansätze haben alle den gleichen Harken: Die müssen vom App-Autor umgesetzt werden, nicht vom App-Benutzer. Damit ändert sich die API für den Großteil der Bottle-Anwender in eine umständliche Richtung und das ist ein absolutes no-go.
sma hat geschrieben: Das push/pop finde ich komisch. Bestenfalls könnte ich akzeptieren, wenn man sagt, dass ein Modul (wie auch immer) automatisch zu einer App wird.
Geplant ist, das man Module anhand ihres Namens importieren kann und das app.push() und app.pop() dann automatisch passiert. Daran muss ich aber noch etwas feilen. Was mach ich z.B., wenn das gewünschte Modul bereits importiert wurde? Man könnte die entsprechenden Routen nachträglich wieder aus dem root-Router entfernen, aber wie man das intuitiv und sauber hin bekommt muss ich noch heraus finden.
Bottle: Micro Web Framework + Development Blog
Benutzeravatar
Defnull
User
Beiträge: 778
Registriert: Donnerstag 18. Juni 2009, 22:09
Wohnort: Göttingen
Kontaktdaten:

So, ich habe eine Weile über das Thema nach gedacht. Die aktuelle API (Dekoratoren mit globalen Nebeneffekten) ist gut für kleine Anwendungen und wir genau so bestehen bleiben.

Die Nutzung von globalen Modul-Variablen ist aber momentan einer der Haupt-Kritikpunkte, wenn es um nicht-ganz-so-kleine Projekte in Bottle geht. Früher oder später möchte ich daher eine zusätzliche Möglichkeit schaffen, Bottle Applikationen auch wiederverwendbar zu gestalten, so das ein modul-import keine Nebeneffekte hat und eine Applikation als in sich abgeschlossene Objektinstanz behandelt werden kann.

Dafür muss eine neue API her. Die bereits vorgeschlagene Stack-Variante hat ja anscheinend nicht so einen großen Anklang gefunden. Ich habe aus euren Vorschlägen und anderen Überlegungen insgesamt 4 unterschiedliche Varianten zusammen getragen. Jede hat vor und Nachteile. Ich will aber nur eine oder maximal zwei davon offiziell unterstützen, um die Dokumentation nicht zu sehr auf zu blähen. Daher ist eure Meinung gefragt:

Variante 1 ist jetzt schon möglich. Ich würde dann auch die get, post, put und delete Dekoratoren als Bottle-Methoden implementieren.

Code: Alles auswählen

myapp = Bottle(config={...})

@myapp.route('/')
def index():
    return 'Hello World!'

@myapp.route('/info')
def info():
    return "This is a small bottle app"
Variante 2 ist ebenfalls jetzt schon möglich.

Code: Alles auswählen

app.push(config={...})

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

@route('/info')
def info():
    return "This is a small bottle app"

myapp = app.pop()
Variante 3 ist eigentlich nur syntaktischer Zucker für die zweite Variante.

Code: Alles auswählen

myapp = Bottle(config={...})

with myapp:

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

    @route('/info')
    def info():
        return "This is a small bottle app"
Als letztes (Variante 4) noch der klassische Controller-Ansatz. Hinter den Kulissen werden dafür aber leider ein paar dreckige Hacks nötig werden (siehe http://www.python-forum.de/topic-22225.html ).

Code: Alles auswählen

class MyApp(BaseApp):

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

    @route('/info')
    def info(self):
        return "This is a small bottle app"

myapp = MyApp(config={...})
Was sieht am besten aus? Was ist am verständlichsten? Gibt es noch bessere Lösungen? Braucht man das überhaupt?
Bottle: Micro Web Framework + Development Blog
Benutzeravatar
noisefloor
User
Beiträge: 3856
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,

erstmal:
Das ist so und soll auch so bleiben, schließlich ist das eines der beliebtesten Eigenschaften von Bottle.
+1!

Zur letzten Frage: auch wenn ich es nicht wirklich brauche findet ich 1 oder 4 am "schönsten" bzw. am verständlichsten - oder meinetwegen IMHO auch am leichtesten zu Greifen für Newbies.

Gruß, noisefloor
Benutzeravatar
snafu
User
Beiträge: 6738
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Ich würde 1 + 4 rein nehmen. Ruhig auch die beiden zusammen, um dem Nutzer die Wahl beim Design seiner App zu lassen. Weiterhin sollten die ursprünglichen Dekoratoren global gelten. Das push-pop-Konzept finde ich ehrlich gesagt ziemlich umständlich und auch gar nicht so passend zu dem, was man von Bottle eigentlich gewohnt ist.
Benutzeravatar
jbs
User
Beiträge: 953
Registriert: Mittwoch 24. Juni 2009, 13:13
Wohnort: Postdam

Bin auch für 1 und 4.

Sehe das wie Snafu. Das mit dem `with` ist ja fast so wie das mit der Klasse, nur dass die Klasse sich IMHO authentischer anfühlt.
[url=http://wiki.python-forum.de/PEP%208%20%28%C3%9Cbersetzung%29]PEP 8[/url] - Quak!
[url=http://tutorial.pocoo.org/index.html]Tutorial in Deutsch[/url]
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

Ich denke 4 macht Sinn weil es recht einfach zu dokumentieren ist und man mit den üblichen Editoren/IDEs einfacher den Überblick bewahren kann. Variante 1 macht ja allein schon aus Gründen der Rückwärtskompatibilität Sinn.
ms4py
User
Beiträge: 1178
Registriert: Montag 19. Januar 2009, 09:37

1 + 4
„Lieber von den Richtigen kritisiert als von den Falschen gelobt werden.“
Gerhard Kocher

http://ms4py.org/
derdon
User
Beiträge: 1316
Registriert: Freitag 24. Oktober 2008, 14:32

Code: Alles auswählen

from operator import concat;chr(int(reduce(concat, map(str, (ord('\x04'),ord('\x03')))))).join(map(str, str(ord('\x0e'))))
uKev
User
Beiträge: 15
Registriert: Mittwoch 9. Dezember 2009, 13:43

Erstmal vielen Dank für deine ganze Mühe!

Ich stehe jetzt vor dem konkreten Problem, dass ich genau diese mount Funktion benötige.

Einmal hab ich meine eigene Hauptanwendung, die bottle mit einem Tornado Server auf Port 8080 startet (main.py) und dann habe ich eine Unabhängige Applikation in einem eigenen Unterordner (secureApp/sApp.py), die ich nach /secureApp/ mounten möchte.

Nun möchte ich allerdings die Unteranwendung nur per Passwort zugänglich machen, ohne sie anpassen zu müssen. Geht das? Das wäre nämlich echt genial.

Davon unabhängig, was mir noch nicht ganz klar ist:

Die einzuhängende App muss ich zuerst importieren, damit ich das Modul im Namensraum habe und an bottle.mount(app..) übergeben kann. richtig?

Damit ich das einzuhängende Modul korrekt importieren kann, darf es die bottle run Methode nur innerhalb eines

Code: Alles auswählen

if __name__ == '__main__': 
Konstrukts aufrufen, da es sonst selbst einen Port öffnet. Ist mein Gedankengang korrekt?

edit:
Jetzt hat sich beim ausprobieren noch folgendes Problem ergeben:
Mein Verzeichnisbaum ist ungefähr so gegliedert:
  • /main.py
    /secureApp/sApp.py
    /secureApp/lib/someLib.py
    /secureApp/conf/config.py
in secureApp/sApp.py importiere ich ein Modul lib/someLib.py, das selbst wieder das Modul conf/config.py importiert.
Wenn ich jetzt innerhalb von secureApp bin und sApp.py starte, funktioniert alles prima.
Wenn ich jetzt aber sApp in main.py importiere bekomme ich folgende Fehlermeldung:
ImportError: No module named conf
Das stimmt ja auch, der Namensraum hat sich geändert. Es müsste jetzt nicht mehr

Code: Alles auswählen

import config from conf
stehen sondern

Code: Alles auswählen

import config from secureApp.conf
Wie löse ich das Problem unabhängig vom aktuellen Arbeitsverzeichnis?
Wie handhabt ihr das mit Konfigurationsdateien?
Benutzeravatar
lynadge
User
Beiträge: 112
Registriert: Sonntag 4. April 2010, 10:17

Hallo.

Habe das Thema bei mir jetzt auch einmal aufgegriffen und zum laufen gebracht.

Bei mir sieht es so aus:

main.py

Code: Alles auswählen

#!/usr/bin/env python
# -*- encoding: utf8 -*-

import bottle
import app

@bottle.route('/')
def index():
    return 'page: main!'

def main():
    bottle.mount(app.myapp, '/app')
    bottle.debug(True)
    bottle.run(reloader=True)


if __name__ == '__main__': main()
app.py

Code: Alles auswählen

# -*- encoding: utf8 -*-

import bottle

myapp = bottle.Bottle()

@myapp.route('/')
def index():
    return 'page: app'

@myapp.route('/hello')
def index():
    return 'page: app.hello'
Mich wunderte nur eins, da stand ich dann ne weile wie vor einer wand, ich probierte die 'app' mit 'http://localhost:8080/app' aufzurufen. Dort kam aber immer ein 'Not Found', bis ich dann am Ende ein '/' an gehangen habe und es lief.

Ist das ein Bug oder ein Feature? ;)
Antworten