11. Framework Flask


Obsah


Virtuální prostředí pythonu

Virtuální prostředí v Pythonu umožňuje izolovat balíčky a závislosti mezi různými projekty, takže každá aplikace může používat specifické verze knihoven bez konfliktu s jinými projekty. Základní motivace, proč používat virtuální prostředí:

Jak vytvořit a používat virtuální prostředí?

  1. Vytvoření virtuálního prostředí

    Tento příkaz vytvoří složku venv, která obsahuje izolované prostředí.

    python -m venv venv
  2. Aktivace virtuálního prostředí

    Aktivace na Windows probíhá pomocí souboru venv\Scripts\Activate:

    venv\Scripts\Activate

    Aktivace na Linux/macOS probíhá pomocí souboru venv/bin/activate:

    source venv/bin/activate
  3. Instalace balíčků

    Jakmile je prostředí aktivní, můžete do něj instalovat balíčky standardně pomocí příkazu pip install ....

    pip install numpy
  4. Deaktivace virtuálního prostředí

    Po skončení práce na projektu stačí spustit:

    deactivate
  5. Odstranění virtuálního prostředí

    Stačí smazat složku venv:

    rm -rf venv  # Linux/macOS
    rd /s /q venv  # Windows
  6. Uložení nainstalovaných balíčků

    Pro budoucí opětovnou instalaci si můžeme vytvořit soubor se seznamem závislostí:

    pip freeze > requirements.txt

    Tento soubor může mít například podobný obsah (kde == označuje přesně tuto verzi a >= minimální verzi):

    numpy==1.21.2
    pandas==1.3.3
    requests>=2.26.0

    Když chcete nainstalovat všechny závislosti z tohoto souboru (například v jiném prostředí) stačí spustit:

    pip install -r requirements.txt

Základy Flasku

Flask je jednoduchý a flexibilní webový framework napsaný v Pythonu. Je určen pro rychlou a snadnou tvorbu webových aplikací a API. Více najdete zde.

Flask je často označován za tzv. mikroframework (narozdíl například od frameworku Django), protože neobsahuje žádné vestavěné nástroje pro správu databází nebo autentizaci uživatelů, což vývojářům umožňuje přizpůsobit aplikaci přesně podle svých potřeb pomocí rozšíření a balíčků.

První aplikace

Nejprve vytvoříme jednoduchou aplikaci, co vypíše jen neformátovaný řetězec Hello world!. Instance třídy Flask (hlavní třída frameworku) představuje samotnou webovou aplikaci.

from flask import Flask

app = Flask(__name__)  # __name__ je název modulu obsaující tento kód


@app.route('/')  # vytvoříme odpověď pro hlavní stránku aplikace
def home():
    return 'Hello world!'

    
if __name__ == '__main__':
    app.run(debug=True, port=4444)  # spuštění aplikace pro testování na specifickém portu

Dekorátor @app.route()

Dekorátor @app.route() (pro jednoduchost předpokládejme, že v proměnné app máme instanci hlavní třídy Flask) je ve Flasku používán k definování koncových bodů aplikace, tedy URL, na které aplikace reaguje. Výsledek takto dekorované funkce je uživateli vrácen, po zaslání HTTP requestu na danou URL.

@app.route('/')  # route pro hlavní stránku
def home():
    return "Hello world!"


@app.route('/about')  # route pro stránku /about
def about():
    return "Toto je stránka O nás."


@app.route('/user/<username>')  # route s parametrem v url
def user_profile(username):
    return f"Ahoj, {username}!"


@app.route('/post/<int:post_id>')  # route s parametrem v url typu int
def post(post_id):
    return f"Zobrazuji příspěvek s ID {post_id}."

Výchozí nastavení route je obsluha GET reqeustů, což můžeme upravit pomocí nepovinného parametru methods.

from flask import Flask, request, jsonify

app = Flask(__name__)


@app.route('/submit', methods=['POST'])  # definujeme obsluhu pro POST request 
def submit():
    # Přijmeme data z požadavku
    data = request.json  # Očekáváme data ve formátu JSON
    if not data:
        return jsonify({"error": "No data provided"}), 400  # vracíme kód 400 - bad request
    
    # Zpracování dat
    name = data.get('name', 'Unknown')
    age = data.get('age', 0)
    
    return jsonify({
        "message": "Data received!",
        "name": name,
        "age": age
    })


if __name__ == '__main__':
    app.run(debug=True, port=5555)

Test lze provést například pomocí příkazu curl.

curl -X POST http://localhost:5555/submit \
-H "Content-Type: application/json" \
-d '{"name": "Petr", "age": 25}'

Pro práci s těmito koncovými body lze použít funkce url_for (dynamické generování URL na základě názvu funkce) a redirect pro přesměrování na jinou existující URL.

from flask import Flask, redirect, url_for

app = Flask(__name__)


@app.route('/')
def home():
    return "Vítejte na hlavní stránce!"


@app.route('/login')
def login():
    return "Toto je přihlašovací stránka."


@app.route('/admin')
def admin():
    # přesměrování na přihlašovací stránku
    return redirect(url_for('login'))


@app.route('/user/')
def user_profile(username):
    return f"Profil uživatele: {username}"


@app.route('/go_to_profile/')
def go_to_profile(username):
    # dynamicky vygeneruje URL pro uživatelský profil a přesměruje na něj
    return redirect(url_for('user_profile', username=username))


if __name__ == '__main__':
    app.run(debug=True)

Statické soubory

Při vytváření webové aplikace se nevyhneme používání statických souborů, ať už obrázků, html šablon, kaskádových stylů nebo Javascriptových skriptů. Pro přehlednost je dobré pro tyto typy souborů vytvořit samostané složky, viz. struktura projektu níže.

# typická struktura projektu ve Flasku
static/
    css/
        main.css
        ...
    img/
        img.png
        ...
    js/
        script.js
        ...
templates/
    base.html
    index.html
    about.html
    ...
app.py
module.py
...

Při vytvoření instance aplikace je potřeba specifikovat, kde tyto soubory hledat.

from flask import Flask

app = Flask(__name__, template_folder="templates", static_folder="static", static_url_path="/")

Perzistence dat

Takto vytvořená aplikace si samozřejmě uchová hodnoty svých proměnných jen po dobu svého vykonávání. Pokud chceme nějaká data uchovat i po ukončení naší webové aplikace, musíme data někam uložit. Nabízí se jednoduché řešení: uložit data do souborů (csv, json, xml atd.), což může být postačující řešení pro jednoduché aplikace.

V případě komplexnějších aplikací je vhodnější použít nějaký databázový systém. V pythonu je k dispozici například balíček SQLAlchemy, který poskytuje ORM (Object-Relational Mapper), což umožňuje pracovat s databázemi pomocí Python objektů namísto přímých SQL dotazů. Kromě toho umožňuje psát SQL dotazy přímo v Pythonu a funguje s bežnými databázovými systémy, jako jsou PostgreSQL, MySQL, SQLite, Oracle atd.

HTML šablony

Pro vytváření HTML šablon se ve Flasku používá šablonovací engine Jinja kombinující html a kód, který je podobný Pythonu. Více zde.

Jednoduchý příklad

Zde je kód aplikace obsahující dvě propojené HTML stránky. Obsah souboru index.html na adrese /:

<!DOCTYPE html>
<html lang="cs">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{ title }}</title>
</head>
<body>
    <h1>{{ message }}</h1>
    <p>Toto je dynamický obsah vložený pomocí Flasku.</p>
    <a href="/about">O nás</a>
</body>
</html>

Obsah souboru about.html na adrese /about:

<!DOCTYPE html>
<html lang="cs">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{ title }}</title>
</head>
<body>
    <h1>{{ title }}</h1>
    <p>{{ content }}</p>
    <a href="/">Zpět na hlavní stránku</a>
</body>
</html>

Samotný kód aplikace, který používá funkci render_template(), která načte HTML šablonu ze složky templates a vloží do ní data předaná jako klíčové parametry (důležité je stejné pojmenování klíče a proměnné v šabloně).

from flask import Flask, render_template

app = Flask(__name__)


@app.route('/')
def home():
    # Předáme data šabloně
    return render_template('index.html', title="Hlavní stránka", message="Vítejte na naší stránce!")


@app.route('/about')
def about():
    return render_template('about.html', title="O nás", content="Toto je stránka o nás.")

    
if __name__ == '__main__':
    app.run(debug=True)

Dědičnost šablon

V předchozím příkladu v obou stránkách opakujeme hlavičku HTML, což lze zjednodušit pomocí dědičnosti. Vytvoříme základní šablonu base.html:

<!DOCTYPE html>
<html lang="cs">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{ title }}</title>
</head>
<body>
    <!-- navigace -->
    <nav>
        <a href="/">Domů</a>
        <a href="/about">O nás</a>
    </nav>
    <hr>

    <!-- hlavní obsah -->
    <div>
        {% block content %}
        <!-- Obsah jednotlivých stránek se vloží sem -->
        {% endblock %}
    </div>

    <hr>
    <!-- patička -->
    <footer>
        <p>2024 Moje Aplikace</p>
    </footer>
</body>
</html>

Předchozí šablonu index.html nyní můžeme zjednodušit, tím že zdědíme šablonu base.html:

{% extends "base.html" %}

{% block content %}
<h1>Vítejte na hlavní stránce!</h1>
<p>Toto je úvodní stránka naší aplikace.</p>
{% endblock %}

Cykly

Pokud proměnná, kterou vkládáme do šablony, je seznam (v příkladu je to proměnná products), můžeme přes nej v šabloně přímo iterovat. V cyklu jsou k dizpicici tyto užitečné proměnné:

<ul>
    {% for product in products %}
        <li>
            <a href="{{ url_for('product_detail', product_id=loop.index0) }}">
                {{ products[loop.index0] }}
            </a>
        </li>
    {% endfor %}
</ul>

V této šabloně jsou odkazy v seznamu parametrizované indexem položky, čehož se využívá níže ve funkci product_detail().

from flask import Flask, render_template, url_for

app = Flask(__name__)
# seznam produktů
products = ["Produkt A", "Produkt B", "Produkt C"]


@app.route('/')
def home():
    return render_template('index.html', title="Seznam produktů", products=products)


@app.route('/product/<int:product_id>')
def product_detail(product_id):
    return f"Detail produktu {products[product_id]} s ID: {product_id}"


if __name__ == '__main__':
    app.run(debug=True)

Podmínky

V šablonách můžeme použít i operátor if-elif-else pro podmíněné vyhodnocení. Podmínky lze navíc umístit v podstatě kamkoliv, takže nemusí generovat celé HTML tagy, ale například jenom hodnotu atributu.

<!-- příklad podmíněného výpisu seznamu v proměnné items -->
<h2>Seznam položek:</h2>
<ul>
    <!-- pokud jsou items neprázdný seznam -->
    {% if items %}
        {% for item in items %}
            <li>{{ item }}</li>
        {% endfor %}
    <!-- pokud jsou items prázdné -->
    {% else %}
        <p>Seznam je prázdný.</p>
    {% endif %}
</ul>

Složené podmínky lze vytvářet pomocí logických operátorů and, or a not.

<!-- 
    příklad podmíněného vyhodnocení pro user = {"name": "Petr", "logged_in": True, "admin": True} 
    k hodnotám slovníku lze v šabloně přistupovat přes tečkovou notaci
-->
{% if user.logged_in and user.admin %}
    <p>Máte administrátorský přístup.</p>
{% elif user.logged_in %}
    <p>Jste přihlášený, ale nemáte administrátorský přístup.</p>
{% else %}
    <p>Jste nepřihlášený. <a href="/login">Přihlásit se</a></p>
{% endif %}

Filtry

Filtry slouží k úpravě nebo formátování dat přímo v šablonách. Představují jednoduchý způsob, jak manipulovat s hodnotami, aniž by bylo nutné měnit samotný Python kód. Filtry se aplikují pomocí operátoru |.

V Jinja2 je k dispozici celá řada předdefivaných základních filtrů. Celý seznam naleznete zde.

<!-- pro string = 'hello world!' se zobrazí: 'Hello World!' -->  
<p>Jméno: {{ data.name|title }}</p>
<!-- pro string = 'Hello world!' se zobrazí: 'Hello' -->  
<p>Popis: {{ string|truncate(5) }}</p>
<!-- pro price = 1234.5678 se zobrazí: 1234.56 -->  
<p>Cena: {{ price|round(2) }} Kč</p>
<!-- pro tags = ['python', 'flask', 'jinja'] se zobrazí: 'python, flask, jinja' -->  
<p>Tagy: {{ tags|join(", ") }}</p>

Vlastní filtr můžeme definovat pomocí dekorátoru @app.template_filter().

@app.template_filter('reverse')
def reverse_string(value):
    return value[::-1]


@app.template_filter('currency')
def format_currency(value):
    return f"{value:,.2f} Kč"

Způsob aplikace vlastního filtru je stejný jako u těch předdefinovaných.

<!-- pro string = 'Hello world!' se zobrazí: '!dlrow olleH' -->
<p>Pozdrav: {{ string|reverse }}</p> 
<!-- pro price = 12345.678 se zobrazí: 'Cena: 12,345.68 Kč' -->
<p>Cena: {{ price|currency }}</p>