Verarbeiten einer Datei mit Python3 und Bottle

Django, Flask, Bottle, WSGI, CGI…
Mars_ars
User
Beiträge: 16
Registriert: Freitag 3. Februar 2017, 12:09

Vielen Dank euch beiden für die Antworten. Ja Sirius3, du hattest recht. Mit einem frischen Blick auf die Dinge habe ich es doch geschafft. Auch habe ich die Anmerkungen von BlackJack berücksichtigt, danke dafür nochmal.
"If ext not in" damit Überprüfe ich zumindest vom Dateinamen das Format der Datei und lasse nicht zu, dass außer fastq-Dateien Dateien hochgeladen werden. Stimmt das nicht?
Den selbstgebastelten new_key behalte ich fürs erste (kann man ja später noch rel. einfach abändern).
Ich habe es sogar schon geschafft eine beispielhafte Plotgrafik einzubinden. Somit stehen mir hoffentlich keine allzu großen Hürden im Weg. :D Ich werde mich nun der Ausarbeitung des eigentlichen Inhalts zuwenden.
Ich muss es einfach nochmal loswerden: Dieses Forum ist absolut spitze und die Helfer einfach fantastisch. Ich fand die schrittweise Heranführung an die Lösung sehr amüsant und vor allem äußerst hilfreich. An dieser Stelle ein großes Lob und Dankeschön für eure Geduld.

P.S.
Sirius3 hat geschrieben: Normalerweise hat man nur eine Route zu den statischen Dateien und nicht eine pro Dateityp.
Anders habe ich es leider nicht hinbekommen. Ich war schon glücklich, dass es so funktioniert hat ^^ .

Hier nochmal der verbesserte Code:

Code: Alles auswählen

@route('/upload', method='POST')
def do_upload():
    new_key=''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(8))
    upload = request.files.get('upload')
    name, ext = os.path.splitext(upload.filename)
    if ext not in ('.fastq'):
        return 'File extension not allowed.'

    save_path = '/tmp/fastq_prog'
    if not os.path.exists(save_path):
        os.makedirs(save_path)

    file_path = "{path}/{file}".format(path=save_path, file=new_key)
    upload.save(file_path, overwrite=True)

    return template('upload', new_key=new_key)

@route('/analyse', method='POST')
def analyse():
    key = request.forms.get('new_key')

    filename = open(os.path.join('/tmp/fastq_prog/',key), 'r')
    count = 0
    for rec in SeqIO.parse(filename, 'fastq'):
        count += 1
#plot 1
    plt.ioff() # turn of interactive plotting mode
    x = [1,2,3]
    y = [5,7,4]

    x2 = [1,2,3]
    y2 = [14,10,12]

    plt.plot(x,y, label='first Line')
    plt.plot(x2,y2, label='second Line')
    plt.xlabel('Plot Number')
    plt.ylabel('Important var')
    plt.legend()

    plt.title('Interesting Graph\nSubtitle')


    plt.savefig('static/sonstige-bilder/plot1.png', bbox_inches='tight')


    return template('result1', count=count)

Code: Alles auswählen

<form action="/analyse" method="post" enctype="multipart/form-data">
			<input type='hidden' name='new_key' value='{{new_key}}'/>
      <p><input value='Analyse' type="submit" name='analyse' /></p>
			</form>
Sirius3
User
Beiträge: 17738
Registriert: Sonntag 21. Oktober 2012, 17:20

@Mars_ars: Du hast das mit der Static Route nicht einfach hinbekommen und hast es deshalb kompliziert gemacht? Blind irgendwelche „Lösungen“ von StackOverflow abzuschreiben ist so, wie wenn man Geschäftsbriefe von Google-Translate übersetzen läßt. »ext not in ".fastq"« läßt Dateien ohne Endung und mit den Endungen ., .f, .fa, .fas, .fast, und .fastq zu. Wenn ein Fehler auftritt, solltest Du »abort« benutzen und nicht einfach nur einen String zurückschicken. Auch in upload solltest Du os.path.join benutzen. Konstanten, wie hier '/tmp/fastq_prog' sollten nicht mehrmals im Programm auftauchen. Dateiobjekte sollten nicht filename heißen und auch wieder geschlossen werden, am einfachsten mit Hilfe des with-Statements. Nutzereingaben sollte man nicht blind übernehmen, vor allem nicht, wenn man damit Systemfunktionen aufruft. Dynamische Daten sollten nicht nach /static geschrieben werden.

Code: Alles auswählen

import os
import uuid
from bottle import route, template, static_file, request

STATIC_ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static')
UPLOAD_PATH = '/tmp/fastq_prog'

@route('/')
def index():
    return template('main_template')

@route('/static/<filepath:path>')
def server_static(filepath):
    return static_file(filepath, root=STATIC_ROOT)

@route('/analyse/<filepath:path>')
def server_analyse(filepath):
    return static_file(filepath, root=UPLOAD_PATH)

@route('/upload', method='POST')
def do_upload():
    new_key = str(uuid.uuid1())
    upload = request.files.get('upload')
    name, ext = os.path.splitext(upload.filename)
    if ext != '.fastq':
        abort(400, 'File extension not allowed.')

    if not os.path.exists(UPLOAD_PATH):
        os.makedirs(UPLOAD_PATH)

    filename = os.path.join(UPLOAD_PATH, new_key + '.fastq')
    upload.save(file_path, overwrite=True)
    return template('upload', new_key=new_key)

@route('/analyse', method='POST')
def analyse():
    key = request.forms.get('new_key')
    try:
        # check for valid ID
        uuid.UUID(key)
    except ValueError:
        abort(400, 'Invalid key.')
    filename = os.path.join(UPLOAD_PATH, key) + '.fastq'
    with open(filename):
        count = 0
        for rec in SeqIO.parse(filename, 'fastq'):
            count += 1

    os.mkdir(os.path.join(UPLOAD_PATH, key))

    x = [1,2,3]
    y = [5,7,4]
    x2 = [1,2,3]
    y2 = [14,10,12]
    plt.plot(x,y, label='first Line')
    plt.plot(x2,y2, label='second Line')
    plt.xlabel('Plot Number')
    plt.ylabel('Important var')
    plt.legend()
    plt.title('Interesting Graph\nSubtitle')

    imagefilename = os.path.join(UPLOAD_PATH, key, 'plot1.png')
    plt.savefig(imagefilename, bbox_inches='tight')
    return template('result', count=count, image='/analyse/{}/plot1.png'.format(key))
Mars_ars
User
Beiträge: 16
Registriert: Freitag 3. Februar 2017, 12:09

@Sirius3 Vielen herzlichen Dank! Das hatte ich nun wirklich nicht erwartet. :D Es ist fantastisch von einem Profi zu lernen. Ich werde versuchen deine Kritikpunkte auch weiterhin zu beachten.
Herzliche Grüße
Mars_ars
Mars_ars
User
Beiträge: 16
Registriert: Freitag 3. Februar 2017, 12:09

@Sirius3 ich habe leider damit ein Problem aus deinem Code:

Code: Alles auswählen

try:
# check for valid ID
	uuid.UUID(key)
except ValueError:
	abort(400, 'Invalid key.')
Einerseits kommt ein ValueError, weil 'abort' nicht definiert ist (Das allererste abort habe ich durch return ersetzt und ein Fehlertemplate erstellt.). Andererseits, wenn ich 'abort' durch 'return 'Invalid key' ersetze, bekomme ich immer diesen Fehler. Anscheinend klappt die Überprüfung nicht. Ich konnte auch nichts passendes dazu finden außer https://docs.python.org/3.5/library/uuid.html. Hast du einen Tipp für mich?
Gruß
Mars_ars
Zuletzt geändert von Anonymous am Dienstag 7. Februar 2017, 16:57, insgesamt 1-mal geändert.
Grund: Quelltext in Python-Codebox-Tags gesetzt.
Sirius3
User
Beiträge: 17738
Registriert: Sonntag 21. Oktober 2012, 17:20

@Mars_ars: beim Aufräumen der Importe ist das abort verloren gegangen:

Code: Alles auswählen

from bottle import route, template, static_file, request, abort
BlackJack

@Mars_ars: Wenn der `ValueError` immer kommt, dann geht irgendwo auf dem Weg der Schlüssel kaputt, denn das sollte funktionieren:

Code: Alles auswählen

In [7]: u = uuid.uuid1()

In [8]: str(u)
Out[8]: '9477da1c-ed4f-11e6-a1cf-901b0e8d499b'

In [9]: uuid.UUID(str(u))
Out[9]: UUID('9477da1c-ed4f-11e6-a1cf-901b0e8d499b')

In [10]: uuid.UUID(str(u) + 'z')
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-10-ec471a35591a> in <module>()
----> 1 uuid.UUID(str(u) + 'z')

/usr/lib/python2.7/uuid.pyc in __init__(self, hex, bytes, bytes_le, fields, int, version)
    134             hex = hex.strip('{}').replace('-', '')
    135             if len(hex) != 32:
--> 136                 raise ValueError('badly formed hexadecimal UUID string')
    137             int = long(hex, 16)
    138         if bytes_le is not None:

ValueError: badly formed hexadecimal UUID string
Mars_ars
User
Beiträge: 16
Registriert: Freitag 3. Februar 2017, 12:09

Herzlichen Dank, beides hat weitergeholfen :D
Mars_ars
User
Beiträge: 16
Registriert: Freitag 3. Februar 2017, 12:09

Hallo liebe Community,
ich bin nun mit dem Projekt größtenteils bzw. ganz fertig und habe heute an den Rechnern der Uni einen merkwürdigen bug entdeckt, der bei mir bisher nicht vorkam:
der User lädt eine Datei hoch, kann diese Bearbeiten und am Ende des Prozesses bekommt er eine graphische Auswertung der Datei (matplotlib). wenn der Benutzer nun wieder zur Startseite geht und erneut eine Datei hochlädt, werden die Graphen stark zerschoßen angezeigt (z.b. werden jeweils die Werte und die Legenden miteinander verwurschtelt bzw. werden doppelt so lang). Ich habe festgestellt, dass es mit dem Serverneustart zutun hat. Damit alles richtig angezeigt wird, muss der Server mit strg+c neugestartet werden. Irgendwie macht das doch keinen Sinn in der Praxis, da man ja einen Server nicht nach jeder Benutzereingabe neu startet/ starten kann.

Code: Alles auswählen

run(reloader=True)
ist im code drin. was könnte schief gehen?
BlackJack

@Mars_ars: Du operierst da mit *einem* Plot. Du musst für jede Anfrage einen neuen erstellen und nicht auf dem einen globalen operieren.
Mars_ars
User
Beiträge: 16
Registriert: Freitag 3. Februar 2017, 12:09

Das verstehe ich leider nicht ganz. Der Plot/ die Plots sind doch in einer eigenen Funktion, müssen doch jedesmal die Werte aus der neue hochgeladenen Datei verwenden. Wird etwa nicht für jede Anfrage ein eigener Plot generiert?
BlackJack

@Mars_ars: Nein es wird nicht für jede Anfrage ein neuer Plot generiert. Wo sollte das denn aus welchem Grund auf magische Weise passieren? Der Plot ist kein lokaler Wert in der Funktion. Du rufst da ausschliesslich Funktionen auf denen Du nie irgendwie angibst *auf welchen Plot* die sich eigentlich beziehen. Das ist halt der eine globale den `matplotlib` für diese Funktionen erstellt, die für die interaktive Nutzung gedacht, und da auch praktisch sind.
Sirius3
User
Beiträge: 17738
Registriert: Sonntag 21. Oktober 2012, 17:20

@Mars_ars: Du rufst plt.plot auf, das in ein globales Diagramm etwas einzeichnet. Und was da gezeichnet wurde, wird über mehrere Anfragen hinweg auch gespeichert. Was Du brauchst, ist für jede Anfrage eine eigene Figure, in die Du dann z.B. per fig.plot etwas zeichnen kannst.
Mars_ars
User
Beiträge: 16
Registriert: Freitag 3. Februar 2017, 12:09

ich hatte eigentlich schon davor jedem plot eine Nr. zugewiesen: z.b.

Code: Alles auswählen

plt.figure(1)
nun habe ich

Code: Alles auswählen

fig1=plt.figure()
fig1.plot(x_range, n, label='N %')
und so weiter... quasi überall plt durch fig1 ersetzt :D
doch hat ein Figure-object kein attribut plot.

in dem Tutorial zu matplotlib verwendeten sie einfach nur zahlen. ich hätte nicht erwartet, dass alles so kompliziert ist^^ (http://matplotlib.org/users/pyplot_tutorial.html)

Soll ich vielleicht die Plots nach plt.safig() mit clear() bereinigen?
BlackJack

@Mars_ars: Dann kümmert sich ein globaler „FigureManager“ um die Plots, das heisst die werden wohl nie wieder aus dem Speicher entfernt. Ungünstig für einen Server. Ich würde `pyplot` ganz weg lassen. Das ist ja eigentlich nur dazu da um interaktiv so was ähnliches wie Matlab zu haben. Wo der Benutzer die Funktionen direkt aufruft, oder für kleine Skripte die einfach nur mal einen Plot erstellen und danach beendet sind.

Wenn man etwas lang laufendes hat, sollte man IMHO die normale `matlotlib`-API verwenden. Also sich beispielsweise den `FigureCanvasAgg` aus dem Agg-Backend-Modul holen, damit ein `Figure`-Objekt erstellen, darauf dann einen Subplot, und auf dem kann man dann fröhlich plotten. Die Objekte werden bei jeder Anfrage neu erstellt, und wenn die Funktion wo die an lokale Namen gebunden sind, abgearbeitet ist, dann können die Objekte von Python auch wieder abgeräumt werden.
Mars_ars
User
Beiträge: 16
Registriert: Freitag 3. Februar 2017, 12:09

Vielen Herzlichen Dank euch beiden. Diesmal insbesondere BlackJack für die sehr ausführliche Antwort.
Da am Freitag schon die Abgabe ist und ich eh sehr viel neues Wissen angeeignet habe (dank euch), ist es nicht zielführend jetzt auf die Schnelle zu versuchen sich krampfartig in das Agg_Backend Modul einzuarbeiten (habe es mir kurz angeschaut und es sieht recht komplex aus). Ich habe das Problem folgendermaßen "lösen" können:

Code: Alles auswählen

plt.figure(1)
        plt.clf()
        und dann den eigentlichen plot
Aus eurer Antwort weiß ich nun, was ich beachten muss, wenn ich mal tatsächlich so etwas auf die Beine stelle. Anscheinend sind solche tiefen Kenntnisse, wie ihr sie versucht zu vermitteln, in dem Einführungsmodul Bioinformatik leider nicht erforderlich. (Die sind schon froh, wenn jeder Depp dem Stoff folgen kann. Soviel zum Pädagogischen^^) Habe mich aber riesig über eure Hilfe zu bottle gefreut, da ich alleine wohl damit gescheitert wäre. Damit habe ich die Mindestanforderungen, die an uns gestellt wurden, fröhlich hinter mir gelassen. Zitat vom Professor: "Mensch, die anderen werden weinen, wenn du denen das vorstellst." :D Ein wunderhübsches und elegantes (selbsterstelltes) Design der Templates war sehr förderlich. :mrgreen:

Liebe Grüße
Mars_ars
BlackJack

@Mars_ars: Das Backend-Modul ist im Grunde egal, Du brauchst da nur die eine Klasse draus. Das `Figure`-Objekt braucht halt irgendetwas wo es drauf zeichnen kann.

Was bei Deiner Lösung noch zu beachten ist wäre dass das nicht „thread safe“ ist. Das können so nicht mehrere Leute gleichzeitig nutzen, die malen dann alle auf dem selben Canvas.
Antworten