Na tomto příkladu si vyzkoušíme použít knihovnu SQLAlchemy na práci s databází. Napíšeme si jednoduchý program pro evidenci úkolů. Také si procvičíme práci s knihovnou Click.
Předpokládáme základní znalost Pythonu. Měli byste mít počítač s nainstalovaným interpretem jazyka Python ve verzi aspoň 3.6. Pro začátek si také vytvořte nové virtuální prostředí.
Do tohoto prostředí si nainstalujte knihovny sqlalchemy
a click
.
Pro zjednodušení začneme čtením dat z databáze. Můžete si stáhnout připravená data. Stáhněte si ho do stejného adresáře, ve kterém budete mít samotný program.
Do souboru ukoly.py
si stáhněte tuto základní kostru.
# ukoly.py
from sqlalchemy import create_engine
db = create_engine("sqlite:///ukoly.sqlite")
Funkce create_engine
vytváří spojení s databází ukoly.sqlite
, která je
uložená v aktuálním adresáři. Knihovna sqlalchemy
umí pracovat i s jinými
typy databází než je SQLite
. Ta je ale nejjednodušší, a velice vhodná na
uložení dat, se kterými budeme pracovat.
Momentálně program nic nedělá. Nejprve musíme nadefinovat, jak vlastně naše data vypadají.
Samotnou databází si můžeme přestavit jako několik tabulek, které mají nějak pojmenované sloupce. V našem příkladu budeme potřebovat jedinou tabulku, ale klidně by jich mohlo být víc.
Pro každou tabulku budeme potřebovat třídu (class
), jejíž instance budou
reprezentovat jednotlivé řádky v ní.
Metodu __repr__
používá Python, když potřebuje zobrazit instanci této třídy.
Není určená na výpis pro uživatele, ale pro ladění programu.
from sqlalchemy import create_engine
from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.exc.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
db = create_engine("sqlite:///ukoly.sqlite")
Base = declarative_base()
class Ukol(Base):
# Název tabulky v databází.
__tablename__ = "ukoly"
# Číselný identifikátor úkolu, toto číslo bude jedinečné.
id = Column(Integer, primary_key=True)
# Text úkolu.
text = Column(String)
# Datum a čas zadání úkolu.
zadano = Column(DateTime)
# Datum a čas vyřešení úkolu. Prázdná hodnota znamená nehotový úkol.
vyreseno = Column(DateTime)
def __repr__(self):
return f"<Ukol(text='{self.text}', zadano={self.zadano}, vyreseno={self.vyreseno})>"
Přidejte jeden import a na konec souboru ještě několik řádků. Pokud chceme z databáze vytahovat data, nebo je ukládat, potřebujeme vytvořit ještě jeden objekt. Takzvané sezení (anglicky session) využije dříve vytvořené spojení a umožňuje nám použít všechny nadefinované třídy.
from sqlalchemy.orm import sessionmaker
...
Session = sessionmaker(bind=db)
sezeni = Session()
dotaz = sezeni.query(Ukol)
print(dotaz.all())
Metoda query
vytvoří dotaz, který bude vracet instance třídy Ukol
. Nezadáme
žádné omezení, takže chceme všechny úkoly. Metoda all
na tomto dotazu potom
vrací seznam všech řádků v tabulce, které odpovídají dotazu.
Tento program už půjde spustit a bude vypisovat všechny úkoly. Tento výpis ale
není úplně pěkný. Upravte program tak, aby každý úkol byl na samostatném řádku,
a v hezky čitelném formátu. Jednotlivé objekty mají atributy id
a text
,
které můžeme použít.
Teď program upravíme tak, abychom ho mohli postupně rozšiřovat dalšími příkazy.
Ve finále chceme, aby program fungoval takto:
$ python ukoly.py vypis
[x] 1. dej si čaj
[x] 2. bež do kina
$ python ukoly.py pridej
Nový úkol: Udělej si úkoly
Zadán úkol 3
$ python ukoly.py vyres 3
$ python ukoly.py vypis
[x] 1. dej si čaj
[x] 2. bež do kina
[x] 3. Udělej si úkoly
Nejprve musíme naimportovat knihovnu click
.
Následně označíme hlavní funkci dekorátorem @click.group()
. Tím řekneme, že
to vlastně není příkaz sám o sobě, ale bude to skupina dalších příkazů. Hned si
jeden vytvoříme a necháme ho vypisovat úkoly.
Session = sessionmaker(bind=db)
sezeni = Session()
@click.group()
def ukolnik():
pass
@ukolnik.command()
def vypis():
dotaz = sezeni.query(Ukol)
ukoly = dotaz.all()
for ukol in ukoly:
symbol = "[x]" if ukol.vyreseno else "[ ]"
print(f"{symbol} {ukol.id}. {ukol.text}")
if __name__ == "__main__":
ukolnik()
Výpis by měl pořád vypadat stejně, akorát ho budeme volat trochu jinak.
Přidejte do programu další příkaz. Bude se jmenovat pridej
, a vždy od
uživatele dostane text úkolu, který hned vypíše.
Teď můžeme metodu upravit tak, aby úkol opravdu vytvořila a uložila.
Nejprve musíme vytvořit instanci třídy Ukol
. Tu potom přidáme do našeho
sezení a řekneme databázi, že ji chceme uložit. Aktuální čas dostaneme ze
standardní knihovny, takže nezapomeňte na začátek programu přidat from
datetime import datetime
.
Pokud bychom vytvářeli několik úkolů, můžeme je všechny přidat a teprve potom
jednou zavolat commit
. Pokud bychom na toto poslední volání zapomněli,
záznamy budou časem uloženy taky, ale nebude úplně přímočaré poznat, kde k tomu
dojde. Je lepší najít vhodné místo a commit
zavolat.
Momentálně program funguje celkem dobře, ale vždycky potřebuje, aby na disku existoval soubor s databází. Bylo by hezké, kdyby si dokázal vytvořit prázdnou databázi.
Nejprve přesuneme vytváření sezení do samostatné funkce, kterou zavoláme v každém příkazu. Toto nebude mít vliv na výsledné chování, ale program bude trošku čitelnější a jednodušší na orientaci.
Vytvořte funkci pripoj_se
, která nebude mít žádné argumenty a bude vracet
nové sezení.
K vytvoření prázdné databáze stačí do funkce pripoj_se
přidat jeden řádek:
def pripoj_se():
Base.metadata.create_all(db)
Session = sessionmaker(bind=db)
return Session()
Třída Base
je společný předek všech našich tříd reprezentujících data. My
máme pouze jednu, ale to není na závadu. Nově přidané volání se podívá, jestli
pro každou třídu existuje odpovídající tabulka, a případně ji vytvoří.
Tato funkce není úplně všemocná. Pokud například budeme měnit existující tabulku, s největší pravděpodobností dostaneme chybovou hlášku. Na obecné migrace dat je lepší použít něco sofistikovanějšího, jako třeba knihovnu alembic.
Pojďme přidat poslední chybějící část: označování úkolů za vyřešené. Začneme zase přidáním kostry příkazu, která dostane číslo úkolu a vypíše ho na výstup.
Postup pro vyřešení úkolu bude následovný: najdeme úkol podle čísla, nastavíme mu čas vyřešení a uložíme ho.
Metodu query
pro vytvoření dotazu už známe. Tentokrát ovšem místo všech úkolů
chceme najít jeden konkrétní. K tomu použijeme filter_by
, která přes
pojmenované argumenty umí vyfiltrovat pouze některé řádky.
Pro vykonání dotazu existuje kromě nám už známé all()
několik metod:
all
vrací všechny výsledky jako seznamfirst
vrací první výsledek, další ignorujeone
zkontroluje, že máme právě jeden výsledek, a vrátí ho. Pokud by jich
byl jiný počet, vyhodí výjimku.one_or_none
se chová podobně, ale místo výjimky vrací None
scalar
očekává ve výsledku jeden řádek s jediným sloupcem, a vrací přímo
hodnotu z tohoto jediného pole@ukolnik.command()
@click.argument("cislo_ukolu", type=click.INT)
def vyres(cislo_ukolu):
sezeni = pripoj_se()
dotaz = sezeni.query(Ukol)
ukol = dotaz.filter_by(id=cislo_ukolu).one()
ukol.vyreseno = datetime.now()
sezeni.add(ukol)
sezeni.commit()
Mohli bychom použít metodu get(cislo_ukolu)
, která najde úkol podle klíče.
To bychom si ale neprocvičili filtrování výsledků dotazu.
Filtrování můžeme aplikovat i pro výpis úkolů. Například bychom mohli vypisovat jenom úkoly, které ještě nejsou dokončené.
Na to se nám může hodit metoda filter
, která umožňuje více porovnání než
známá filter_by
.
@ukolnik.command()
@click.option("--jen-nehotove", default=False, is_flag=True)
def vypis(jen_nehotove):
sezeni = pripoj_se()
dotaz = sezeni.query(Ukol)
if jen_nehotove:
dotaz = dotaz.filter(Ukol.vyreseno == None)
ukoly = dotaz.all()
for ukol in ukoly:
symbol = "[x]" if ukol.vyreseno else "[ ]"
print(f"{symbol} {ukol.id}. {ukol.text}")
Tady je několik tipů, co by se v tomto programu dalo vylepšit:
order_by()
, které můžeme zadat
sloupec, podle kterého se bude řadit. Také můžeme řadit v opačném pořadí,
třeba pomocí Ukol.zadano.desc()
.delete()
, která smaže všechny odpovídající záznamy.{ "data": { "sessionMaterial": { "id": "session-material:2019/brno-jaro-knihovny:sqlalchemy:0", "title": "To-Do List", "html": "\n \n \n\n <h1>To-Do List</h1>\n<h2>Co je cílem tohoto cvičení?</h2>\n<p>Na tomto příkladu si vyzkoušíme použít knihovnu SQLAlchemy na práci s databází.\nNapíšeme si jednoduchý program pro evidenci úkolů. Také si procvičíme práci s\nknihovnou Click.</p>\n<h2>Předpoklady</h2>\n<p>Předpokládáme základní znalost Pythonu. Měli byste mít počítač s nainstalovaným\ninterpretem jazyka Python ve verzi aspoň 3.6. Pro začátek si také vytvořte nové\nvirtuální prostředí.</p>\n<p>Do tohoto prostředí si nainstalujte knihovny <code>sqlalchemy</code> a <code>click</code>.</p>\n<h2>Krok 1 – připojení k databázi</h2>\n<p>Pro zjednodušení začneme čtením dat z databáze. Můžete si stáhnout\n<a href=\"/2019/brno-jaro-knihovny/beginners/todo-list/static/ukoly.sqlite\">připravená data</a>. Stáhněte si ho do\nstejného adresáře, ve kterém budete mít samotný program.</p>\n<p>Do souboru <code>ukoly.py</code> si stáhněte tuto základní kostru.</p>\n<div class=\"highlight\"><pre><span></span><span class=\"c1\"># ukoly.py</span>\n<span class=\"kn\">from</span> <span class=\"nn\">sqlalchemy</span> <span class=\"kn\">import</span> <span class=\"n\">create_engine</span>\n\n\n<span class=\"n\">db</span> <span class=\"o\">=</span> <span class=\"n\">create_engine</span><span class=\"p\">(</span><span class=\"s2\">"sqlite:///ukoly.sqlite"</span><span class=\"p\">)</span>\n</pre></div><p>Funkce <code>create_engine</code> vytváří spojení s databází <code>ukoly.sqlite</code>, která je\nuložená v aktuálním adresáři. Knihovna <code>sqlalchemy</code> umí pracovat i s jinými\ntypy databází než je <code>SQLite</code>. Ta je ale nejjednodušší, a velice vhodná na\nuložení dat, se kterými budeme pracovat.</p>\n<p>Momentálně program nic nedělá. Nejprve musíme nadefinovat, jak vlastně naše\ndata vypadají.</p>\n<h2>Krok 2 – první dotaz</h2>\n<p>Samotnou databází si můžeme přestavit jako několik tabulek, které mají nějak\npojmenované sloupce. V našem příkladu budeme potřebovat jedinou tabulku, ale\nklidně by jich mohlo být víc.</p>\n<p>Pro každou tabulku budeme potřebovat třídu (<code>class</code>), jejíž instance budou\nreprezentovat jednotlivé řádky v ní.</p>\n<p>Metodu <code>__repr__</code> používá Python, když potřebuje zobrazit instanci této třídy.\nNení určená na výpis pro uživatele, ale pro ladění programu.</p>\n<div class=\"highlight\"><pre><span></span><span class=\"kn\">from</span> <span class=\"nn\">sqlalchemy</span> <span class=\"kn\">import</span> <span class=\"n\">create_engine</span>\n<span class=\"kn\">from</span> <span class=\"nn\">sqlalchemy</span> <span class=\"kn\">import</span> <span class=\"n\">Column</span><span class=\"p\">,</span> <span class=\"n\">Integer</span><span class=\"p\">,</span> <span class=\"n\">String</span><span class=\"p\">,</span> <span class=\"n\">DateTime</span>\n<span class=\"kn\">from</span> <span class=\"nn\">sqlalchemy.exc.declarative</span> <span class=\"kn\">import</span> <span class=\"n\">declarative_base</span>\n<span class=\"kn\">from</span> <span class=\"nn\">sqlalchemy.orm</span> <span class=\"kn\">import</span> <span class=\"n\">sessionmaker</span>\n\n\n<span class=\"n\">db</span> <span class=\"o\">=</span> <span class=\"n\">create_engine</span><span class=\"p\">(</span><span class=\"s2\">"sqlite:///ukoly.sqlite"</span><span class=\"p\">)</span>\n<span class=\"n\">Base</span> <span class=\"o\">=</span> <span class=\"n\">declarative_base</span><span class=\"p\">()</span>\n\n\n<span class=\"k\">class</span> <span class=\"nc\">Ukol</span><span class=\"p\">(</span><span class=\"n\">Base</span><span class=\"p\">):</span>\n <span class=\"c1\"># Název tabulky v databází.</span>\n <span class=\"n\">__tablename__</span> <span class=\"o\">=</span> <span class=\"s2\">"ukoly"</span>\n\n <span class=\"c1\"># Číselný identifikátor úkolu, toto číslo bude jedinečné.</span>\n <span class=\"nb\">id</span> <span class=\"o\">=</span> <span class=\"n\">Column</span><span class=\"p\">(</span><span class=\"n\">Integer</span><span class=\"p\">,</span> <span class=\"n\">primary_key</span><span class=\"o\">=</span><span class=\"bp\">True</span><span class=\"p\">)</span>\n <span class=\"c1\"># Text úkolu.</span>\n <span class=\"n\">text</span> <span class=\"o\">=</span> <span class=\"n\">Column</span><span class=\"p\">(</span><span class=\"n\">String</span><span class=\"p\">)</span>\n <span class=\"c1\"># Datum a čas zadání úkolu.</span>\n <span class=\"n\">zadano</span> <span class=\"o\">=</span> <span class=\"n\">Column</span><span class=\"p\">(</span><span class=\"n\">DateTime</span><span class=\"p\">)</span>\n <span class=\"c1\"># Datum a čas vyřešení úkolu. Prázdná hodnota znamená nehotový úkol.</span>\n <span class=\"n\">vyreseno</span> <span class=\"o\">=</span> <span class=\"n\">Column</span><span class=\"p\">(</span><span class=\"n\">DateTime</span><span class=\"p\">)</span>\n\n <span class=\"k\">def</span> <span class=\"fm\">__repr__</span><span class=\"p\">(</span><span class=\"bp\">self</span><span class=\"p\">):</span>\n <span class=\"k\">return</span> <span class=\"n\">f</span><span class=\"s2\">"<Ukol(text='{self.text}', zadano={self.zadano}, vyreseno={self.vyreseno})>"</span>\n</pre></div><p>Přidejte jeden import a na konec souboru ještě několik řádků. Pokud chceme z\ndatabáze vytahovat data, nebo je ukládat, potřebujeme vytvořit ještě jeden\nobjekt. Takzvané sezení (anglicky <em>session</em>) využije dříve vytvořené spojení a\numožňuje nám použít všechny nadefinované třídy.</p>\n<div class=\"highlight\"><pre><span></span><span class=\"kn\">from</span> <span class=\"nn\">sqlalchemy.orm</span> <span class=\"kn\">import</span> <span class=\"n\">sessionmaker</span>\n\n<span class=\"o\">...</span>\n\n<span class=\"n\">Session</span> <span class=\"o\">=</span> <span class=\"n\">sessionmaker</span><span class=\"p\">(</span><span class=\"n\">bind</span><span class=\"o\">=</span><span class=\"n\">db</span><span class=\"p\">)</span>\n<span class=\"n\">sezeni</span> <span class=\"o\">=</span> <span class=\"n\">Session</span><span class=\"p\">()</span>\n\n<span class=\"n\">dotaz</span> <span class=\"o\">=</span> <span class=\"n\">sezeni</span><span class=\"o\">.</span><span class=\"n\">query</span><span class=\"p\">(</span><span class=\"n\">Ukol</span><span class=\"p\">)</span>\n<span class=\"k\">print</span><span class=\"p\">(</span><span class=\"n\">dotaz</span><span class=\"o\">.</span><span class=\"n\">all</span><span class=\"p\">())</span>\n</pre></div><p>Metoda <code>query</code> vytvoří dotaz, který bude vracet instance třídy <code>Ukol</code>. Nezadáme\nžádné omezení, takže chceme všechny úkoly. Metoda <code>all</code> na tomto dotazu potom\nvrací seznam všech řádků v tabulce, které odpovídají dotazu.</p>\n<p>Tento program už půjde spustit a bude vypisovat všechny úkoly. Tento výpis ale\nnení úplně pěkný. Upravte program tak, aby každý úkol byl na samostatném řádku,\na v hezky čitelném formátu. Jednotlivé objekty mají atributy <code>id</code> a <code>text</code>,\nkteré můžeme použít.</p>\n<div class=\"solution\" id=\"solution-0\">\n <h3>Řešení</h3>\n <div class=\"solution-cover\">\n <a href=\"/2019/brno-jaro-knihovny/beginners/todo-list/index/solutions/0/\"><span class=\"link-text\">Ukázat řešení</span></a>\n </div>\n <div class=\"solution-body\" aria-hidden=\"true\">\n <div class=\"highlight\"><pre><span></span><span class=\"o\">...</span>\n\n<span class=\"n\">ukoly</span> <span class=\"o\">=</span> <span class=\"n\">dotaz</span><span class=\"o\">.</span><span class=\"n\">all</span><span class=\"p\">()</span>\n<span class=\"k\">for</span> <span class=\"n\">ukol</span> <span class=\"ow\">in</span> <span class=\"n\">ukoly</span><span class=\"p\">:</span>\n <span class=\"n\">symbol</span> <span class=\"o\">=</span> <span class=\"s2\">"[x]"</span> <span class=\"k\">if</span> <span class=\"n\">ukol</span><span class=\"o\">.</span><span class=\"n\">vyreseno</span> <span class=\"k\">else</span> <span class=\"s2\">"[ ]"</span>\n <span class=\"k\">print</span><span class=\"p\">(</span><span class=\"n\">f</span><span class=\"s2\">"{symbol} {ukol.id}. {ukol.text}"</span><span class=\"p\">)</span>\n</pre></div>\n </div>\n</div><h2>Krok 3 – uživatelské rozhraní</h2>\n<p>Teď program upravíme tak, abychom ho mohli postupně rozšiřovat dalšími příkazy.</p>\n<p>Ve finále chceme, aby program fungoval takto:</p>\n<div class=\"highlight\"><pre><span></span><span class=\"gp\">$ </span>python ukoly.py vypis\n<span class=\"go\">[x] 1. dej si čaj</span>\n<span class=\"go\">[x] 2. bež do kina</span>\n<span class=\"gp\">$ </span>python ukoly.py pridej\n<span class=\"go\">Nový úkol: Udělej si úkoly</span>\n<span class=\"go\">Zadán úkol 3</span>\n<span class=\"gp\">$ </span>python ukoly.py vyres <span class=\"m\">3</span>\n<span class=\"gp\">$ </span>python ukoly.py vypis\n<span class=\"go\">[x] 1. dej si čaj</span>\n<span class=\"go\">[x] 2. bež do kina</span>\n<span class=\"go\">[x] 3. Udělej si úkoly</span>\n</pre></div><p>Nejprve musíme naimportovat knihovnu <code>click</code>.</p>\n<p>Následně označíme hlavní funkci dekorátorem <code>@click.group()</code>. Tím řekneme, že\nto vlastně není příkaz sám o sobě, ale bude to skupina dalších příkazů. Hned si\njeden vytvoříme a necháme ho vypisovat úkoly.</p>\n<div class=\"highlight\"><pre><span></span><span class=\"n\">Session</span> <span class=\"o\">=</span> <span class=\"n\">sessionmaker</span><span class=\"p\">(</span><span class=\"n\">bind</span><span class=\"o\">=</span><span class=\"n\">db</span><span class=\"p\">)</span>\n<span class=\"n\">sezeni</span> <span class=\"o\">=</span> <span class=\"n\">Session</span><span class=\"p\">()</span>\n\n\n<span class=\"nd\">@click.group</span><span class=\"p\">()</span>\n<span class=\"k\">def</span> <span class=\"nf\">ukolnik</span><span class=\"p\">():</span>\n <span class=\"k\">pass</span>\n\n\n<span class=\"nd\">@ukolnik.command</span><span class=\"p\">()</span>\n<span class=\"k\">def</span> <span class=\"nf\">vypis</span><span class=\"p\">():</span>\n <span class=\"n\">dotaz</span> <span class=\"o\">=</span> <span class=\"n\">sezeni</span><span class=\"o\">.</span><span class=\"n\">query</span><span class=\"p\">(</span><span class=\"n\">Ukol</span><span class=\"p\">)</span>\n <span class=\"n\">ukoly</span> <span class=\"o\">=</span> <span class=\"n\">dotaz</span><span class=\"o\">.</span><span class=\"n\">all</span><span class=\"p\">()</span>\n <span class=\"k\">for</span> <span class=\"n\">ukol</span> <span class=\"ow\">in</span> <span class=\"n\">ukoly</span><span class=\"p\">:</span>\n <span class=\"n\">symbol</span> <span class=\"o\">=</span> <span class=\"s2\">"[x]"</span> <span class=\"k\">if</span> <span class=\"n\">ukol</span><span class=\"o\">.</span><span class=\"n\">vyreseno</span> <span class=\"k\">else</span> <span class=\"s2\">"[ ]"</span>\n <span class=\"k\">print</span><span class=\"p\">(</span><span class=\"n\">f</span><span class=\"s2\">"{symbol} {ukol.id}. {ukol.text}"</span><span class=\"p\">)</span>\n\n\n<span class=\"k\">if</span> <span class=\"vm\">__name__</span> <span class=\"o\">==</span> <span class=\"s2\">"__main__"</span><span class=\"p\">:</span>\n <span class=\"n\">ukolnik</span><span class=\"p\">()</span>\n</pre></div><p>Výpis by měl pořád vypadat stejně, akorát ho budeme volat trochu jinak.</p>\n<h2>Krok 4 – přidávání úkolů</h2>\n<p>Přidejte do programu další příkaz. Bude se jmenovat <code>pridej</code>, a vždy od\nuživatele dostane text úkolu, který hned vypíše.</p>\n<div class=\"solution\" id=\"solution-1\">\n <h3>Řešení</h3>\n <div class=\"solution-cover\">\n <a href=\"/2019/brno-jaro-knihovny/beginners/todo-list/index/solutions/1/\"><span class=\"link-text\">Ukázat řešení</span></a>\n </div>\n <div class=\"solution-body\" aria-hidden=\"true\">\n <div class=\"highlight\"><pre><span></span><span class=\"nd\">@ukolnik.command</span><span class=\"p\">()</span>\n<span class=\"nd\">@click.option</span><span class=\"p\">(</span><span class=\"s2\">"--zadani"</span><span class=\"p\">,</span> <span class=\"n\">prompt</span><span class=\"o\">=</span><span class=\"s2\">"Nový úkol"</span><span class=\"p\">)</span>\n<span class=\"k\">def</span> <span class=\"nf\">pridej</span><span class=\"p\">(</span><span class=\"n\">zadani</span><span class=\"p\">):</span>\n <span class=\"k\">print</span><span class=\"p\">(</span><span class=\"n\">f</span><span class=\"s2\">"OK: {zadani}"</span><span class=\"p\">)</span>\n</pre></div>\n </div>\n</div><p>Teď můžeme metodu upravit tak, aby úkol opravdu vytvořila a uložila.</p>\n<p>Nejprve musíme vytvořit instanci třídy <code>Ukol</code>. Tu potom přidáme do našeho\nsezení a řekneme databázi, že ji chceme uložit. Aktuální čas dostaneme ze\nstandardní knihovny, takže nezapomeňte na začátek programu přidat <code>from\ndatetime import datetime</code>.</p>\n<div class=\"solution\" id=\"solution-2\">\n <h3>Řešení</h3>\n <div class=\"solution-cover\">\n <a href=\"/2019/brno-jaro-knihovny/beginners/todo-list/index/solutions/2/\"><span class=\"link-text\">Ukázat řešení</span></a>\n </div>\n <div class=\"solution-body\" aria-hidden=\"true\">\n <div class=\"highlight\"><pre><span></span><span class=\"nd\">@ukolnik.command</span><span class=\"p\">()</span>\n<span class=\"nd\">@click.option</span><span class=\"p\">(</span><span class=\"s2\">"--zadani"</span><span class=\"p\">,</span> <span class=\"n\">prompt</span><span class=\"o\">=</span><span class=\"s2\">"Nový úkol"</span><span class=\"p\">)</span>\n<span class=\"k\">def</span> <span class=\"nf\">pridej</span><span class=\"p\">(</span><span class=\"n\">zadani</span><span class=\"p\">):</span>\n <span class=\"n\">ukol</span> <span class=\"o\">=</span> <span class=\"n\">Ukol</span><span class=\"p\">(</span><span class=\"n\">text</span><span class=\"o\">=</span><span class=\"n\">zadani</span><span class=\"p\">,</span> <span class=\"n\">zadano</span><span class=\"o\">=</span><span class=\"n\">datetime</span><span class=\"o\">.</span><span class=\"n\">now</span><span class=\"p\">())</span>\n <span class=\"n\">sezeni</span><span class=\"o\">.</span><span class=\"n\">add</span><span class=\"p\">(</span><span class=\"n\">ukol</span><span class=\"p\">)</span>\n <span class=\"n\">sezeni</span><span class=\"o\">.</span><span class=\"n\">commit</span><span class=\"p\">()</span>\n</pre></div>\n </div>\n</div><p>Pokud bychom vytvářeli několik úkolů, můžeme je všechny přidat a teprve potom\njednou zavolat <code>commit</code>. Pokud bychom na toto poslední volání zapomněli,\nzáznamy budou časem uloženy taky, ale nebude úplně přímočaré poznat, kde k tomu\ndojde. Je lepší najít vhodné místo a <code>commit</code> zavolat.</p>\n<h2>Krok 5 – vytvoření databáze</h2>\n<p>Momentálně program funguje celkem dobře, ale vždycky potřebuje, aby na disku\nexistoval soubor s databází. Bylo by hezké, kdyby si dokázal vytvořit prázdnou\ndatabázi.</p>\n<p>Nejprve přesuneme vytváření sezení do samostatné funkce, kterou zavoláme v\nkaždém příkazu. Toto nebude mít vliv na výsledné chování, ale program bude\ntrošku čitelnější a jednodušší na orientaci.</p>\n<p>Vytvořte funkci <code>pripoj_se</code>, která nebude mít žádné argumenty a bude vracet\nnové sezení.</p>\n<div class=\"solution\" id=\"solution-3\">\n <h3>Řešení</h3>\n <div class=\"solution-cover\">\n <a href=\"/2019/brno-jaro-knihovny/beginners/todo-list/index/solutions/3/\"><span class=\"link-text\">Ukázat řešení</span></a>\n </div>\n <div class=\"solution-body\" aria-hidden=\"true\">\n <div class=\"highlight\"><pre><span></span><span class=\"o\">...</span>\n\n<span class=\"k\">def</span> <span class=\"nf\">pripoj_se</span><span class=\"p\">():</span>\n <span class=\"n\">Session</span> <span class=\"o\">=</span> <span class=\"n\">sessionmaker</span><span class=\"p\">(</span><span class=\"n\">bind</span><span class=\"o\">=</span><span class=\"n\">db</span><span class=\"p\">)</span>\n <span class=\"k\">return</span> <span class=\"n\">Session</span><span class=\"p\">()</span>\n\n\n<span class=\"nd\">@click.group</span><span class=\"p\">()</span>\n<span class=\"k\">def</span> <span class=\"nf\">ukolnik</span><span class=\"p\">():</span>\n <span class=\"k\">pass</span>\n\n\n<span class=\"nd\">@ukolnik.command</span><span class=\"p\">()</span>\n<span class=\"k\">def</span> <span class=\"nf\">vypis</span><span class=\"p\">():</span>\n <span class=\"n\">sezeni</span> <span class=\"o\">=</span> <span class=\"n\">pripoj_se</span><span class=\"p\">()</span>\n <span class=\"o\">...</span>\n\n\n<span class=\"nd\">@ukolnik.command</span><span class=\"p\">()</span>\n<span class=\"nd\">@click.option</span><span class=\"p\">(</span><span class=\"s2\">"--zadani"</span><span class=\"p\">,</span> <span class=\"n\">prompt</span><span class=\"o\">=</span><span class=\"s2\">"Nový úkol"</span><span class=\"p\">)</span>\n<span class=\"k\">def</span> <span class=\"nf\">pridej</span><span class=\"p\">(</span><span class=\"n\">zadani</span><span class=\"p\">):</span>\n <span class=\"n\">sezeni</span> <span class=\"o\">=</span> <span class=\"n\">pripoj_se</span><span class=\"p\">()</span>\n <span class=\"o\">...</span>\n</pre></div>\n </div>\n</div><p>K vytvoření prázdné databáze stačí do funkce <code>pripoj_se</code> přidat jeden řádek:</p>\n<div class=\"highlight\"><pre><span></span><span class=\"k\">def</span> <span class=\"nf\">pripoj_se</span><span class=\"p\">():</span>\n <span class=\"n\">Base</span><span class=\"o\">.</span><span class=\"n\">metadata</span><span class=\"o\">.</span><span class=\"n\">create_all</span><span class=\"p\">(</span><span class=\"n\">db</span><span class=\"p\">)</span>\n <span class=\"n\">Session</span> <span class=\"o\">=</span> <span class=\"n\">sessionmaker</span><span class=\"p\">(</span><span class=\"n\">bind</span><span class=\"o\">=</span><span class=\"n\">db</span><span class=\"p\">)</span>\n <span class=\"k\">return</span> <span class=\"n\">Session</span><span class=\"p\">()</span>\n</pre></div><p>Třída <code>Base</code> je společný předek všech našich tříd reprezentujících data. My\nmáme pouze jednu, ale to není na závadu. Nově přidané volání se podívá, jestli\npro každou třídu existuje odpovídající tabulka, a případně ji vytvoří.</p>\n<p>Tato funkce není úplně všemocná. Pokud například budeme měnit existující\ntabulku, s největší pravděpodobností dostaneme chybovou hlášku. Na obecné\nmigrace dat je lepší použít něco sofistikovanějšího, jako třeba knihovnu\n<a href=\"https://pypi.org/project/alembic/\">alembic</a>.</p>\n<h2>Krok 6 – řešení úkolů</h2>\n<p>Pojďme přidat poslední chybějící část: označování úkolů za vyřešené. Začneme\nzase přidáním kostry příkazu, která dostane číslo úkolu a vypíše ho na výstup.</p>\n<div class=\"solution\" id=\"solution-4\">\n <h3>Řešení</h3>\n <div class=\"solution-cover\">\n <a href=\"/2019/brno-jaro-knihovny/beginners/todo-list/index/solutions/4/\"><span class=\"link-text\">Ukázat řešení</span></a>\n </div>\n <div class=\"solution-body\" aria-hidden=\"true\">\n <div class=\"highlight\"><pre><span></span><span class=\"nd\">@ukolnik.command</span><span class=\"p\">()</span>\n<span class=\"nd\">@click.argument</span><span class=\"p\">(</span><span class=\"s2\">"cislo_ukolu"</span><span class=\"p\">,</span> <span class=\"nb\">type</span><span class=\"o\">=</span><span class=\"n\">click</span><span class=\"o\">.</span><span class=\"n\">INT</span><span class=\"p\">)</span>\n<span class=\"k\">def</span> <span class=\"nf\">vyres</span><span class=\"p\">(</span><span class=\"n\">cislo_ukolu</span><span class=\"p\">):</span>\n <span class=\"k\">print</span><span class=\"p\">(</span><span class=\"n\">f</span><span class=\"s2\">"Značím {cislo_ukolu} jako vyřešené"</span><span class=\"p\">)</span>\n</pre></div>\n </div>\n</div><p>Postup pro vyřešení úkolu bude následovný: najdeme úkol podle čísla, nastavíme\nmu čas vyřešení a uložíme ho.</p>\n<p>Metodu <code>query</code> pro vytvoření dotazu už známe. Tentokrát ovšem místo všech úkolů\nchceme najít jeden konkrétní. K tomu použijeme <code>filter_by</code>, která přes\npojmenované argumenty umí vyfiltrovat pouze některé řádky.</p>\n<p>Pro vykonání dotazu existuje kromě nám už známé <code>all()</code> několik metod:</p>\n<ul>\n<li><code>all</code> vrací všechny výsledky jako seznam</li>\n<li><code>first</code> vrací první výsledek, další ignoruje</li>\n<li><code>one</code> zkontroluje, že máme právě jeden výsledek, a vrátí ho. Pokud by jich\nbyl jiný počet, vyhodí výjimku.</li>\n<li><code>one_or_none</code> se chová podobně, ale místo výjimky vrací <code>None</code></li>\n<li><code>scalar</code> očekává ve výsledku jeden řádek s jediným sloupcem, a vrací přímo\nhodnotu z tohoto jediného pole</li>\n</ul>\n<div class=\"highlight\"><pre><span></span><span class=\"nd\">@ukolnik.command</span><span class=\"p\">()</span>\n<span class=\"nd\">@click.argument</span><span class=\"p\">(</span><span class=\"s2\">"cislo_ukolu"</span><span class=\"p\">,</span> <span class=\"nb\">type</span><span class=\"o\">=</span><span class=\"n\">click</span><span class=\"o\">.</span><span class=\"n\">INT</span><span class=\"p\">)</span>\n<span class=\"k\">def</span> <span class=\"nf\">vyres</span><span class=\"p\">(</span><span class=\"n\">cislo_ukolu</span><span class=\"p\">):</span>\n <span class=\"n\">sezeni</span> <span class=\"o\">=</span> <span class=\"n\">pripoj_se</span><span class=\"p\">()</span>\n <span class=\"n\">dotaz</span> <span class=\"o\">=</span> <span class=\"n\">sezeni</span><span class=\"o\">.</span><span class=\"n\">query</span><span class=\"p\">(</span><span class=\"n\">Ukol</span><span class=\"p\">)</span>\n <span class=\"n\">ukol</span> <span class=\"o\">=</span> <span class=\"n\">dotaz</span><span class=\"o\">.</span><span class=\"n\">filter_by</span><span class=\"p\">(</span><span class=\"nb\">id</span><span class=\"o\">=</span><span class=\"n\">cislo_ukolu</span><span class=\"p\">)</span><span class=\"o\">.</span><span class=\"n\">one</span><span class=\"p\">()</span>\n <span class=\"n\">ukol</span><span class=\"o\">.</span><span class=\"n\">vyreseno</span> <span class=\"o\">=</span> <span class=\"n\">datetime</span><span class=\"o\">.</span><span class=\"n\">now</span><span class=\"p\">()</span>\n <span class=\"n\">sezeni</span><span class=\"o\">.</span><span class=\"n\">add</span><span class=\"p\">(</span><span class=\"n\">ukol</span><span class=\"p\">)</span>\n <span class=\"n\">sezeni</span><span class=\"o\">.</span><span class=\"n\">commit</span><span class=\"p\">()</span>\n</pre></div><div class=\"admonition note\"><p>Mohli bychom použít metodu <code>get(cislo_ukolu)</code>, která najde úkol podle klíče.\nTo bychom si ale neprocvičili filtrování výsledků dotazu.</p>\n</div><h2>Krok 7 – výpis jen nedokončených úkolů</h2>\n<p>Filtrování můžeme aplikovat i pro výpis úkolů. Například bychom mohli vypisovat\njenom úkoly, které ještě nejsou dokončené.</p>\n<p>Na to se nám může hodit metoda <code>filter</code>, která umožňuje více porovnání než\nznámá <code>filter_by</code>.</p>\n<div class=\"highlight\"><pre><span></span><span class=\"nd\">@ukolnik.command</span><span class=\"p\">()</span>\n<span class=\"nd\">@click.option</span><span class=\"p\">(</span><span class=\"s2\">"--jen-nehotove"</span><span class=\"p\">,</span> <span class=\"n\">default</span><span class=\"o\">=</span><span class=\"bp\">False</span><span class=\"p\">,</span> <span class=\"n\">is_flag</span><span class=\"o\">=</span><span class=\"bp\">True</span><span class=\"p\">)</span>\n<span class=\"k\">def</span> <span class=\"nf\">vypis</span><span class=\"p\">(</span><span class=\"n\">jen_nehotove</span><span class=\"p\">):</span>\n <span class=\"n\">sezeni</span> <span class=\"o\">=</span> <span class=\"n\">pripoj_se</span><span class=\"p\">()</span>\n <span class=\"n\">dotaz</span> <span class=\"o\">=</span> <span class=\"n\">sezeni</span><span class=\"o\">.</span><span class=\"n\">query</span><span class=\"p\">(</span><span class=\"n\">Ukol</span><span class=\"p\">)</span>\n\n <span class=\"k\">if</span> <span class=\"n\">jen_nehotove</span><span class=\"p\">:</span>\n <span class=\"n\">dotaz</span> <span class=\"o\">=</span> <span class=\"n\">dotaz</span><span class=\"o\">.</span><span class=\"n\">filter</span><span class=\"p\">(</span><span class=\"n\">Ukol</span><span class=\"o\">.</span><span class=\"n\">vyreseno</span> <span class=\"o\">==</span> <span class=\"bp\">None</span><span class=\"p\">)</span>\n\n <span class=\"n\">ukoly</span> <span class=\"o\">=</span> <span class=\"n\">dotaz</span><span class=\"o\">.</span><span class=\"n\">all</span><span class=\"p\">()</span>\n <span class=\"k\">for</span> <span class=\"n\">ukol</span> <span class=\"ow\">in</span> <span class=\"n\">ukoly</span><span class=\"p\">:</span>\n <span class=\"n\">symbol</span> <span class=\"o\">=</span> <span class=\"s2\">"[x]"</span> <span class=\"k\">if</span> <span class=\"n\">ukol</span><span class=\"o\">.</span><span class=\"n\">vyreseno</span> <span class=\"k\">else</span> <span class=\"s2\">"[ ]"</span>\n <span class=\"k\">print</span><span class=\"p\">(</span><span class=\"n\">f</span><span class=\"s2\">"{symbol} {ukol.id}. {ukol.text}"</span><span class=\"p\">)</span>\n</pre></div><h2>Další vylepšení</h2>\n<p>Tady je několik tipů, co by se v tomto programu dalo vylepšit:</p>\n<ul>\n<li>Ošetření chyb: momentálně program spadne, pokud se pokusíme vyřešit\nneexistující úkol.</li>\n<li>Řazení výpisu: teď jsou úkoly vypsané od nejstaršího. Možná bychom je chtěli\nřadit v opačném pořadí. Dotaz má metodu <code>order_by()</code>, které můžeme zadat\nsloupec, podle kterého se bude řadit. Také můžeme řadit v opačném pořadí,\ntřeba pomocí <code>Ukol.zadano.desc()</code>.</li>\n<li>Mohli bychom přidat další příkaz, který smaže některé úkoly (třeba ty, které\njsou vyřešené, nebo starší než nějaký limit). Dotaz s aplikovanými filtry má\nmetodu <code>delete()</code>, která smaže všechny odpovídající záznamy.</li>\n</ul>\n\n\n " } } }