Flask
Luentovideo pohjautuu vanhaan luentomateriaaliin.
Flask on mikrosovelluskehys, jolla tehdään web-sovelluksia. Se rakentuu kahden kirjaston päälle: Werkzeug hoitaa HTTP-tason ja Jinja templatet.
Flask-sovellus on
WSGI-sovellus.
Sen voi ajaa WSGI-palvelimella (PythonAnywhere, Cloud Run) tai CGI-ohjelmana
users.jyu.fi-palvelimella. Sovelluskoodi on kummassakin tapauksessa
sama; vain käynnistystapa vaihtuu.
Tässä materiaalissa käydään pääpiirteissään läpi Flaskin
Quickstart-tutoriaalin
sisältö. Asennus omalle koneelle, PythonAnywhereen ja
users.jyu.fi-palvelimelle käsitellään
pääteohjauksessa 1.
Minimaalinen sovellus
# importoidaan Flask-luokka ja luodaan siitä samantien esiintymä
from flask import Flask
app = Flask(__name__)
# kerrotaan Flaskille, mitä osoitetta seuraava funktio vastaa
@app.route("/")
def hello_world():
return "Hello World!"
Tiedoston nimi on tavallisesti app.py. Jos käytät muuta nimeä,
kerro se --app-valitsimella tai
FLASK_APP-ympäristömuuttujassa.
Kehityspalvelin ja debug-tila
Flaskin mukana tulee pieni www-palvelin kehityskäyttöön:
flask run --debug
# jos sovellustiedosto on muun niminen kuin app.py
flask --app oma run --debug
Debug-tila tekee kaksi asiaa: sovellus käynnistyy uudelleen aina koodin
muuttuessa, ja virheet näytetään selaimessa pinolistauksena. Sovellus avataan
osoitteesta http://127.0.0.1:5000/.
Kehityspalvelin ja debug-tila ovat vain omaa konetta varten.
Debuggerissa voi ajaa mielivaltaista Python-koodia selaimen kautta, joten
julkisella palvelimella se on suoraan murtautumisreitti. Älä myöskään käytä
app.run()-kutsua tuotannossa: siellä sovelluksen käynnistää
WSGI-palvelin tai CGI-käärekoodi.
Reititys ja osoitteet
Flask käyttää
dekoraattoria
kertomaan, mikä osoite vastaa mitäkin funktiota. Dekoraattori on
app.route.
@app.route("/hello")
def hello():
return "Hello World"
Osoitteen lopun kauttaviivalla on merkitystä:
# tätä voi kutsua osoitteilla /projects/ ja /projects
# jälkimmäinen ohjautuu automaattisesti ensimmäiseen
@app.route("/projects/")
def projects():
return "The project page"
# tätä voi kutsua vain muodossa /about
# osoite /about/ palauttaa 404
@app.route("/about")
def about():
return "The about page"
Muuttujat osoitteessa
Osoitteessa voi olla muuttujia, jotka annetaan funktiolle parametrina:
@app.route("/kayttaja/<tunnus>")
def nayta_kayttaja(tunnus):
return f"Käyttäjä {tunnus}"
Muuttujalle voi antaa muuntimen, joka samalla tarkistaa arvon kelpoisuuden. Kelpaamaton arvo tuottaa 404-vastauksen, joten tarkistusta ei tarvitse tehdä itse:
| Muunnin | Kelpuuttaa |
|---|---|
string |
merkkijonot ilman /-merkkiä (oletus) |
int | kokonaisluvut |
float | liukuluvut |
path |
polku, joka kelpuuttaa myös /-merkit |
uuid | UUID-tunnisteet |
@app.route("/viesti/<int:viesti_id>")
def nayta_viesti(viesti_id):
# viesti_id on nyt kokonaisluku, ei merkkijono
return f"Viesti numero {viesti_id}"
Samaan funktioon voi osoittaa useita reittejä:
@app.route("/hello")
@app.route("/hello/<nimi>")
def hello(nimi=None):
if nimi:
return f"Hello {nimi}"
return "Hello World"
Sallitut HTTP-metodit
methods-parametri kertoo, mitkä HTTP-metodit funktio hyväksyy.
Oletuksena sallittu on vain GET (ja sen mukana HEAD).
Muilla metodeilla Flask vastaa itse koodilla
405 Method Not Allowed.
@app.route("/kirjaudu", methods=["GET", "POST"])
def kirjaudu():
if request.method == "POST":
return kasittele_kirjautuminen()
return nayta_lomake()
Saman osoitteen GET- ja POST-käsittelyn voi myös eriyttää omiksi
funktioikseen lyhennemuotoisilla
app.get- ja
app.post-dekoraattoreilla,
jolloin kummastakin tulee lyhyempi:
@app.get("/kirjaudu")
def kirjautumislomake():
return render_template("kirjaudu.html")
@app.post("/kirjaudu")
def kirjaudu():
...
url_for
Osoitteita ei kirjoiteta käsin vaan muodostetaan
url_for-funktiolla. Sen ensimmäinen parametri on
funktion nimi, ei osoite. Ylimääräiset parametrit menevät joko
osoitteen muuttujiin tai querystringiin, ja funktio hoitaa samalla
prosenttikoodauksen.
from flask import url_for
with app.test_request_context():
print(url_for("projects")) # /projects/
print(url_for("about")) # /about
print(url_for("nayta_viesti", viesti_id=42)) # /viesti/42
print(url_for("hae", sana="Ällö kurssi")) # /hae?sana=%C3%84ll%C3%B6+kurssi
Esimerkissä käytetty
test_request_context
teeskentelee selainpyyntöä, jotta url_for-kutsu toimii myös
komentoriviltä ajettuna.
Kovakoodattu osoite rikkoutuu heti, kun reittiä muutetaan, ja se on väärin myös
silloin kun sovellus ei ole palvelimen juuressa — kuten
users.jyu.fi-palvelimella, jossa sovellus on
flask.cgi-tiedoston alla. url_for ottaa tämän
automaattisesti huomioon.
Staattiset tiedostot
Tyylitiedostot, skriptit ja kuvat sijoitetaan sovelluksen viereen
static-kansioon. Niihin viitataan url_for-funktiolla,
jolloin osoite on oikea myös silloin kun sovellus ei ole palvelimen juuressa:
<link rel="stylesheet" href="{{ url_for('static', filename='tyyli.css') }}" />
<img src="{{ url_for('static', filename='kuvat/logo.svg') }}" alt="Sovelluksen logo" />
CGI-ympäristössä jokainen staattinen tiedosto käynnistäisi
static-kansion kautta haettuna koko Python-tulkin uudelleen. Se on
hidasta ja tarpeetonta. Sijoita CSS, kuvat ja muut staattiset tiedostot
mieluummin tavalliseen www-kansioosi ja viittaa niihin suoraan, jolloin Apache
tarjoilee ne itse.
request
request-objekti sisältää selaimen lähettämät tiedot ja muut
HTTP-pyyntöön liittyvät tiedot. Se on voimassa vain pyynnön käsittelyn ajan.
from flask import request
| Ominaisuus | Sisältö |
|---|---|
request.method |
Käytetty HTTP-metodi merkkijonona |
request.args |
Osoitteen querystring-parametrit. Nämä ovat käytettävissä myös POST-pyynnössä. |
request.form |
Lomakkeen rungossa tulleet kentät (POST ja PUT) |
request.values |
args ja form yhdistettynä |
request.files |
Lähetetyt tiedostot |
request.json /
request.get_json() |
JSON-runko valmiiksi jäsennettynä |
request.headers |
Pyynnön otsakkeet. Haku ei ole kirjainkokoherkkä. |
request.cookies |
Evästeet sanakirjana |
request.path |
Polku ilman querystringiä |
request.full_path |
Polku querystring mukaan lukien |
request.url |
Koko osoite |
request.query_string |
Querystring sellaisenaan, tavujonona |
request.remote_addr |
Yhteyden lähdeosoite |
request.environ |
Koko WSGI-ympäristö, vrt. CGI-ympäristömuuttujat |
request.remote_addr on sen koneen osoite, joka avasi yhteyden.
Cloud Runissa ja muiden kuormantasaajien takana se on kuormantasaajan osoite,
ei käyttäjän. Käyttäjän osoite on silloin
X-Forwarded-For-otsakkeessa, ja se saadaan luotettavasti käyttöön
Werkzeugin ProxyFix-välikerroksella. Älä tee pääsynvalvontaa
IP-osoitteen varaan.
Arvoja luetaan get- ja getlist-metodeilla. Ne
toimivat samoin sekä args- että form-rakenteessa:
# toisena parametrina oletusarvo, jota käytetään jos kenttää ei ole annettu
nimi = request.form.get("nimi", "")
# type-parametri muuntaa arvon; jos muunnos epäonnistuu, palautetaan oletusarvo
lkm = request.form.get("lkm", 1, type=int)
# getlist palauttaa aina listan; jos arvoja ei ole, lista on tyhjä
arvosanat = request.form.getlist("arvosana")
Hakasulkeilla lukeminen (request.form["nimi"]) on myös
mahdollista, mutta silloin puuttuva kenttä nostaa poikkeuksen. Flask muuntaa sen
koodiksi 400 Bad Request. Useimmiten get oletusarvon
kanssa on selkeämpi, koska puuttuva kenttä on tavallinen tilanne eikä
poikkeus.
response
Flask muodostaa vastauksen automaattisesti sen perusteella, mitä näkymäfunktio palauttaa:
- merkkijono — sisältö, statuskoodi 200 ja mediatyyppi
text/html - pari
(sisältö, statuskoodi)tai kolmikko(sisältö, statuskoodi, otsakkeet) Response-objekti, kun vastausta pitää muokata tarkemmin. Sellaisen saa valmiista paluuarvostamake_response-funktiolla.- dict tai lista — muunnetaan JSON-vastaukseksi
from flask import Response, make_response, render_template
@app.route("/")
def index():
vastaus = make_response(render_template("index.html"), 200)
vastaus.headers["Content-Type"] = "application/xhtml+xml; charset=UTF-8"
vastaus.headers["Cache-Control"] = "no-store"
vastaus.headers["X-Oma-Otsake"] = "Testi"
return vastaus
@app.route("/teksti")
def teksti():
return Response("Pelkkää tekstiä", content_type="text/plain; charset=UTF-8")
Vanhoissa esimerkeissä näkee rivin vastaus.charset = "UTF-8".
Se ei toimi. charset-attribuutti poistettiin
Werkzeug 3.0:sta, ja sitä ennenkin sen asettaminen jälkikäteen jätti
Content-Type-otsakkeen ennalleen. Aseta merkistö osana koko
tyyppiä: content_type="text/plain; charset=UTF-8" tai suoraan
headers["Content-Type"]-otsakkeeseen.
Huomaa myös ero: mimetype-parametrille annetaan paljas tyyppi
("text/plain") ja content_type-parametrille koko arvo
parametreineen.
Kurssin tehtävissä dokumentit tarjoillaan XHTML:nä. Koska
render_template palauttaa aina text/html-tyypin, tyyppi
on helpointa vaihtaa kerralla kaikille näkymille
after_request-koukussa:
@app.after_request
def xhtml_tyyppi(vastaus):
if vastaus.mimetype == "text/html":
vastaus.headers["Content-Type"] = "application/xhtml+xml; charset=UTF-8"
return vastaus
Edelleenohjaus ja virhesivut
Uudelleenohjaus tehdään
redirect-funktiolla ja käsittely
keskeytetään virheeseen
abort-funktiolla.
from flask import abort, redirect, url_for
@app.route("/vanha-osoite")
def vanha():
# oletuksena 302; lomakkeen käsittelyn jälkeen käytä 303
return redirect(url_for("uusi"), code=301)
@app.route("/salainen")
def salainen():
if not kirjautunut():
abort(403) # keskeyttää käsittelyn ja tuottaa virhevastauksen
return render_template("salainen.html")
Flaskin omat virhesivut voi korvata omilla
errorhandler-koukulla:
@app.errorhandler(404)
def ei_loydy(virhe):
# toinen paluuarvo on statuskoodi - älä palauta virhesivua koodilla 200
return render_template("virhe.html", koodi=404), 404
Lomakkeen käsittely
Tyypillinen lomake käsitellään POST/Redirect/GET-kuviolla: virheellinen syöte palauttaa lomakkeen takaisin virheilmoituksineen ja aiemmin annettuine arvoineen, ja onnistunut käsittely ohjaa selaimen hakemaan tulossivun GET-pyynnöllä.
@app.get("/kommentit")
def kommentit():
return render_template("kommentit.html", kommentit=hae_kommentit())
@app.post("/kommentit")
def lisaa_kommentti():
teksti = request.form.get("kommentti", "").strip()
if not teksti:
# lomake takaisin: virheilmoitus ja käyttäjän aiempi syöte mukana
return render_template("kommentit.html",
kommentit=hae_kommentit(),
virhe="Kirjoita kommentti ennen lähettämistä.",
syote=teksti), 400
tallenna_kommentti(teksti)
# 303: selain hakee tuloksen GET-pyynnöllä, joten sivun päivittäminen
# ei lähetä lomaketta uudelleen
return redirect(url_for("kommentit"), code=303)
Jinja-templatet
Yhtenäisen sivuston sivuilla on paljon samankaltaisia osia: navigointi, otsikkoalue, alatunniste, käyttäjän tarkistus. Ilman templateja ne kopioidaan sivulta toiselle — ja jonkin sivun kohdalta unohtuu.
Templateilla saavutetaan:
- yhteiset osat kootaan yhteen paikkaan
- yhteiset osat liitetään sivulle automaattisesti
- sivun tekijä keskittyy sisältöön
- ohjelmalogiikka erottuu esitystavasta
HTML-koodia ei pidä kirjoittaa Python-ohjelman sisään, koska tuloksena on ylläpitokelvoton koodin ja merkkauksen sekasotku. Pyri MVC-malliin, jossa toimintalogiikka pysyy erossa näkymästä.
Tee Python-koodissa kaikki mahdollisimman valmiiksi ja pidä Jinja-koodi yksinkertaisena. Jos templateen ilmestyy laskentaa tai monimutkaisia ehtoja, se kuuluu Python-puolelle.
from flask import render_template
@app.route("/hello/")
@app.route("/hello/<nimi>")
def hello(nimi=None):
return render_template("hello.html", nimi=nimi)
Templateja etsitään templates-alikansiosta, joka on samassa
kansiossa kuin sovellus. Templatessa ovat käytettävissä
render_template-kutsun parametrit sekä request-,
session- ja g-objektit.
Jinja-koodi erotetaan merkkauksesta kolmella merkinnällä:
{% ... %}ohjausrakenteet{{ ... }}arvon tulostaminen{# ... #}kommentti, joka ei päädy valmiiseen sivuun
{# kurssit taulukkona; tyhjä lista käsitellään else-haarassa #}
<table>
<tr><th>#</th><th>Kurssi</th><th>Opintopisteet</th></tr>
{% for kurssi in kurssit %}
<tr class="{{ 'parillinen' if loop.index is even else 'pariton' }}">
<td>{{ loop.index }}</td>
<td>
<a href="{{ url_for('kurssi', tunnus=kurssi.tunnus) }}">{{ kurssi.nimi }}</a>
{% if kurssi.uusi %}<strong>uusi</strong>{% endif %}
</td>
<td>{{ kurssi.op }}</td>
</tr>
{% else %}
<tr><td colspan="3">Yhtään kurssia ei löytynyt.</td></tr>
{% endfor %}
</table>
Silmukassa on käytettävissä loop-muuttuja:
loop.index on 1-alkuinen ja loop.index0 0-alkuinen
kierroslaskuri, loop.first ja loop.last kertovat
ensimmäisen ja viimeisen kierroksen. Jinjan silmukkaa ei voi keskeyttää
break-lauseella.
Silmukalla voi olla {% else %}-haara, joka suoritetaan kun
läpikäytävässä kokoelmassa ei ole yhtään alkiota. Se on siisti tapa hoitaa tyhjän
listan tapaus ilman erillistä {% if %}-tarkistusta.
Uudet muuttujat luodaan set-komennolla, ja arvoja muokataan
filttereillä,
jotka erotetaan |-merkillä:
{% set otsikko = nimi|default("Tuntematon")|title %}
<p>Kommentteja: {{ kommentit|length }}</p>
Templatejen periytyminen
Periytyminen on se mekanismi, jolla yhteiset osat kootaan yhteen paikkaan. Pohjatemplate määrittelee sivun rakenteen ja jättää siihen nimettyjä lohkoja, jotka yksittäiset sivut täyttävät.
{# templates/base.html #}
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="fi" xml:lang="fi">
<head>
<meta charset="UTF-8" />
<title>{% block otsikko %}Sovellus{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='tyyli.css') }}" />
</head>
<body>
{% include "navigointi.html" %}
<main>
{% block sisalto %}{% endblock %}
</main>
<footer><p>TIES4080</p></footer>
</body>
</html>
{# templates/kommentit.html #}
{% extends "base.html" %}
{% block otsikko %}Kommentit{% endblock %}
{% block sisalto %}
<h1>Kommentit</h1>
{% if virhe %}
<p class="virhe">{{ virhe }}</p>
{% endif %}
<ul>
{% for kommentti in kommentit %}
<li>{{ kommentti }}</li>
{% endfor %}
</ul>
{% endblock %}
{% extends %} on oltava templaten ensimmäinen rivi.
{% include %} liittää toisen templaten sellaisenaan, ja toistuvat
pätkät kannattaa tehdä makroina:
{% macro kentta(nimi, otsikko, arvo="") %}
<p>
<label for="{{ nimi }}">{{ otsikko }}</label>
<input id="{{ nimi }}" name="{{ nimi }}" type="text" value="{{ arvo }}" />
</p>
{% endmacro %}
{{ kentta("p1", "Pelaaja 1", pelaaja1) }}
Autoescape ja turvallisuus
Jinja koodaa oletuksena kaikki {{ }}-merkinnällä tulostetut arvot
niin, että &, <, > ja
lainausmerkit muuttuvat entiteeteiksi. Flaskissa autoescape on päällä
.html-, .htm-, .xhtml- ja
.xml-päätteisissä templateissa.
{{ muuttuja|safe }} ohittaa koodauksen. Käyttäjän syötteeseen
käytettynä se avaa suoran reitin skriptin ajamiseen sivullasi. XHTML-tyypillä
tarjoiltuna seuraus on vielä välittömämpi: yksikin koodaamaton
&-merkki tekee dokumentista epäkelvon XML:ää, ja selain
näyttää koko sivun sijasta jäsennysvirheen. Escapaus ei siis ole vain
tietoturva-asia vaan korrektiusvaatimus.
Osoitteet muodostetaan templatessakin url_for-funktiolla. Se
hoitaa prosenttikoodauksen, ja autoescape hoitaa &-merkkien
muuttamisen &-entiteeteiksi — kahta eri koodausta ei siis
tarvitse tehdä käsin.
Tiedostojen vastaanotto
Tiedostoja lähettävän lomakkeen on käytettävä POST-metodia ja
multipart/form-data-koodausta:
<form action="{{ url_for('lataa') }}" method="post" enctype="multipart/form-data">
<input type="file" name="liite" />
<input type="submit" value="Lähetä" />
</form>
import os
from werkzeug.utils import secure_filename
@app.post("/lataa")
def lataa():
tiedosto = request.files.get("liite")
if tiedosto is None or not tiedosto.filename:
return render_template("lataa.html", virhe="Valitse tiedosto."), 400
# secure_filename siivoaa polkuerottimet ja muut vaaralliset merkit
nimi = secure_filename(tiedosto.filename)
tiedosto.save(os.path.join(app.config["LATAUSKANSIO"], nimi))
return redirect(url_for("valmis"), code=303)
Werkzeugin
secure_filename
poistaa nimestä polkuerottimet ja muut merkit, joilla tiedosto voitaisiin
kirjoittaa väärään kansioon. Älä koskaan käytä käyttäjän antamaa tiedostonimeä
sellaisenaan, ja rajoita
lähetyksen kokoa asetuksella MAX_CONTENT_LENGTH.
Sessiot ja evästeet
Flaskin session on sanakirja, jonka sisältö kulkee selaimessa
allekirjoitetussa evästeessä. Allekirjoitus estää sisällön muuttamisen, mutta
ei salaa sitä: kuka tahansa voi lukea evästeen sisällön. Älä
siis tallenna sinne salasanoja tai muuta arkaluontoista.
import os
from flask import session
# ilman salaista avainta sessio ei toimi lainkaan
app.secret_key = os.environ["SECRET_KEY"]
session["kayttaja"] = tunnus # tallenna
tunnus = session.get("kayttaja") # lue
session.pop("kayttaja", None) # poista (uloskirjautuminen)
Avain asetetaan
secret_key-attribuuttiin.
Salaista avainta ei kirjoiteta lähdekoodiin eikä versionhallintaan, vaan se
luetaan ympäristömuuttujasta tai erillisestä asetustiedostosta.
Kts. evästeet ja sessiot.
Flash-viestit
Kun lomakkeen käsittely päättyy uudelleenohjaukseen, käsittelijä ei voi enää itse
kertoa käyttäjälle mitä tapahtui: tulossivun tuottaa eri funktio eri pyynnössä.
Tähän on
flash-funktio.
Se tallentaa viestin sessioon, ja seuraava sivunlataus näyttää sen kerran ja
poistaa sen.
Koska viestit kulkevat sessiossa, secret_key on oltava asetettuna.
Samasta syystä viesti kulkee selaimen evästeessä, joten sinne ei kirjoiteta mitään
arkaluontoista.
from flask import flash, redirect, url_for
@app.post("/kommentit")
def lisaa_kommentti():
teksti = request.form.get("kommentti", "").strip()
if not teksti:
flash("Kirjoita kommentti ennen lähettämistä.", "virhe")
return redirect(url_for("kommentit"), code=303)
tallenna_kommentti(teksti)
flash("Kommentti tallennettiin.", "onnistui")
return redirect(url_for("kommentit"), code=303)
Toinen parametri on vapaavalintainen luokka, jolla viestit voidaan erotella
toisistaan ulkoasussa. Templatessa viestit luetaan
get_flashed_messages-funktiolla.
Luonteva paikka sille on pohjatemplate, jolloin ilmoitukset toimivat kaikilla
sivuilla ilman erillistä koodia:
{# templates/base.html #}
{% with viestit = get_flashed_messages(with_categories=true) %}
{% if viestit %}
<ul class="ilmoitukset">
{% for luokka, viesti in viestit %}
<li class="{{ luokka }}">{{ viesti }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
Ilman with_categories=true-parametria funktio palauttaa pelkät
viestit ilman luokkia. Lukeminen tyhjentää jonon, joten sama viesti ei näy
kahdesti.
Flash sopii ilmoituksiin, jotka kertovat mitä juuri tapahtui: tallennus onnistui, kirjautuminen vanheni, poisto tehtiin. Kenttäkohtaiset validointivirheet sen sijaan kuuluvat lomakkeen viereen, ja silloin lomake kannattaa palauttaa suoraan käyttäjän aiemmilla arvoilla täytettynä eikä uudelleenohjata.
Konfiguraatio ja sovelluksen rakenne
Asetukset kulkevat app.config-sanakirjassa. Ympäristökohtaiset
arvot luetaan ympäristömuuttujista, jolloin sama koodi toimii sekä omalla koneella
että palvelimella:
app.config["MAX_CONTENT_LENGTH"] = 2 * 1024 * 1024 # 2 MiB
app.config["LATAUSKANSIO"] = os.environ.get("LATAUSKANSIO", "/tmp")
Pyyntökohtaista tietoa voi säilyttää g-objektissa, ja koko
sovellukselle yhteiset toimenpiteet hoituvat koukuilla
before_request ja
teardown_request:
from flask import g
@app.before_request
def avaa_yhteys():
g.kanta = avaa_tietokanta()
@app.teardown_request
def sulje_yhteys(poikkeus):
kanta = g.pop("kanta", None)
if kanta is not None:
kanta.close()
Kun sovellus kasvaa, näkymät jaetaan blueprinteiksi, jolloin kukin osa-alue on omassa moduulissaan. Harjoitustyössä tämä kannattaa ottaa käyttöön heti alusta.
Globaali muuttuja on prosessikohtainen. CGI-ympäristössä tulkki käynnistyy joka pyynnöllä uudelleen, ja pilvipalvelussa samasta sovelluksesta voi olla käynnissä useita instansseja omine muuttujineen. Tila kuuluu osoitteeseen, dokumenttiin, sessioon tai tietokantaan.
Lokitus
Kun sovellus on palvelimella, virheilmoitusta ei voi enää katsoa selaimesta.
Loki on ainoa tapa nähdä, mitä sovelluksessa tapahtui. Flaskissa on valmis
lokitin
app.logger,
joka on tavallinen Pythonin
logging-kirjaston
lokitin:
app.logger.debug("Kehityksen aikainen yksityiskohta")
app.logger.info("Kommentti tallennettiin, pituus %s merkkiä", len(teksti))
app.logger.warning("Tuntematon kieli pyynnössä: %s", kieli)
app.logger.error("Tallennus epäonnistui")
try:
yhteys = avaa_tietokanta()
except sqlite3.Error:
# exception kirjaa virheen ja koko pinolistauksen; käytä vain except-lohkossa
app.logger.exception("Tietokantayhteys ei aukea")
abort(500)
Anna muuttujat erillisinä parametreina ("... %s", arvo) äläkä
liimaa niitä valmiiksi merkkijonoon. Silloin muotoilu tehdään vasta jos viesti
todella kirjoitetaan, ja lokirivit pysyvät samanmuotoisina, mikä helpottaa
hakemista.
Lokituksen asetukset
Oletuksena Flask kirjoittaa lokin standardivirhevirtaan. Omat asetukset
annetaan
dictConfig-funktiolla,
ja se on kutsuttava ennen kuin Flask-olio luodaan:
import os
from logging.config import dictConfig
from flask import Flask
dictConfig({
"version": 1,
"formatters": {
"oletus": {
"format": "[%(asctime)s] %(levelname)s %(module)s: %(message)s",
},
},
"handlers": {
"tiedosto": {
"class": "logging.handlers.RotatingFileHandler",
"filename": os.path.abspath("../hidden/sovellus.log"),
"maxBytes": 1000000,
"backupCount": 3,
"formatter": "oletus",
},
},
"root": {"level": "INFO", "handlers": ["tiedosto"]},
})
app = Flask(__name__)
RotatingFileHandler vaihtaa tiedostoa, kun se kasvaa liian
suureksi, eikä loki siksi täytä levykiintiötä. Lokitaso valitaan ympäristön
mukaan: DEBUG omalla koneella, INFO tai
WARNING palvelimella.
Loki eri ympäristöissä
| Ympäristö | Minne loki menee | Miten sitä luetaan |
|---|---|---|
| Oma kone | Kehityspalvelimen konsoli | Suoraan päätteessä, johon flask run jäi pyörimään |
| PythonAnywhere | Standarditulostus server.log-tiedostoon,
standardivirhe error.log-tiedostoon |
Web-välilehden Log files -kohdasta |
| Cloud Run | Standarditulostus ja -virhe kerätään Cloud Loggingiin | Palvelun lokinäkymästä |
| users.jyu.fi | Vain omaan tiedostoon; palvelimen omiin lokeihin ei ole pääsyä | Pääteyhteydellä tail -f |
Cloud Runissa ja PythonAnywheressä lokia ei kirjoiteta tiedostoon vaan standardivirtaan, jonka palvelu kerää talteen. Tiedostoon kirjoittaminen on niissä turhaa ja jopa harhaanjohtavaa, koska tiedostojärjestelmä on väliaikainen ja instansseja voi olla useita, kullakin oma tiedostonsa.
Älä kirjoita lokia cgi-bin-kansioon tai sen
alikansioihin. Kansioon ei saa olla kirjoitusoikeutta, joten kirjoitus
epäonnistuu — ja jos se onnistuisi, CGI lakkaisi toimimasta. Käytä kansiota
cgi-bin-hakemiston ulkopuolelta, esimerkiksi
../hidden/, ja anna polku absoluuttisena
(os.path.abspath), koska prosessin työhakemisto ei välttämättä ole
se, mitä oletat.
Älä käytä print-funktiota lokitukseen.
CGI-ohjelmassa standarditulostus menee sellaisenaan selaimelle ja rikkoo
vastauksen keskeltä. Lokitus kuuluu app.logger-oliolle.
# lokin seuraaminen pääteyhteydellä
tail -f ~/public_html/hidden/sovellus.log
Salasanat, istuntotunnisteet, evästeiden sisältö, henkilötiedot ja kokonaiset lomakerungot eivät kuulu lokiin. Loki jää talteen pitkäksi aikaa, sitä lukee useampi ihminen kuin arvaat, ja se päätyy varmuuskopioihin. Kirjaa mieluummin tunniste ja tapahtuma kuin koko sisältö.
Luentoesimerkit
- Esimerkki 1 (lähdekoodi, template jinja.html)
- Esimerkki 2 (lähdekoodi, templatet jinja2.html ja base.html)
Lisätietoa
- Flaskin dokumentaatio ja Quickstart
- Jinja: Template Designer Documentation
- Werkzeug: Data Structures (MultiDict ja kumppanit)
- Message Flashing — flash-viestien käyttötapa
- Logging — Flaskin lokitus
- Flask-WTF — lomakkeiden validointi ja CSRF-suojaus
- WSGI, CGI ja Flask
- HTTP-protokolla
- Pääteohjaus 1: Flaskin asennus
Käyttäjien kommentit