WSGI, CGI ja Flask
Python-sovellus ei koskaan puhu selaimen kanssa suoraan, vaan aina jonkin www-palvelinrajapinnan välityksellä. Tällä kurssilla rajapintoja on kaksi:
- WSGI (Web Server Gateway Interface) on nykyaikainen tapa. Python-tulkki jää muistiin odottamaan seuraavia pyyntöjä. Flask, Django ja käytännössä kaikki Python-sovelluskehykset ovat WSGI-sovelluksia. PythonAnywhere käyttää WSGI:tä.
- CGI
(Common Gateway Interface) on vanhin ja yksinkertaisin rajapinta: www-palvelin
käynnistää jokaista pyyntöä varten uuden prosessin.
users.jyu.fi(karahka2) tarjoaa vain tämän, joten siellä Flask-sovellus on käynnistettävä CGI-ohjelmana.
Sovelluskoodiisi tämä ei vaikuta. Kirjoitat tavallisen
Flask-sovelluksen, ja rajapinta vaihtuu vain sen mukaan, millä
käynnistystiedostolla sovellus ajetaan. Tässä luennossa CGI:tä käsitellään vain
siltä osin kuin sitä tarvitaan Flaskin käynnistämiseen
users.jyu.fi-palvelimella.
Käytännön asennusohjeet ja harjoitukset ovat pääteohjauksessa 1.
WSGI lyhyesti
WSGI-sovellus on funktio (tai mikä tahansa kutsuttava olio), joka saa kaksi parametria ja palauttaa vastauksen sisällön tavujonoina:
# environ on dict, joka sisältää CGI-tyyliset ympäristömuuttujat: pyydetyn
# osoitteen, metodin, otsakkeet ja pyynnön rungon (wsgi.input)
# start_response on funktio, jolla asetetaan HTTP-status ja HTTP-otsakkeet
def application(environ, start_response):
sisalto = "Ympäristömuuttujat\n"
for avain in environ:
sisalto += "%s\t:\t%s\n" % (avain, environ[avain])
status = "200 OK"
otsakkeet = [
("Content-Type", "text/plain;charset=UTF-8"),
("Content-Length", str(len(sisalto.encode("UTF-8")))),
]
start_response(status, otsakkeet)
# Paluuarvo on iteroituva kokoelma tavujonoja, ei merkkijonoja
return [sisalto.encode("UTF-8")]
Olennaista:
- Sovellus ei tulosta mitään
print-funktiolla. Kaikki sisältö palautetaan. - HTTP-otsakkeet asetetaan
start_response-funktiolla, ei tulostamalla. - Vähintään
Content-Typeon asetettava. - Paluuarvo on tavuja (
bytes), joten merkkijono on koodattavaencode("UTF-8").
Flask on rakennettu
Werkzeugin päälle, ja Werkzeug
hoitaa juuri tämän rajapinnan. app-olio on WSGI-sovellus.
Siksi et joudu itse kirjoittamaan edelläolevan kaltaista koodia kuin korkeintaan
kerran opetusmielessä.
ASGI ja rajapintojen erot
ASGI (Asynchronous Server Gateway Interface) on WSGI:n asynkroninen seuraaja. Sitä ei tällä kurssilla käytetä, mutta se kannattaa tuntea, koska uudet Python-kehykset rakentuvat sen varaan.
WSGI-sovellus on tavallinen funktio, joka ottaa vastaan yhden pyynnön ja palauttaa yhden vastauksen. ASGI-sovellus on asynkroninen funktio, joka saa kolme parametria:
async def application(scope, receive, send):
# scope kertoo yhteyden tyypin ja tiedot: "http", "websocket" tai "lifespan"
# receive odottaa seuraavaa viestiä asiakkaalta
# send lähettää viestin asiakkaalle
await send({"type": "http.response.start", "status": 200,
"headers": [(b"content-type", b"text/plain; charset=utf-8")]})
await send({"type": "http.response.body", "body": "Hello World".encode("UTF-8")})
Erot ovat käytännössä nämä:
- Vastaus ei ole yksi paluuarvo vaan jono viestejä. Siksi sama rajapinta kantaa myös WebSocket-yhteydet ja striimatun vastauksen, joita WSGI ei luontevasti taivu käsittelemään.
- Yksi prosessi voi käsitellä useaa pyyntöä yhtä aikaa. Kun sovellus odottaa tietokantaa tai toista palvelinta, tulkki ehtii tehdä muuta. WSGI:ssä sama vaatii yleensä oman säikeen tai prosessin kutakin samanaikaista pyyntöä kohti, ja CGI:ssä aina kokonaan uuden prosessin.
- Sovelluskehykset ja palvelimet ovat eri joukko.
ASGI-kehyksiä ovat esimerkiksi FastAPI, Starlette ja Quart, palvelimia uvicorn
ja hypercorn. Flask on WSGI-sovellus, vaikka se versiosta 2.0 alkaen sallii
async def-näkymät; ASGI-palvelimella sen voi ajaaasgiref-kirjastonWsgiToAsgi-kääreellä.
Quart on näistä
lähimpänä tuttua: se on Flaskin rajapinnan asynkroninen toteutus samalta
Pallets-projektilta, joten reitit, templatet ja request-olio
toimivat samoin kuin Flaskissa. Käytännössä route-funktiot vain
määritellään async def-muodossa:
from quart import Quart, render_template
app = Quart(__name__)
@app.route("/")
async def etusivu():
return await render_template("lauta.html", koko=8)
Koska sovellus on ASGI-sovellus, Quartilla voi kirjoittaa myös WebSocket- käsittelijöitä ja striimattuja vastauksia, mikä ei Flaskilla onnistu. Suurin osa Flask-laajennuksista toimii sellaisenaan, ja dokumentaatiossa on oma ohjeensa Flask-sovelluksen siirtämiseen Quartille.
Kurssin loppupuolen tehtävissä sovellukset julkaistaan Googlen Cloud
Run -palvelussa, jossa ASGI-sovellus toimii siinä missä WSGI-sovelluskin. Niissä
Quart on siis ihan käyttökelpoinen valinta, jos haluat kirjoittaa asynkronista
koodia. Sen sijaan users.jyu.fi-palvelimella Quartia ei voi käyttää,
koska siellä ei aja edes WSGI-sovelluksia.
| Rajapinta | Prosessimalli | Samanaikaisuus | Soveltuu |
|---|---|---|---|
| CGI | uusi prosessi joka pyynnölle | ei mitään; tulkin käynnistys joka kerta | pienet sovellukset ja rajoittuneet palvelimet, kuten
users.jyu.fi |
| WSGI | tulkki jää muistiin | säie tai prosessi kutakin pyyntöä kohti | tavalliset sivustot ja lomakesovellukset |
| ASGI | tulkki jää muistiin | useita pyyntöjä samassa säikeessä; odotusaika hyödynnetään | WebSocketit, striimaus, paljon rinnakkaisia yhteyksiä |
Sovelluslogiikan kannalta valinta ei useinkaan näy: reitit ja templatet kirjoitetaan samalla tavalla, ja rajapinta ratkaisee lähinnä sen, miten palvelin käynnistää sovelluksen ja montako pyyntöä se pystyy hoitamaan yhtä aikaa.
WSGI ja PythonAnywhere
PythonAnywhere ajaa sovelluksia WSGI-rajapinnan kautta. Web-välilehdellä luodaan uusi sovellus (Add a new web app), ja tyypiksi valitaan joko Manual configuration tai suoraan Flask.
WSGI configuration file on tiedosto, josta palvelin etsii
nimenomaan application-nimisen olion. Flask-sovelluksessa riittää
tuoda oma app ja nimetä se uudelleen:
import sys
# oman koodin kansio hakupolkuun
polku = "/home/oma_tunnus/mysite"
if polku not in sys.path:
sys.path.insert(0, polku)
from oma import app as application
Log files -kohdasta löytyvät lokit:
access.log: kuka on ladannut minkäkin resurssin ja milloinerror.log: virheilmoitukset eli kaikki STDERR:iin kirjoitettuserver.log: muu lokitieto, myös STDOUT:iin tehdyt tulosteet
import sys
print("tämä näkyy server.logissa")
print("tämä näkyy error.logissa", file=sys.stderr)
Huomaa ero CGI-ympäristöön: CGI-ohjelmassa print menisi suoraan
selaimelle ja rikkoisi vastauksen.
Virheet saa myös suoraan selaimeen lisäämällä WSGI-konfiguraatiotiedostoon:
from werkzeug.debug import DebuggedApplication
application.debug = True
application = DebuggedApplication(application)
- Koodimuutokset eivät tule voimaan ennen kuin sovellus käynnistetään uudelleen editorin tai Web-välilehden reload-painikkeella. kts. Reload web app.
- Staattiset tiedostot jaetaan Web-välilehden Static Files -kohdassa: vasempaan sarakkeeseen URL, oikeaan tiedostopolku.
- Ilmaisella tunnuksella ulkoisia osoitteita saa hakea vain
sallitusta listasta.
Esimerkiksi
appro.mit.jyu.fiei ole listalla.
Älä käytä globaaleja muuttujia sovelluksen tilan säilyttämiseen. Globaalit muuttujat ovat prosessikohtaisia:
- CGI-sovelluksessa tulkki käynnistetään joka pyynnöllä uudelleen, joten globaalit nollautuvat aina.
- WSGI-sovelluksessa tulkki jää muistiin, mutta se voidaan käynnistää uudelleen milloin tahansa.
- Kuormantasatussa ympäristössä (esim. Cloud Run) samasta sovelluksesta on käynnissä useita instansseja, joilla kullakin on omat globaalinsa.
Sovelluksen tila on siis tallennettava muualle: dokumentin rakenteeseen, osoitteeseen, evästeisiin, sessioon tai tietokantaan.
CGI ja Flask users.jyu.fi-palvelimella
users.jyu.fi-palvelin ei aja WSGI-sovelluksia, joten Flask on
käynnistettävä CGI-ohjelmana. CGI:stä riittää tietää seuraava:
- www-palvelin käynnistää jokaista pyyntöä kohti uuden prosessin ja suorittaa tiedoston sen ensimmäisellä rivillä (shebang) mainitulla tulkilla
- pyynnön tiedot välitetään ohjelmalle ympäristömuuttujissa ja standardisyötteessä
- ohjelman standarditulostus menee sellaisenaan selaimelle, ja sen on alettava HTTP-otsakkeilla, joita seuraa tyhjä rivi
Yksinkertaisin mahdollinen CGI-ohjelma näyttää tältä:
#!/usr/bin/python3
# -*- coding: utf-8 -*-
print("""Content-Type: text/plain; charset=UTF-8
Hello world!""")
Tätä käsin tulostamista ei tarvitse tehdä omassa
sovelluksessa — Flask hoitaa otsakkeet puolestasi. Yllä oleva
hello.cgi kannattaa silti kokeilla ensin, koska sillä varmistuu, että
palvelimen asetukset ja tiedosto-oikeudet ovat kunnossa.
Asennuksen tarkistuslista
Viimeisimmät tiedot löytyvät aina digipalvelujen ohjeesta: CGI/SSI-tekniikoiden käyttäminen www-palveluissa (users.jyu.fi/groups.jyu.fi). Ohje on ristiriitatilanteessa aina tämän sivun edellä.
- CGI-ohjelman on oltava
W:\cgi-bin\-kansiossa tai sen alikansiossa. Mikään muu kansio ei kelpaa. Tällä kurssilla käytetään kansiotaW:\cgi-bin\ties4080\. - Tiedostopäätteen on oltava
.cgi - Suoritettavan tiedoston ja sen kansion omistajan on oltava oma tunnuksesi ja
ryhmän
users. Opiskelijatunnuksilla ryhmä on ainausers, jotenchgrp usersriittää; ryhmän voi tarkistaaid-komennolla. - Tiedostoon tai kansioon ei saa olla kirjoitusoikeutta muilla kuin omistajalla
- Tiedostolla on oltava suoritusoikeus
[tunnus@halava ties4080]$ chgrp users hello.cgi
[tunnus@halava ties4080]$ chmod 755 hello.cgi
[tunnus@halava ties4080]$ chmod go-w .
[tunnus@halava ties4080]$ ls -al hello.cgi
-rwxr-xr-x. 1 tunnus users 3777 Feb 5 09:59 hello.cgi
- Rivinvaihtojen on oltava unix-muotoisia (
LF), ei windows-muotoisia (CRLF) - Tiedoston alussa ei saa olla BOM-merkintää
- Harvinaisissa tapauksissa SELinux voi aiheuttaa ongelmia; toimi silloin digipalvelujen ohjeen mukaan
Yleisin syy Internal Server Error -virheeseen on jokin näistä, ei
ohjelmakoodi.
Virtuaaliympäristö ja shebang
Flask asennetaan omaan virtuaaliympäristöön
cgi-bin/ties4080/-kansioon:
python3 -m venv venv
. venv/bin/activate
pip install --upgrade pip
pip install Flask Flask-WTF
deactivate
Shebang-rivin on osoitettava tämän virtuaaliympäristön tulkkiin.
Polku ei ole sama kuin se, jonka näet halava/jalava-koneessa
pwd-komennolla, koska ohjelma suoritetaan
karahka2-palvelimella:
#!/home/oma_tunnus/public_html/cgi-bin/ties4080/venv/bin/python
flask.cgi ja oma.py
flask.cgi on ainoa tiedosto, jossa CGI näkyy. Se toimii siltana:
ottaa Flask-sovelluksen ja ajaa sen CGI-rajapinnan kautta. Sisällöltään se vastaa
sitä, mitä PythonAnywhere kirjoittaa WSGI-konfiguraatiotiedostoon.
#!/home/oma_tunnus/public_html/cgi-bin/ties4080/venv/bin/python
# -*- coding: utf-8 -*-
# suorittaa Flask-sovelluksen CGI-ohjelmana users.jyu.fi-palvelimella
import sys
from wsgiref.handlers import CGIHandler
try:
from oma import app as application
from werkzeug.debug import DebuggedApplication
if __name__ == "__main__":
application.debug = True
CGIHandler().run(DebuggedApplication(application))
except Exception:
# Tänne päädyttäessä Werkzeug ei toimi, joten HTTP-otsake on tulostettava
# itse. STDOUT menee tässä tapauksessa suoraan selaimelle.
print("Content-Type: text/plain;charset=UTF-8\n")
print("Sovellus ei käynnisty:\n")
for virhe in sys.exc_info():
print(virhe)
wsgiref on Pythonin vakiokirjastoa, ja sen
CGIHandler on juuri se palanen, joka kääntää CGI-pyynnön
WSGI-kutsuksi. Varsinainen sovellus on tavallinen Flask-sovellus tiedostossa
oma.py — tiedoston nimen on vastattava flask.cgi:n
from oma import app-riviä:
# -*- coding: utf-8 -*-
from flask import Flask, Response, request
app = Flask(__name__)
# @app.route määrää, mille osoitteelle tämä funktio suoritetaan
@app.route("/")
def hello_world():
return Response("Hello World", content_type="text/plain; charset=UTF-8")
Sovellus avataan osoitteesta
https://users.jyu.fi/~oma_tunnus/cgi-bin/ties4080/flask.cgi/
Viimeinen /-merkki on olennainen, koska se vastaa
@app.route("/")-riviä. Kaikki flask.cgi:n jälkeen
tulevat polut ohjautuvat Flaskin reiteille:
.../flask.cgi/laskuri vastaa reittiä
@app.route("/laskuri").
Debuggaus
Ensisijainen tapa on debugata omalla koneella, jossa Flaskin oma kehityspalvelin näyttää virheet suoraan:
flask run --debug
Palvelimella syntaksivirheet löytyvät ajamalla ohjelma komentoriviltä virtuaaliympäristön tulkilla. Sovellus ei voi tällöin oikeasti toimia, mutta koodin virheet paljastuvat:
. venv/bin/activate
python oma.py
users.jyu.fi-palvelimen omiin lokitiedostoihin ei ole pääsyä,
joten virheet on joko näytettävä selaimessa (DebuggedApplication,
ks. edellä) tai kirjoitettava omaan lokitiedostoon:
import logging, os
logging.basicConfig(filename=os.path.abspath("../hidden/flask.log"),
level=logging.DEBUG)
try:
yhteys = sqlite3.connect(os.path.abspath("../hidden/resepti"))
except sqlite3.Error as e:
logging.debug("Kanta ei aukea: %s", e)
Älä kirjoita lokia cgi-bin-kansioon tai sen
alikansioihin. Se ei toimi, koska kansioon ei saa olla
kirjoitusoikeutta.
Lokia on helpointa seurata pääteyhteydellä:
tail -f ../hidden/flask.log
Käytä except-lohkossa aina mahdollisimman tarkkaa
poikkeustyyppiä. Kaiken kaappaava except: piilottaa myös omat
ohjelmointivirheesi.
Merkistöt
Sinun on aina tiedettävä, mikä merkistö on merkkijonoissa käytössä. Käytä kaikkialla UTF-8:aa.
- Kirjoita ohjelmakoodi ja templatet UTF-8-merkistössä. Python 3 olettaa
lähdekoodin olevan UTF-8, joten
# -*- coding: utf-8 -*--riviä ei enää tarvita. Se ei kuitenkaan haittaa. - Merkkijonon ja tavujonon välillä liikutaan metodeilla
encodejadecode. Sitä tarvitaan lähinnä WSGI-tason koodissa; Flask hoitaa koodauksen puolestasi.
Merkistö kerrotaan HTTP-otsakkeessa. Flaskissa se asetetaan
Response-olion kautta:
return Response(sisalto, content_type="text/html; charset=UTF-8")
return Response(sisalto, content_type="text/plain; charset=UTF-8")
# kurssin viikkotehtävissä käytetään XHTML:ää:
return Response(sisalto, content_type="application/xhtml+xml; charset=UTF-8")
HTTP-otsakkeen pitäisi riittää, mutta jos merkistö mainitaan jossain
muuallakin, myös siellä on oltava UTF-8: mahdollinen xml-deklaraatio,
meta-elementti ja lomakkeen
accept-charset-attribuutti.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fi" lang="fi">
<head>
<meta charset="UTF-8" />
<title>Malli</title>
</head>
<body>
<form action="" method="post" accept-charset="UTF-8">
</form>
</body>
</html>
Querystring ja lomakkeet
Sivun osoitteeseen voidaan liittää parametreja:
https://osoite.example/?parametri=arvo¶metri2=arvo1¶metri2=arvo2
https://osoite.example/?nimi=M%C3%A4lli%20Henkil%C3%B6&arvosana=5&arvosana=3&kurssi=Helppo+kurssi&kurssi=%C3%84ll%C3%B6+kurssi
Osoitteen perään lisätään ?-merkki, jonka jälkeen luetellaan
avaimia ja arvoja &-merkillä eroteltuina. Samalla avaimella voi
olla useita arvoja.
Parametrien lukeminen
Flaskissa parametrit luetaan Request-objektista. Lomakkeelta tulevat tiedot käsitellään täsmälleen samalla tavalla:
request.argsosoitteen parametrit (MultiDict)request.formlomakeparametrit (POST)request.valuesmolemmat yhdessärequest.methodkäytetty metodi (GETtaiPOST)request.query_stringkoko querystring sellaisenaan (tavujonona)request.environWSGI-ympäristö
MultiDict toimii kuten tavallinen dict, mutta
palauttaa avainta kohti vain ensimmäisen arvon. Kaikki arvot saa
getlist-metodilla:
from flask import request
nimi = request.args.get("nimi", "Tuntematon")
# type-parametri muuntaa arvon ja palauttaa oletuksen, jos muunnos epäonnistuu
koko = request.args.get("l", 8, type=int)
arvosanat = request.args.getlist("arvosana")
Parametrien koodaaminen osoitteeseen
Osoitteessa esiintyvät erikoismerkit on koodattava percent-encoding-tavalla. Välilyönti, lainausmerkki, hakasulkeet ja ääkköset eivät kelpaa osoitteeseen sellaisenaan.
from urllib.parse import urlencode, quote_plus
# koko kyselymerkkijono kerralla, myös listat toimivat doseq-parametrilla
kysely = urlencode({"nimi": "Mälli Henkilö", "kurssi": "Ällö kurssi"})
# nimi=M%C3%A4lli+Henkil%C3%B6&kurssi=%C3%84ll%C3%B6+kurssi
# yksittäinen arvo
arvo = quote_plus("Ällö kurssi")
Flask-sovelluksessa osoitteet kannattaa rakentaa
url_for-funktiolla,
joka hoitaa koodauksen puolestasi:
from flask import url_for
osoite = url_for("lauta", l=8, p1="Mälli Henkilö")
Kun osoite sijoitetaan html-dokumenttiin, on lisäksi koodattava
&-merkit &-entiteetiksi. Tähän käytetään
html.escape-funktiota:
import html
linkki = '<a href="' + html.escape(osoite) + '">Linkki</a>'
Jinja2-templateissa autoescape hoitaa html-koodauksen automaattisesti, joten
templatessa riittää {{ osoite }}. Nämä ovat eri
asioita: percent-encoding tekee merkkijonosta kelvollisen osoitteen, ja
html.escape tekee osoitteesta kelvollisen attribuutin arvon.
Molempia tarvitaan.
Vanha cgi-kirjasto
Vanhoissa esimerkeissä (myös tämän kurssin aiemmissa materiaaleissa)
parametrit luetaan cgi-kirjaston FieldStorage-luokalla
ja virheet näytetään cgitb-moduulilla:
import cgi, cgitb # ei toimi enää Python 3.13:ssa
cgitb.enable()
fields = cgi.FieldStorage()
nimi = fields.getfirst("nimi", "Tuntematon")
arvosanat = fields.getlist("arvosana")
cgi- ja cgitb-moduulit poistettiin Pythonin
vakiokirjastosta versiossa 3.13
(PEP 594). Flask-sovelluksessa
niitä ei tarvita lainkaan: request.args ja
request.form korvaavat FieldStorage-luokan, ja
DebuggedApplication korvaa cgitb:n.
Jos joudut ylläpitämään vanhaa koodia, moduulit saa takaisin asentamalla
legacy-cgi-paketin, jolloin
vanhat import cgi- ja import cgitb-rivit toimivat
sellaisenaan:
pip install legacy-cgi
Uutta koodia ei kannata kirjoittaa niiden varaan.
Esimerkkejä
- JSON, omat luokat, lomakkeen korvaaminen linkillä (lähdekoodi, template)
- Python ja
CGI-ohjelmointiluento (youtube, 2016). Huomaa, että videon Python 2
-syntaksi ja
cgi-kirjaston käyttö ovat vanhentuneita; periaate on yhä sama.
Lisätietoa
- Pääteohjaus 1: users.jyu.fi, CGI ja Flask
- Flask: The Request Object
- Flask: URL Route Registrations
- Werkzeug
- wsgiref — WSGI Utilities and Reference Implementation
- PEP 3333 – Python Web Server Gateway Interface v1.0.1
- ASGI Documentation
- Quart (Migration from Flask)
- PEP 594 – Removing dead batteries from the standard library
- legacy-cgi
- CGI/SSI-tekniikoiden käyttäminen www-palveluissa (digipalvelut)
- Vanha luentomateriaali
Käyttäjien kommentit