Někdy se hodí míti webové stránky dynamické v tom smyslu, že mohou na dotazy uživatele reagovat nějakými vlastními výpočty nebo dotazy někam jinam. A především – čím dál tím častěji se hodí psát GUI programu přímo webové a neotravovat se s portací nativního na různé platformy (web je totiž všude, jak se ostatně předpokládalo již před dvaceti lety).
Tudíž my se nyní podíváme na základy webové komunikace a webových aplikací, a to pochopitelně pěkně z pohodlí programovacího jazyka Python, konkrétně knihovny Bottle (jejíž dokumentaci se v následujících stránkách pokusím vycucnout o to nejzajímavější).
PS: V této přednášce se pokusím slepit dohromady slušnou hromadu pojmů a technologií, takže se nebudu zdržovat moc velkými podrobnostmi a soustředím se především na vlastní komunikaci mezi klientem (vámi, resp. vaším prohlížečem) a serverem (svým způsobem webovou stránkou, na kterou se zrovna díváte, a tím vším za ní, co už není vidět) a odpovědí, kterou server na váš dotaz vyplodí.
Když se třebas ve svém mobilu rozhodnete podívat na nějakou stránku, stanou se zrychleně řečeno přibližně následující věci (a v tomto pořadí):
Abyste se mohli serveru na něco zeptat, musíte pro něj vygenerovat správný HTTP-dotaz. To se sice dá i ručně, ale mnohem jednodušší je to nechat na příslušném klientovi. Snad úplně nejjednodušeji se dotaz vytváří z prostředí webového prohlížeče pomocí tzv. odkazů, tedy HTML-elementů <a href="adresa">
:
Když se na tento soubor podíváte svým prohlížečem (viz výstup na předchozím slajdu), uvidíte (s největší pravděpodobností) modré podtržené lomítko (což je ten text mezi otvírací a koncovou značkou <a>TEXT</a>
). Důležité však je, že když na něj kliknete myší (nebo po zafokusování TABem odentrujete z klávesnice), prohlížeč pošle serveru* následující HTTP-dotaz (zjednodušená, leč stále funkční varianta):
http://localhost:8080
(případně http://127.0.0.1:8080
, což vyjde nastejno).
Server na náš požadavek vrátí následující HTTP-odpověď:
Z níž nepřekvapivě prohlížeč zobrazí právě text Zdraví Vás Váš osobní webový server!.
Uvedeným dotazem GET /
jsme požádali server, aby nám vrátil svůj tzv. kořenový dokument. Typicky jím bývá statický soubor index.html v hlavním poskytovaném adresáři, ale může jím být i cokoliv jiného. V případě knihovny Bottle záleží na tom, jak nastavíme přísloušnou tzv. URL-cestu (route) pro obslužnou funkci dotazu:
Zde jsme pomocí příslušného bottlího dekorátoru svázali funkci index() s dotazem na kořen webového serveru, přičemž ten jsme spustili pomocí funkce run() ve výchozím nastavení na adrese http://localhost:8080
.
PS: A protože text je také vlastně svým způsobem HTML, bohatě stačí, aby nejjednodušší vrácená data od serveru byl obyčejný, nijak formátovaný, řetězec (s českou větou v tomto případě). O všechno další – navázání komunikace, správné HTTP-hlavičky, vyřešení kódování přenášených textů… – se už server i klient postarají sami.
Webový server z předchozího příkladu je tak jednoduchý, že je postavený nad knihovnou Bottle. Ta není součástí Python'u, takže ji musíte nejdříve nainstalovat:
Na to, co všechno umí, je instalace extrémně maličká – pouhý jeden pythoní soubor o velikosti necelých 150 kilobajtů!
Vzhledem ke své „velikosti“ se Bottle vyloženě nabízí k tomu, aby se ani neinstaloval – příslušný soubor stačí prostě vzít a nakopírovat ho ke svému projektu. Díky pythoní implementaci modulů už vše ostatní bude fungovat „samo od sebe“.
Dá se říct, že knihovna Bottle je založena právě na velmi snadném zadávání cest v adrese a jejich provázáním s funkcemi určenými pro jejich zpracování. Přidání další obsloužené adresy je tak otázka připsání jedné funkce s příslušným dekorátorem:
Tento server již umí kromě dotazu http://localhost:8080
reagovat i na dotaz http://localhost:8080/pozdrav
.
http://localhost:8080/pozdrav/
(s lomítkem na konci)! To jde sice zařídit snadno, ale z praktických důvodů se na to podíváme až za chvíli.
Vzhledem k tomu, že jsou „routy“ zaváděny pomocí dekorátorů, možná nepřekvapí, že je možné aplikovat jich na jednu funkci vícero:
Ve výsledku libovolný z uvedených dotazů bude zpracován příslušnou funkcí. Jde to samozřejmě napsat i lépe (zvlášť ten první dotaz na kořen), jak za chvilku uvidíme, ale pro ukázku principu to snad prozatím stačí.
PS: Samozřejmě si to chce dát pozor, aby příslušné zřetězení mělo rozumný smysl, jinak zmatete jak návštěvníky, tak sebe.
Routy v Bottle toho však umí mnohem více. Včetně kupříkladu následujícího „zneužití“ cesty pro předávání parametrů webové aplikaci:
http://localhost:8080/pozdrav/
nebo http://localhost:8080/pozdrav/Alisa
.
Zde jsem využil bottlí možnosti mapovat pojmenovanou část cesty <jmeno>
(vyznačenou špičatými závorkami) na stejnojmenný parametr obslužné funkce.
/
součástí routy není, ta slouží „pouze“ k zachycení a pojmenování příslušné části cesty v adrese. Jinak jich samozřejmě můžeme použít víc, jak ukazuje následující příklad.
Když zkusíme přepsat příklad na rozparsování URL z CherryPy..
..do Bottle, dostaneme kupříkladu toto:
Výstup pro dotaz výše je pak:
PS: Všechny tři části adresy musí být přítomny, jinak server nahlásí chybu – funkce player() díky dekorátoru nic jiného než úplný dotaz zpracovat neumí a žádnou jinou v programu nemáme.
Dynamické routy v Bottle toho však umí mnohem více. Jejich plná forma totiž jest <name:filter:config>
, což znamená, že kromě jména můžete na zachycenou část cesty poštvat i nějakou další logiku. Kromě toho, že si filtry můžete psát i vlastní, je jich několik předpřipraveno:
:int |
zachytí řetězec představující celé číslo a převede ho na typ int |
---|---|
:float |
zachytí řetězec představující reálné číslo a převede ho na typ float |
:path |
ve spolupráci s ostatními dynamickými částmi routy zachytí nechamtivě libovolný dostupný úsek cesty v adrese, tzn. včetně případných lomítek |
:re[:exp] |
zachytí část cesty odpovídající pythonímu regulárnímu výrazu zadanému jako parametr :exp |
V dalším si ukážeme několik příkladů jejich použití.
Půjčme si pro ukázku další příklad na rozparsování URL z CherryPy:
Jeho překlad do Bottle s použitím filtrů by mohl vypadat třebas takto:
Výstup pro dotaz výše je pak:
Pokračujíc z předchozího příkladu, předpokládejme nyní, že nás z celé cesty ke skladbě zajímá pouze skupina a vlastní název písničky:
Pro dotaz z přechozího slajdu..
..pak dostaneme výstup:
Pro vlastní filtr si neinvenčně půjčím příklad z dokumentace:
Uvedený filtr umí nepřekvapivě reagovat na dotazy typu http://localhost:8080/follow/1,2,3,4,5
, přičemž funkce follow_ids() obdrží na vstupu regulérní pythoní seznam [1, 2, 3, 4, 5]
.
Několik poznámek:
Bottle
, proto ono zavedení app = bottle.Bottle()
(více o aplikacích vzápětí);
router.add_filter('JMÉNO_FILTRU', OBSLUHUJÍCÍ_FUNKCE)
;
...
S výjimkou předchozího příkladu jsme zatím vždy používali tzv. výchozí webovou aplikaci frejmworku Bottle. Prakticky to především znamená, že veškeré obslužné funkce patřily do stejné aplikace (a globálního jmenného prostoru).
Jak nám ukázal předchozí příklad, není ve výchozí aplikaci dostupné zdaleko všechno. Ale snad ještě závažnějším důvodem pro používání vlastních aplikací je možnost jejich vzájemné kombinace, tedy především importu do/z pythoních modulů.
Vlastní webovou aplikaci ve frejmworku Bottle zavedeme jako instanci třídy bottle.Bottle
:
V tuto chvíli samozřejmě musíme veškeré odkazy na výchozí aplikaci v našem kódu nahradit odkazy na tuto novou aplikaci vlastní. Kupříkladu základní ukázkový webový server přepsaný z výchozí do vlastní aplikace vypadá takto:
PS: Z praktických důvodů budou všechny následující příklady psány s pomocí vlastních aplikací.
Zdroje na serveru také mohou dostávat parametry. V nejjednodušším případě v podobě specielně formátovaného řetězce přímo v adrese dotazu. Zkusme například náš server naučit reagovat na zadání jména a věku, tedy reagovat například na následující dotazy:
Jak je vidět, uvedený zdroj můžeme samozřejmě zavolat i s prohozenými parametry, neúplnými parametry nebo dokonce úplně bez nich. Avšak v rámci Bottle budou všechny přítomny (ve správně dekódované podobě nebo jako prázdný řetězec, pokud nebyly zadány) pod svým jménem na objektu bottle.request.query
, tedy asi trošku překvapivě na objektu výchozí aplikace a nikoli objektu aplikace dané!
PS: HTTP-dotaz pro první z příkladů vypadá takto (azbuka byla nahrazena svojí pro potřeby URL-dotazu iskejpovanou unicodovou podobou):
Veškeré zatím uvedené dotazy po protokolu HTTP jsou zajištěny tzv. metodou GET
. Příslušné dvojce parametr=hodnota je však možno poslat i jiným způsobem než jen pomocí odkazu, totiž pomocí tzv. HTML-formuláře:
Adresa zdroje je uvedena v atributu @action vlastního formuláře a jména parametrů jsou převzata z atributů @name na příslušných vstupních polích. Příslušný dotaz na server je ve výsledku úplně stejný jako na předchozím slajdu a je obsloužen úplně stejným kódem, takže tady ani jedno neopakuji.
Parametry předané pomocí metody GET
jsou tedy přístupné pod svými jmény v rámci objektu bottle.request.query. Samotný objekt bottle.request odpovídající příslušnému dotazu na server je přitom dosti obsažný:
Většinu z jeho parametrů nebudete pro běžnou práci asi hned tak potřebovat, takže si – navíc k již probranému query – ukážeme jen ty základní.
V rámci metody GET
jsou data dotazu předávána v rámci samotného URL, což má pochopitelně své mouchy a omezení (těžko třeba aploudovat giga dat na server přes adresu). Formuláře však umožňují i jinou variantu jejich zasílání – pomocí metody POST
:
HTTP-dotaz v tomto případě již vypadá úplně jinak:
Parametry jsou sice předávány ve stejné podobě (takže s překódovanou azbukou), ale nyní v těle samotného dotazu, což je mnohem flexibilnější varianta. V rámci frejmworku Bottle jsou takto předané parametry dostupné v rámci objektu bottle.request.forms
.
Asi v duchu výchozího nastavení přednosti metody GET
, dokud není řečeno jinak, odmítne Bottle zpracovat POST-dotaz, dokud ho nepovolíte na příslušné routě pomocí nastavení parametru method="POST"
:
Potom už se však můžete chovat k zaslaným údajům stejně jako u metody GET
, pouze se na ně ptáte na objektu bottle.request.forms
.
Bottle se svým vyšperkovaným systémem routování se tedy umí k různým HTTP-dotazům chovat různým způsobem. Ale než se podíváme na podrobnosti, doplňme si pár informací k formulářům:
get
a nemusí se uvádět.
<input type="hidden" name="PARAMETR" value="HODNOTA">
.
Často – ale rozhodně ne vždy! (jak uvidíme vzápětí) – zastávají metody GET
a POST
stejnou práci a byla by docela otrava muset každou z nich řešit zvlášť. Nepřekvapivě lze v Bottle zařídit jednotný přístup k oběma zdrojům údajů najednou pomocí objektu bottle.request.params
:
Poznámky:
GET
, tak pro POST
nastavením příslušných rout pro obě metody;
query
nebo forms
– jsou zrovna k dispozici).
Kromě řetězení dekorátorů je také možné povolené metody pro příslušnou routu uvést jako seznam:
Pokud ve své aplikaci používáte mix více HTTP-metod, můžete kód zpřehlednit (a zkrátit) použitím příslušných dekorátorových zkratek:
plná routa | zkratka |
---|---|
@route('CESTA') @route('CESTA', method='GET') |
@get('CESTA') |
@route('CESTA', method='POST') |
@post('CESTA') |
@route('CESTA', method='PUT') |
@put('CESTA') |
@route('CESTA', method='DELETE') |
@delete('CESTA') |
@route('CESTA', method='PATCH') |
@patch('CESTA') |
PS: Pro výchozí aplikaci jsou to metody objektu bottle, pro vlastní aplikace jsou to metody příslušné aplikace.
Zatímco v předchozím jsme se snažili veškerou obsluhu dat poslaných z nejrůzněších zdrojů spojit dohromady, jsou nepřekvapivě situace, kdy budete chtít udělat přesný opak. Neinvenčně si pro ukázku drobně upravím příklad z dokumentace:
Poznámky:
/login
, přistupuje na něj pomocí metody GET a bude mu tudíž zobrazen přihlašovací formulář z obslužné funkce login();
@method="post"
probíhá metodou POST, o zpracování zaslaných dat se proto postará druhá obslužná funkce do_login();
PS: Pro routy zde používám odpovídající dekorátorové zkratky. A samozřejmě funkce pro kontrolu přihlášení check_login() by v reálu musela vypadat úplně jinak ^_~
Výše uvedená metoda jednotného přístupu k parametrům pomocí bottle.request.params však nefunguje ve všech případech – pomocí metody POST je totiž také možno zasílat objekty typu soubor, které ale v rámci frejmworku Bottle skončí na objektu request.files:
Když si vyzkoušíte předchozí příklad, zjistíte několik věcí:
bottle.FileUpload
ve slovníku request.files.
V uvedeném příkladu tedy prakticky stačí sledovat hodnotu ve files.soubor
– až bude různá od prázdného řetězce, máme objekt aploudnutého souboru. Který mimo jiné má metodu save(), která se postará o korektní uložení přijatých dat.
save(destination, overwrite=False, chunk_size=65536)
. Přitom především je-li destination adresář, je k němu automaticky přidáno jméno aploudnutého souboru (atribut filename příslušného objektu). Více viz oficiální dokumentace.
Diverzitě v předávání dat však není u knihovny Bottle ještě konec:
application/json
jsou k dispozici na request.json;
Zasílání nedynamických souborů je pochopitelně v rámci Bottle také ošetřeno. O vše se stará metoda bottle.static_file():
Je vřele nedoporučováno používat pro atribut root relativní cesty – trochu jiná konfigurace spuštění serveru (a tím pádem Python'u) a hned budou ukazovat úplně jinam. A ano, uvedená funkce je definována přímo na objektu bottle, nikoli na vlastní aplikaci.
Kromě již zmiňované vhodnosti absolutní cesty pro parametr root, se zmiňme ještě o několika dalších zajímavostech
download=True
použije vlastní název souboru, nastavení na řetězec pak uvedený text.
Bottle samozřejmě umožňuje i vlastní reakci na nejrůznější chyby HTTP-komunikace:
bottle.abort(40x, 'CHYBA')
– klasická chybová stránka (stačí doplnit vlastní HTML-řetězec pro její obsah);
bottle.redirect('URL'[, 30x])
– přesměrování na jinou adresu (ve výchozím nastavení posílá 303 See Other).
Vyrobí-li zpracování dotazu nějakou jinou chybu, projeví se jako 500 Internal Server Error.
Bottle toho umí strašně moc, ale skoro všechno důležité pro základní vývoj jsem si snad už řekli. Přesto však v tutoriálu zbývá ještě několik zajímavostí:
A konečně – pokud Bottle chybí něco, co potřebujete, podporuje i pluginy. A můžete si napsat i vlastní ^_~
Při vývoji aplikace se vám mohou hodit následující věci, které Bottle umí:
bottle.debug(True)
(mimo jiné poskytuje „ukecanější“ chybová hlášení);
bottle.run(reloader=True)
python -m bottle --debug --reload --plugin 'utils:DebugPlugin(exc=True)'' test
).
Jen je nezapomeňte vypnout, než vaši aplikaci vypustíte do světa ^_~
Ačkoli toho webový frejmwork Bottle umí skutečně hodně, v praxi ho budete pravděpodobně chtít používat – samozřejmě kromě vývoje, kdy si s ním bohatě vystačíte – především pro jeho extrémně snadné mapování zdrojů (tedy webových adres) na konkrétní pythoní funkce, jež se starají o jejich správu.
Jinak prakticky všechno v něm může být nahrazeno něčím jiným. A to dokonce právě včetně úžasného routovacího systému. Typicky budete asi především nahrazovat webový server, protože je postavený na interní pythoní implementaci, a tudíž je jednovláknový, což není pro nasazení na volně přístupný web zrovna to pravé ořechové (samotný Bottle doporučuje kupříkladu CherryPy).