Microwebframeworks

Sockets, TCP/IP, (XML-)RPC und ähnliche Themen gehören in dieses Forum
Antworten
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

Simon Willison erwähnt in [Django Heresies](http://www.slideshare.net/simon/django-heresies) 5 Mikro-Web-Rahmenwerke [juno][], [newf][], [mnml][], [itty][] und [djng][], die ich mir im folgenden genauer anschauen möchte. Wer mag, ergänze doch bitte weitere Rahmenwerke.

[juno]: http://github.com/breily/juno
[newf]: http://github.com/JaredKuolt/newf
[mnml]: http://github.com/garethr/mnml
[itty]: http://github.com/toastdriven/itty
[djng]: http://github.com/simonw/djng

Beachtenswert finde ich, dass *github* allgegenwärtig ist. Mir gefällt's.

Außerdem ist es wohl ein ungeschriebenes Gesetz, das der Name 4 Buchstaben haben muss. Das ist dann auch der Grund, warum ich "web.py" oder "Werkzeug" nicht erwähne. Die Anzahl der Buchstaben passt nicht ;) Vielleicht, wenn es "wkzg" hieße...

[Und wieder einmal kommt das Forum nicht mit meinem Artikel klar, obwohl ich den Codeanteil schon sehr reduziert habe. Wer den Rest lesen will, findet ihn hier: http://github.com/sma/microwebframeworks/tree/master ]

Stefan
fred.reichbier
User
Beiträge: 155
Registriert: Freitag 29. Dezember 2006, 18:27

Interessant, danke dafür!
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

fred.reichbier hat geschrieben:Interessant, danke dafür!
Danke :)
Y0Gi
User
Beiträge: 1454
Registriert: Freitag 22. September 2006, 23:05
Wohnort: ja

Simons Erwährungen in Slides, bei Twitter und in seinem Blog dazu habe ich ebenfalls verfolgt. Auch wenn mir gerade missfällt, dass Ian Bicking alles basht, was nicht WebOb verwendet und offenbar versucht, SQLAlchemy mit aller Gewalt gegenüber SQLObject als schlecht darzustellen, hat er doch insofern Recht, dass diese aus einer Datei bestehenden Frameworks einen großen Fehler machen, wenn sie ihre eigenen bescheidenen Request-/Response-Objekte zusammen bauen. Da gibt es sehr viel Zeug, was beachtet werden muss, damit auch Dinge wie Datei-Uploads oder HTTP-Eigenschaften richtig und Useragent-konform umgesetzt werden, und selbst das findet in entsprechenden Implementierungen aus gutem Grund nicht in einem einzelnen Modul Platz (ok, technisch wäre das möglich, aber nicht wirklich sinnvoll).

djng ist übrigens kein solches Microframework, sondern nur ein Django-Wrapper, der nach außen wie eins aussieht. Mir gefällt allerdings dieser Ansatz als Grundlage um bestimmte gute Konzeptänderungen (wie: ein View [sowie andere Elemente] ist eine Callable, die einen Request erhält und eine Response liefert) an Django zu testen und einzuführen.

Hier noch 'n Link dazu: http://simonwillison.net/2009/May/19/djng/
Benutzeravatar
Defnull
User
Beiträge: 778
Registriert: Donnerstag 18. Juni 2009, 22:09
Wohnort: Göttingen
Kontaktdaten:

An itty arbeite ich gerade. Ich versuche, das ganze noch etwas eleganter hin zu bekommen, ohne die API groß zu verändern.

Wichtigster Punkt bisher: POST und GET Parameter werden nun erst dann geparsed, wenn man sie auch braucht. Das spart Speicher und CPU, wenn man sie nicht benutzt.

Das hab ich noch vor:
  • Zusätzliche Unterstützung von schnelleren String-Mappings ohne RegExp für statische URLs wie '/' oder '/index'
  • Selbstlernende Sortierung der Mappings: Häufig benutzte Mappings sollten in der Liste weiter oben stehen, damit sie früher gefunden werden.
  • Das setzen von content_type, status_code und headers sollte auch über das Request-Objekt möglich sein. Momentan ist das über das Anpassen der Funktions-Attribute zwar möglich, aber nicht Thread-Save)
  • Die Dekoratoren sollen deutlich mächtiger werden. Außerdem möchte ich noch ein paar dazu bauen, die ich aus den großen Frameworks wirklich vermisse. Die wichtigsten wären wohl @jasonify und @render, wobei letzterer ein wrapper für verschiedene Template-Engines werden soll.
Mal sehen, was mir noch ein fällt :) Ich spekuliere eigentlich nicht auf einen Merge zurück nach itty, dafür verändere ich jetzt schon zu viel. Trotzdem ist itty ne sehr schöne Basis.
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

Klingt spannend. Bei Mikrooptimierungen wie einer LRU-Sortierung bin ich zwar skeptisch, aber diese komische Idee, Dinge im Funktionsobjekt zu speichern aus zu bauen, finde ich ist eine gute Idee.

Bei Templates bin ich mir nicht sicher, was besser ist:

Code: Alles auswählen

@render
@get('/')
def index(request):
    return { 'posts': Post.objects.all() }

vs.

@get('/')
def index(request):
    render('index', posts=Post.objects.all())
Was ich noch ganz praktisch finde ist, wenn man gemeinsamen Code wie das Laden einen bestimmten Objekts aus verschiedenen Funktionen ziehen kann. Bei Sinatra geht's weil nicht nur die erste sondern alle passenden Handler aufgerufen werden und man sich den Status in ad-hoc Exemplarvariablen merkt. Man könnte hier (ich glaube, web.py macht es so) ein thread-lokales Objekt benutzen. Daraus könnte sich dann auch das Template bedienen. Oder man speicherts (so macht es z.B. JSP) im Request-Objekt...

Code: Alles auswählen

@get('/post/:id', '/post/:id/*')
def before_post(request, id):
    data.post = Post.objects.get(id=id) or not_found

@get('/post/:id')
def view_post(request):
    render('view_post')

@get('/post/:id/edit')
def edit_post(request):
    data.post_types = PostType.objects.all()
    render('edit_post')
Stefan
Benutzeravatar
Defnull
User
Beiträge: 778
Registriert: Donnerstag 18. Juni 2009, 22:09
Wohnort: Göttingen
Kontaktdaten:

sma hat geschrieben:Bei Mikrooptimierungen wie einer LRU-Sortierung bin ich skeptisch
Das entspricht auch eher meinen persönlichen Anforderungen. Ich rechne mit über 60 Routen, von denen 90% nur selten benutzt werden (unwichtigere Ajax APIs) aber manche sehr häufig angefragt werden ('/'). Ich denke schon, das die richtige Sortierung etwas bringen kann. Der Algorithmus, der mir für die Umsortierung vor schwebt, ist übrigens recht simpel: Zufällig (Alle X Requests) wird der aktuelle Eintrag mit seinem Vorgänger getauscht. Statistisch gesehen wandern die häufigen Routen damit weiter nach vorne.
sma hat geschrieben: Bei Templates bin ich mir nicht sicher, was besser ist:
Wenn man drüber nach denkt, hast du eigentlich recht.
Solche Dekoratoren können sehr einfach vom Anwender nachgerüstet werden, sind aber recht schwer allgemein und flexibel genug zu gestalten, um sie ins Framework zu packen. Da macht es mehr Sinn, die Dekoratoren als Beispiele in die Dokumentation zu packen. Konsequent weiter gedacht müsste man die Adapter für flup, Pylons, CherryPy & co dann aber auch in die Dokumentation verbannen.
sma hat geschrieben: Was ich noch ganz praktisch finde ist, wenn man gemeinsamen Code wie das Laden einen bestimmten Objekts aus verschiedenen Funktionen ziehen kann.
Ich würde das entweder über Dekoratoren regeln, die Daten im Request-Objekt ablegen oder neue Parameter hinzu fügen, oder explizit im Funktionsrumpf durch das aufrufen von Helfer-Funktionen erreichen. Das ist übersichtlicher und vor allem unabhängig von der Reihenfolge der Routen.

Code: Alles auswählen

@get('/post/:id')
def show(id):
  post = getpost(id)
  ...

def getpost(id):
  ...

sma hat geschrieben: Man könnte hier (ich glaube, web.py macht es so) ein thread-lokales Objekt benutzen. Daraus könnte sich dann auch das Template bedienen. Oder man speicherts (so macht es z.B. JSP) im Request-Objekt...
Itty ist (nach meiner Änderung) komplett Thread-Save, da alle relevanten Variablen im Request-Objekt gehalten werden, das mit jedem Aufruf neu erschaffen wird. Alles, was über einen Seitenaufruf hinweg existieren muss (Sessions, Object Caches) sollte als Modulvariable (Nicht Thread-Save) oder mit Hilfe von threading.local() (Thread-Save) gespeichert werden. Beides ist sehr einfach außerhalb des Frameworks erreichbar und muss daher nicht ins Framework eingebaut werden, finde ich.
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

Defnull hat geschrieben:Ich rechne mit über 60 Routen, von denen 90% nur selten benutzt werden (unwichtigere Ajax APIs) aber manche sehr häufig angefragt werden ('/').
Und alle beginnen bei '/'? Kann man nicht etwa das API unter '/api' ablegen und so die Menge der URLs vorfiltern?

Statt zufällig (wahrscheinlich dauert das Berechnen der Zufallszahl länger als die eigentliche Suche) zu sortieren, mache es doch so:

Code: Alles auswählen

arr.insert(0, arr.pop(5))
Ob man explizite Hilfsfunktionen für "crosscutting concerns" einer klassischen AOP-Lösung vorzieht ist vielleicht Geschmackssache. Ich finde es allerdings irgendwie falsch, alle Informationen im Request-Objekt zu merken. Sollte man dann nicht lieber eine Art "Kontext" übergeben, wo Request und Response zwei Eigenschaften von sind?

Und noch was zur Suche. Wenn du maximal 99 Routen hast, würde ich sie alle zu einem regulären Ausdruck zusammenfassen. Ein kurzer Benchmark, der in einer Schleife 99 URLs /100 bis /198 durchgeht, und nach /194 sucht, braucht 0.6 sek. In einem Ausdruck (/100)|(/101)|... finde ich /194 in 0.1 sek. Ohne reguläre Ausdrücke finde ich das ganze übrigens in 0.06 sek. Gemessen habe ich jeweils 10.000 Versuche.

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

sma hat geschrieben:
Defnull hat geschrieben:Ich rechne mit über 60 Routen, von denen 90% nur selten benutzt werden (unwichtigere Ajax APIs) aber manche sehr häufig angefragt werden ('/').
Und alle beginnen bei '/'? Kann man nicht etwa das API unter '/api' ablegen und so die Menge der URLs vorfiltern?
Sind sie, aber "vorfiltern' ist mit itty noch nicht moeglich, da alle Routen gleich berechtigt sind. Es gibt keine Route-Gruppen, die einen gemeinsamen Pfad haben. Das will ich auch nicht einbauen, das das die Sache unnoetig kompliziert macht.
sma hat geschrieben:Statt zufällig (wahrscheinlich dauert das Berechnen der Zufallszahl länger als die eigentliche Suche) zu sortieren, mache es doch so:

Code: Alles auswählen

arr.insert(0, arr.pop(5))
Noch einfacher. Wie schon geschrieben: "Zufällig (Alle X Requests) wird der aktuelle Eintrag mit seinem Vorgänger getauscht."

Bei X=1000 brauche ich alle 1000 Anfragen nur eine Swap-Operation auf das REQUEST_MAPPING Array, bei der die aktuelle Route mit ihrem Vorgaenger im Array getauscht wird. So wandern haeufig benutzte Routen langsam immer weiter nach oben.
sma hat geschrieben: Ich finde es allerdings irgendwie falsch, alle Informationen im Request-Objekt zu merken. Sollte man dann nicht lieber eine Art "Kontext" übergeben, wo Request und Response zwei Eigenschaften von sind?
Falsch ist momentan nur der Name. Vorher war das Request-Objekt ja wirklich nur ein netter Wrapper fuer die environ-Variablen. Inzwischen ist es zur (einzig notwendigen) Schnittstelle zwischen Handler und App-Server geworden. Das es Informationen ueber die Anfrage und die Antwort mischt, ist durchaus beabsichtigt. Betrachte es als Transaktion. Alles, was mit dem aktuellen Seitenaufruf zu tun hat, soll frueher oder spaeter ueber das Transaktions-Objekt laufen. Ich plane auch eine request.abort(code, message) Methode, damit der Entwickler nicht mehr die itty-Exceptions benutzen muss. Dann wird der Ansatz auch wieder konsistent.
sma hat geschrieben: Und noch was zur Suche. Wenn du maximal 99 Routen hast, würde ich sie alle zu einem regulären Ausdruck zusammenfassen.
Dann muesste ich mein eigenes second level routing innerhalb eines request handlers am Framework vorbei implementieren. Klar, das waere schneller, wuerde aber das Routing des Frameworks komplett untergraben und haette ziemliche Probleme bei komplexen Routen wie '/api/:session/:resource/:action/prepare'. Die richtige Sortierung der Routen macht da denke ich mehr Sinn. Seltene Routen duerfen auch gerne ein paar ms laenger brauchen.
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 one-file-micro-web-framework präsentieren :D

Bottle

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

Es ist mit 600 Zeilen etwas größer, als die meisten von sma vorgestellten Frameworks, aber dafür auch etwas mächtiger (und nützlicher). Unter Anderem gibt es einige Performance-Optimierungen unter der Haube (selbstoptimierende Routing Tabellen, schnelle statische Routen, on-demand-parsing von QUERY_STRING und POST-data), einen einfachen Routing Syntax ("/hello/:name") und vollwertiges Cookie Management. Alles Dinge, die in fast keinem anderen micro-framework zu finden waren.

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. Über Feedback (und Forks) würde ich mich freuen ;) Das ganze steht unter einer MIT Lizenz und ist demnach ziemlich offen.

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

@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/')

run(host='localhost', port=8080)
Antworten