Obsah
1. Kombinace PyWebIO, Matplotlibu, Pandasu a Minia aneb webová aplikace za půl hodiny
3. Zobrazení grafu s naměřenými daty i regresní přímkou
4. Načtení dat přímo do datového rámce knihovny Pandas
6. Spuštění Minia v režimu serveru
7. Načtení seznamu objektů uložených v Miniu
8. Seznam datových souborů s výsledky měření
10. Přečtení dat uložených ve formátu CSV
11. Zobrazení grafu z dat načtených z Minia
12. Od skriptu k dynamické webové stránce s využitím PyWebIO
13. Prázdná dynamická webová stránka
16. Zobrazení popup okna s daty senzoru
17. Zobrazení popup okna s grafem
19. Repositář s demonstračními příklady
1. Kombinace PyWebIO, Matplotlibu, Pandasu a Minia aneb webová aplikace za půl hodiny
V úvodníku článku o knihovně PyWebIO bylo napsáno: v mnoha situacích potřebuje programátor vytvořit aplikaci s formuláři a dialogy, popř. s grafy, tedy aplikaci s GUI. Pokud používá Python, může využít již popsané knihovny Tkinter, PyObject, PyQt/PySide atd. Alternativně je ovšem možné vytvořit webovou aplikaci, a to čistě v Pythonu: bez HTML, CSS a JavaScriptu. Pro tento účel slouží knihovna PyWebIO.
Dnes si v několika krocích ukážeme vytvoření takové aplikace, přičemž funkční požadavky (odvozené z praxe) jsou ve stručnosti následující:
- Má se zobrazit seznam senzorů, které posílají data do S3 nebo Minia (tedy storage)
- U každého senzoru se zobrazí jeho jméno, datum odeslání dat a dvojice ovládacích prvků „Data“ a „Graf“
- Po výběru ovládacího prvku „Data“ se zobrazí popup okno s tabulkou s daty získanými senzorem (například teploměrem)
- Po výběru ovládacího prvku „Graf“ se zobrazí popup okno s průběhem hodnot a taktéž s regresní přímkou (resp. úsečkou)
Začneme skutečně od začátku – od tohoto primitivního skriptu:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 def main(): pass if __name__ == "__main__": main()
Na konci článku budeme mít prakticky plnohodnotnou webovou aplikaci určenou pro nasazení v intranetu.
2. Formát vstupních dat
Vstupní data získaná jednotlivými senzory jsou uložena ve formátu CSV (Comma Separated Values) a mají jednoduchou strukturu – první sloupec obsahuje časová razítka, druhý sloupec naměřenou hodnotu:
"Time","Value" 2020-12-01 06:21:00,30 2020-12-01 06:21:20,4 2020-12-01 06:21:40,5 2020-12-01 06:22:00,5 2020-12-01 06:22:20,5 2020-12-01 06:22:40,8 2020-12-01 06:23:00,8 2020-12-01 06:23:20,8 2020-12-01 06:23:40,11 2020-12-01 06:24:00,11 2020-12-01 06:24:20,10 2020-12-01 06:24:40,21 2020-12-01 06:25:00,21 2020-12-01 06:25:20,25 2020-12-01 06:25:40,22 2020-12-01 06:26:00,22 2020-12-01 06:26:20,23 2020-12-01 06:26:40,23 2020-12-01 06:27:00,23 2020-12-01 06:27:20,13 2020-12-01 06:27:40,15 2020-12-01 06:28:00,15 2020-12-01 06:28:20,13 2020-12-01 06:28:40,16 2020-12-01 06:29:00,16 2020-12-01 06:29:20,16 2020-12-01 06:29:40,11 2020-12-01 06:30:00,11 2020-12-01 06:30:20,22 2020-12-01 06:30:40,15 2020-12-01 06:31:00,15 2020-12-01 06:31:20,12 2020-12-01 06:31:40,18 2020-12-01 06:32:00,18 2020-12-01 06:32:20,23 2020-12-01 06:32:40,19 2020-12-01 06:33:00,19 2020-12-01 06:33:20,22 2020-12-01 06:33:40,9 2020-12-01 06:34:00,9 2020-12-01 06:34:20,9 2020-12-01 06:34:40,22 2020-12-01 06:35:00,20 2020-12-01 06:35:20,20 2020-12-01 06:35:40,17 2020-12-01 06:36:00,13 2020-12-01 06:36:20,13 2020-12-01 06:36:40,11 2020-12-01 06:37:00,16
Pro každý senzor existuje samostatný datový soubor.
3. Zobrazení grafu s naměřenými daty i regresní přímkou
S problematikou tvorby grafů s využitím knihovny Matplotlib jsme se seznámili například v článku Tvorba grafů v Jupyter Notebooku s využitím knihovny Matplotlib. Ukažme si tedy, jak lze vykreslit graf z hodnot, které jsou přečteny ze souboru CSV a zobrazeny formou základního grafu s průběhem „funkce“. Pro větší zajímavost je do grafu vložena i regresní přímka, která byla z pohledu Matplotlibu popsána zde:
#!/usr/bin/env python3 import sys import csv import numpy as np import matplotlib.pyplot as plt # Check if command line argument is specified (it is mandatory). if len(sys.argv) < 2: print("Usage:") print(" plot_values.py input_file.csv") print("Example:") print(" plot_values.py overall.csv") sys.exit(1) # First command line argument should contain name of input CSV. input_csv = sys.argv[1] # Try to open the CSV file specified. with open(input_csv) as csv_input: # And open this file as CSV csv_reader = csv.reader(csv_input) # Skip header next(csv_reader, None) rows = 0 # Read all rows from the provided CSV file data = [(row[0], int(row[1])) for row in csv_reader] print(data) # Linear regression time = [item[0] for item in data] messages = [item[1] for item in data] # Linear regression x = np.arange(0, len(messages)) coef = np.polyfit(x, messages, 1) poly1d_fn = np.poly1d(coef) # Create new graph plt.plot(messages, "b", poly1d_fn(np.arange(0, len(messages))), "y--") # Title of a graph plt.title("Sensor values") # Add a label to x-axis plt.xlabel("Time") # Add a label to y-axis plt.ylabel("Values") plt.legend(loc="upper right") # Set the plot layout plt.tight_layout() # And save the plot into raster format and vector format as well plt.savefig("graph.png") plt.savefig("graph.svg") # Try to show the plot on screen plt.show()
Výsledek může vypadat následovně:

Obrázek 1: Graf vykreslený předchozím skriptem.
4. Načtení dat přímo do datového rámce knihovny Pandas
Načtení dat lze ve skutečnosti realizovat ještě jednodušeji, a to s využitím knihovny Pandas (ta nám později nabídne i vykreslení tabulky do HTML). Malou úpravou získáme tento skript; samotný graf by měl být pro stejná vstupní data totožný:
#!/usr/bin/env python3 import sys import pandas as pd import numpy as np import matplotlib.pyplot as plt # Check if command line argument is specified (it is mandatory). if len(sys.argv) < 2: print("Usage:") print(" plot_values_pandas.py input_file.csv") print("Example:") print(" plot_values_pandas.py overall.csv") sys.exit(1) # First command line argument should contain name of input CSV. input_csv = sys.argv[1] # Try to open the CSV file specified. df = pd.read_csv(input_csv) # Print info about data frame print(df.info()) print(df.describe()) # Linear regression time = df["Time"] messages = df["Value"] # Linear regression x = np.arange(0, len(messages)) coef = np.polyfit(x, messages, 1) poly1d_fn = np.poly1d(coef) # Create new graph plt.plot(messages, "b", poly1d_fn(np.arange(0, len(messages))), "y--") # Title of a graph plt.title("Sensor values") # Add a label to x-axis plt.xlabel("Time") # Add a label to y-axis plt.ylabel("Values") plt.legend(loc="upper right") # Set the plot layout plt.tight_layout() # And save the plot into raster format and vector format as well plt.savefig("graph.png") plt.savefig("graph.svg") # Try to show the plot on screen plt.show()

Obrázek 2: Graf vykreslený předchozím skriptem.
Aby bylo později možné graf (resp. přesněji řečeno jeho rasterizovanou podobu) uložit do datového bufferu a následně poslat do webové aplikace, budeme muset skript nepatrně upravit – použijeme konstrukci fig, ax = plt.subplots() a skript se tedy změní následovně:
#!/usr/bin/env python3 import sys import pandas as pd import numpy as np import matplotlib.pyplot as plt # Check if command line argument is specified (it is mandatory). if len(sys.argv) < 2: print("Usage:") print(" plot_values_pandas.py input_file.csv") print("Example:") print(" plot_values_pandas.py overall.csv") sys.exit(1) # First command line argument should contain name of input CSV. input_csv = sys.argv[1] # Try to open the CSV file specified. df = pd.read_csv(input_csv) # Print info about data frame print(df.info()) print(df.describe()) # Linear regression time = df["Time"] messages = df["Value"] # Linear regression x = np.arange(0, len(messages)) coef = np.polyfit(x, messages, 1) poly1d_fn = np.poly1d(coef) # Create a figure containing a single axes. fig, ax = plt.subplots() # Create new graph ax.plot(messages, "b", poly1d_fn(np.arange(0, len(messages))), "y--") # Title of a graph ax.set_title("Sensor values") # Add a label to x-axis ax.set_xlabel("Time") # Add a label to y-axis ax.set_ylabel("Values") ax.legend(loc="upper right") # And save the plot into raster format and vector format as well fig.savefig("sensors.png") fig.savefig("sensors.svg") plt.show()

Obrázek 3: Graf vykreslený předchozím skriptem.
5. Projekt MinIO
V této kapitole se ve stručnosti seznámíme s projektem nazvaným MinIO. Jedná se o sadu několika služeb a nástrojů, které uživatelům poskytují distribuované datové úložiště určené pro ukládání obecných (strukturovaných i nestrukturovaných) dat. Typicky se jedná o soubory používané v oblasti AI (Artifical Intelligence) a ML (Machine Learning), ovšem kromě těchto populárních (a vlastně do značné míry i módních) oblastí IT je pochopitelně možné službu MinIO použít i pro ukládání logů, souborů, k nimž je zapotřebí rychle přistupovat z mnoha různých, mnohdy vzájemně vzdálených oblastí (zde využijeme možnost distribuovaného systému), jako centrální úložiště dokumentů, obrázků, videí, pochopitelně i obrazů souborových systémů pro Docker apod. MinIO dosahuje velmi slušné rychlosti přístupu k datům (při vhodně nadimenzované síti, která je většinou limitujícím faktorem) a mj. i díky velmi dobré stabilitě ukazuje přednosti programovacího jazyka Go, v němž je celý systém naprogramován.
Jedním z nejdůležitějších a v důsledku i nejpraktičtějších vlastností projektu MinIO je fakt, že se pro přístup k datům používá stejná technologie, jaká je implementována i v populární službě Amazon S3 či možná přesněji AWS S3. To mj. znamená, že dodávaný MinIO Client SDK popsaný v navazujících kapitolách může sloužit jak pro přístup k datům uloženým v Miniu, tak i k datům uloženým ve cloudu na S3. Díky tomu lze například snadněji nastavit konfiguraci pro vývoj, konfiguraci CI, zajistit si možnost využití veřejného cloudu (S3) nebo naopak privátního cloudu (založeného na Miniu) atd. Navíc je MinIO Client SDK určen jen pro přístup k datům a nikoli pro ovládání dalších služeb, takže je jeho zahrnutí do vyvíjené aplikace méně náročné na systémové prostředky. Musíme si totiž uvědomit, že přístup k datům je mnohdy zapotřebí i z relativně málo výkonných zařízení IoT atd. (mj. i z tohoto důvodu se MinIO co do snadnosti integrace porovnává s Redisem, i když oblasti nasazení těchto dvou technologií jsou mnohdy značně odlišné).
Instalace služby (přesněji řečeno serverové části) projektu MinIO je snadná a přímočará. Jelikož se jedná o aplikaci naprogramovanou v jazyce Go, je služba dodávána ve formě jediného (i když relativně objemného) spustitelného souboru. K dispozici je ovšem i obraz pro Docker, překlad lze provést ze zdrojových souborů atd. Dnes se zaměříme na první způsob, tedy na stažení již připravených souborů projektu MinIO. Musíme si pouze vybrat soubor pro právě používaný operační systém a procesorovou architekturu. Pro testování budu používat Linux a architekturu x86–64. Službu MinIO, přesněji řečeno spustitelný binární soubor, který po svém spuštění službu nabídne, získáme jednoduše jediným příkazem:
$ wget https://dl.min.io/server/minio/release/linux-amd64/minio
Následně je nutné nastavit příznak „x“ pro stažený soubor, aby bylo možné službu spustit přímo z příkazového řádku:
$ chmod +x minio
Dále pro jistotu otestujeme, zda je stažený soubor skutečně spustitelný:
$ ./minio version
Podobným způsobem lokálně nainstalujeme i konzoli projektu MinIO. Ta se jmenuje mc. Nejdříve stáhneme příslušný spustitelný soubor pro zvolený operační systém a architekturu mikroprocesoru:
$ wget https://dl.min.io/client/mc/release/linux-amd64/mc
Následně, podobně jako v předchozích krocích, nastavíme příznak „x“, aby byla konzole spustitelná:
$ chmod +x mc
A ověříme si, že tomu tak skutečně je:
$ ./mc version Version: 2019-10-09T22:54:57Z Release-tag: RELEASE.2019-10-09T22-54-57Z Commit-id: f93fe1330a3647b1afaff0ed8c188d2897bf391e
$ mc $ ./mc
A konečně v posledním kroku nainstalujeme balíček pro Python, který zajistí přístup k běžícímu Miniu (nebo i k S3):
$ pip3 install --user minio Requirement already satisfied: minio in ./.local/lib/python3.6/site-packages Requirement already satisfied: urllib3 in /usr/lib/python3.6/site-packages (from minio) Requirement already satisfied: pytz in /usr/lib/python3.6/site-packages (from minio) Requirement already satisfied: certifi in ./.local/lib/python3.6/site-packages (from minio) Requirement already satisfied: python-dateutil in ./.local/lib/python3.6/site-packages (from minio) Requirement already satisfied: six>=1.5 in ./.local/lib/python3.6/site-packages (from python-dateutil->minio)
6. Spuštění Minia v režimu serveru
Pokud již máme připravený spustitelný soubor nazvaný minio (viz též předchozí kapitolu), je inicializace a následné spuštění služby MinIO na lokálním počítači otázkou jediného příkazu. Musíme pouze specifikovat, že se má spustit server a na jakém disku a adresáři budou umístěny soubory spravované službou MinIO:
$ ./minio server .
Zprávy zobrazené po spuštění serveru:
API: http://192.168.1.34:9000 http://192.168.122.1:9000 http://192.168.130.1:9000 http://127.0.0.1:9000 RootUser: minioadmin RootPass: minioadmin Console: http://192.168.1.34:40747 http://192.168.122.1:40747 http://192.168.130.1:40747 http://127.0.0.1:40747 RootUser: minioadmin RootPass: minioadmin Command-line: https://docs.min.io/docs/minio-client-quickstart-guide $ mc alias set myminio http://192.168.1.34:9000 minioadmin minioadmin Documentation: https://docs.min.io
Povšimněte si, že se po spuštění zobrazily všechny informace nutné pro spuštění webového rozhraní, pro použití konzole ovládané z příkazového řádku i pro instalaci SDK pro podporované programovací jazyky. Dále jsme získali i dvojici klíčů, které použijeme v následujících kapitolách, a to jak při přístupu přes webovou konzoli, tak i v demonstračních příkladech založených na SDK Minia.
7. Načtení seznamu objektů uložených v Miniu
Nyní navážeme na kostru celé „aplikace“ uvedené v první kapitole a přidáme do ní základní funkcionalitu – získání a zobrazení seznamu objektů uložených v Miniu v určitém bucketu. Jedná se o relativně jednoduchou operaci, protože klient pro Minio nabízí přímo k tomu určenou metodu nazvanou list_objects, které se pouze předá jméno bucketu. Samozřejmě je ovšem nejprve nutné se připojit k Miniu se specifikací adresy a dvojice klíčů (ty lze zajistit přidáním nového uživatele přes konzoli Minia):
client = Minio( minio_address, minio_access_key, minio_secret_key, secure=False )
Celý příklad bude po úpravách vypadat následovně:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 from minio import Minio bucket_name = "sensors" minio_address = "localhost:9000" minio_access_key = "tester" minio_secret_key = "tester01" def main(): client = Minio( minio_address, minio_access_key, minio_secret_key, secure=False ) found = client.bucket_exists(bucket_name) print("Bucket found:", found) objects = client.list_objects(bucket_name, recursive=False) for obj in objects: print(obj.bucket_name, obj.object_name, obj.last_modified, obj.etag, obj.size, obj.content_type) if __name__ == "__main__": main()
Příklad výsledku pro náhodně přidané soubory (objekty):
$ python3 sensors_v02.py Bucket found: True sensors pyreverse 2022-05-06 14:45:33.030000+00:00 f992f0e93eaa023caa2e9a735c344fa9 1857 None sensors storage.go 2022-05-06 14:45:28.235000+00:00 63a36e97f46ac991ebe883c569a0fd5b 40041 None
8. Seznam datových souborů s výsledky měření
Nyní do Minia přidáme několik datových souborů, jejichž jména budou odpovídat jménům senzorů. Bude se jednat o soubory ve formátu CSV obsahující dvojici sloupců. Viz též příklad uvedený ve druhé kapitole, popř. seznam souborů nabídnutých v repositáři s demonstračními příklady.
Datové soubory lze nahrát přes konzoli Minia (mc) nebo přes webovou administrační konzoli (zde ručně):

Obrázek 4: Přidání nového uživatele přes administrační konzoli.

Obrázek 5: Přidání souboru do Minia přes administrační konzoli.

Obrázek 6: Seznam souborů (objektů) uložených v Miniu.
Po opětovném spuštění příkladu z předchozí kapitoly získáme tyto výsledky (časová razítka se samozřejmě budou odlišovat):
$ python3 sensors_v02.py Bucket found: True sensors sensor_1.csv 2022-05-06 16:04:59.590000+00:00 6761c54a2d4d19342b0583a2350c2007 21181 None sensors sensor_2.csv 2022-05-06 16:05:03.488000+00:00 cdc3fca684b7e7d3cba1a040caf6b571 1135 None sensors sensor_3.csv 2022-05-06 16:05:06.402000+00:00 fbbfc6fa348238aecffca3b8fad32716 1507 None sensors sensor_4.csv 2022-05-06 16:05:09.237000+00:00 e6822d4390400f5045a787c169036a1c 1089 None sensors sensor_5.csv 2022-05-06 16:05:11.655000+00:00 5a48f56ce4057e360473007af2e82284 1338 None
9. Načtení objektů z Minia
Dalším krokem bude načtení objektů (tedy uložených dat bez jejich další interpretace) z Minia. Jedná se o rychlou operaci, která však vyžaduje dekódování dat, protože údaje poskytnuté přímo Miniem obsahují (kromě dalších věcí) i krátkou hlavičku. Ve chvíli, kdy známe jméno objektu a bucketu, v němž je objekt uložen, je získání původních dat otázka dvou řádků kódu:
response = client.get_object(bucket_name, obj.object_name) data = response.read().decode() ... ... ...
Popř. i s kontrolou chyb a uzavřením prostředků:
try: response = client.get_object(bucket_name, obj.object_name) data = response.read().decode() ... ... ... except ResponseError as err: print(err) finally: response.close() response.release_conn()
Upravená verze příkladu, který získá všechna data uložená v daném bucketu, může vypadat takto:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 from minio import Minio, ResponseError bucket_name = "sensors" minio_address = "localhost:9000" minio_access_key = "tester" minio_secret_key = "tester01" def main(): client = Minio( minio_address, minio_access_key, minio_secret_key, secure=False ) found = client.bucket_exists(bucket_name) print("Bucket found:", found) objects = client.list_objects(bucket_name, recursive=False) for obj in objects: print(obj.bucket_name, obj.object_name, obj.last_modified, obj.etag, obj.size, obj.content_type) try: response = client.get_object(bucket_name, obj.object_name) data = response.read().decode() print(data) except ResponseError as err: print(err) finally: response.close() response.release_conn() if __name__ == "__main__": main()
10. Přečtení dat uložených ve formátu CSV
Víme, že data získaná z Minia jsou ve skutečnosti uložena ve formátu CSV, takže je nutné je správně interpretovat. Pro tento účel použijeme knihovnu Pandas, přičemž je při převodu vhodné použít paměťový buffer realizovaný například v balíčku io.StringIO. Načtení objektu z Minia a jeho převod na datový rámec knihovny Pandas může vypadat následovně. Načtení i převod do datového rámce je realizován na pouhých tří řádcích:
response = client.get_object(bucket_name, obj.object_name) buff = StringIO(response.read().decode()) data = pd.read_csv(buff)
Úplný zdrojový kód takto upraveného demonstračního příkladu bude vypadat následovně:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 from minio import Minio, ResponseError import pandas as pd from io import StringIO bucket_name = "sensors" minio_address = "localhost:9000" minio_access_key = "tester" minio_secret_key = "tester01" def main(): client = Minio( minio_address, minio_access_key, minio_secret_key, secure=False ) found = client.bucket_exists(bucket_name) print("Bucket found:", found) objects = client.list_objects(bucket_name, recursive=False) for obj in objects: print(obj.bucket_name, obj.object_name, obj.last_modified, obj.etag, obj.size, obj.content_type) try: response = client.get_object(bucket_name, obj.object_name) buff = StringIO(response.read().decode()) data = pd.read_csv(buff) print(data) except ResponseError as err: print(err) finally: response.close() response.release_conn() if __name__ == "__main__": main()
11. Zobrazení grafu z dat načtených z Minia
Nyní zkombinujeme kód, který byl ukázán ve třetí kapitole (zobrazení grafu na základě obsahu datového rámce) a taktéž v kapitole předchozí (načtení datového rámce z Minia). Skutečně se prakticky jedná o spojení obou příkladů s nepatrným refaktoringem, takže se přímo podívejme na to, jak tento příklad bude vypadat:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 from minio import Minio, ResponseError import pandas as pd from io import StringIO import numpy as np import matplotlib import matplotlib.pyplot as plt matplotlib.use('agg') # required, use a non-interactive backend bucket_name = "sensors" minio_address = "localhost:9000" minio_access_key = "tester" minio_secret_key = "tester01" def create_graph(filename, df): # Linear regression time = df["Time"] messages = df["Value"] # Linear regression x = np.arange(0, len(messages)) coef = np.polyfit(x, messages, 1) poly1d_fn = np.poly1d(coef) # Create a figure containing a single axes. fig, ax = plt.subplots() # Create new graph ax.plot(messages, "b", poly1d_fn(np.arange(0, len(messages))), "y--") # Title of a graph ax.set_title("Sensor values") # Add a label to x-axis ax.set_xlabel("Time") # Add a label to y-axis ax.set_ylabel("Values") ax.legend(loc="upper right") # And save the plot into raster format and vector format as well fig.savefig(filename) def main(): client = Minio( minio_address, minio_access_key, minio_secret_key, secure=False ) found = client.bucket_exists(bucket_name) print("Bucket found:", found) objects = client.list_objects(bucket_name, recursive=False) for obj in objects: print(obj.bucket_name, obj.object_name, obj.last_modified, obj.etag, obj.size, obj.content_type) try: response = client.get_object(bucket_name, obj.object_name) buff = StringIO(response.read().decode()) df = pd.read_csv(buff) filename = obj.object_name[0:-4] + ".png" create_graph(filename, df) except ResponseError as err: print(err) finally: response.close() response.release_conn() if __name__ == "__main__": main()
Tento skript vytvoří tolik grafů, kolik datových souborů našel v Miniu:

Obrázek 7: Druhý vykreslený graf.

Obrázek 8: Třetí vykreslený graf.
12. Od skriptu k dynamické webové stránce s využitím PyWebIO
V rámci dalších kapitol předěláme výše uvedený skript do podoby webové aplikace, která uživateli nabídne seznam senzorů a poté možnost si zobrazit příslušný graf popř. přímo data získaná senzorem. Pro tento účel použijeme knihovnu PyWebIO, s níž jsme se již na stránkách Roota seznámili.
Knihovna PyWebIO je nabízena přes PyPi, takže její instalace by měla být jednoduchá a přímočará. Knihovnu nainstalujeme pro aktuálně přihlášeného uživatele:
$ pip3 install --user pywebio Collecting pywebio Downloading https://files.pythonhosted.org/packages/a5/bd/d4e775b6bf43dc83d1c1c596ae91c5ce58baf9199ac28b67b8886a9de8aa/pywebio-1.6.0.tar.gz (468kB) 100% |████████████████████████████████| 471kB 200kB/s Collecting tornado>=5.0 (from pywebio) Downloading https://files.pythonhosted.org/packages/01/d1/8750ad20cbcefb499bb8b405e243f83c2c89f78d139e6f8c8d800640f554/tornado-6.1-cp36-cp36m-manylinux1_x86_64.whl (427kB) 100% |████████████████████████████████| 430kB 874kB/s Collecting user-agents (from pywebio) Downloading https://files.pythonhosted.org/packages/8f/1c/20bb3d7b2bad56d881e3704131ddedbb16eb787101306887dff349064662/user_agents-2.2.0-py3-none-any.whl Collecting ua-parser>=0.10.0 (from user-agents->pywebio) Downloading https://files.pythonhosted.org/packages/9d/22/4d16b08db329fd440eed366d35e4dd7195c9babb4ecac5218f28081522a2/ua_parser-0.10.0-py2.py3-none-any.whl Building wheels for collected packages: pywebio Running setup.py bdist_wheel for pywebio ... done Stored in directory: /home/ptisnovs/.cache/pip/wheels/fb/29/d3/09c684d68476f021ef044809643cee71560603503907dd42b3 Successfully built pywebio Installing collected packages: tornado, ua-parser, user-agents, pywebio Successfully installed pywebio-1.6.0 tornado-6.1 ua-parser-0.10.0 user-agents-2.2.0
13. Prázdná dynamická webová stránka
Začneme tou nejjednodušší možnou podobou webové aplikace – stránkou, v níž se pouze zobrazí informační zpráva. První varianta takové aplikace může vypadat následovně:
from pywebio import * from pywebio.input import * from pywebio.output import * def main(): put_info("Výsledky měření senzorů") start_server(main, port=8080, debug=True)
Po spuštění tohoto skriptu by se měl spustit webový server na zadané adrese, který uživateli poskytne následující podobu webové aplikace:

Obrázek 9: Webová aplikace v její první (surové) podobě nenabízí příliš funkcí.
14. Zobrazení seznamu senzorů
V dalším kroku zobrazíme ve webové aplikaci seznam senzorů. Bude se jednat o upravený seznam objektů získaných z Minia:
objects = client.list_objects(bucket_name, recursive=False)
Tento seznam zobrazíme formou tabulky, která se vytvoří (poměrně primitivním a neidiomatickým způsobem) například takto:
table = [['Senzor', 'Datum', 'Graf', 'Info']] for obj in objects: row = [obj.object_name, obj.last_modified, "Graf", "Info"] table.append(row)
Zobrazení tabulky je přímočaré:
put_table(table)
S tímto výsledkem:

Obrázek 10: Seznam senzorů zobrazených ve webové aplikaci.
A pro úplnost si ukažme celý zdrojový kód tohoto demonstračního příkladu:
from pywebio import * from pywebio.input import * from pywebio.output import * from minio import Minio, ResponseError bucket_name = "sensors" minio_address = "localhost:9000" minio_access_key = "tester" minio_secret_key = "tester01" client = Minio( minio_address, minio_access_key, minio_secret_key, secure=False ) def main(): put_info("Výsledky měření senzorů") found = client.bucket_exists(bucket_name) if not found: put_error("Data nelze přečíst - chybné připojení k Miniu") objects = client.list_objects(bucket_name, recursive=False) table = [['Senzor', 'Datum', 'Graf', 'Info']] for obj in objects: row = [obj.object_name, obj.last_modified, "Graf", "Info"] table.append(row) put_table(table) start_server(main, port=8080, debug=True)
15. Popup okna v PyWebIO
U každého senzoru by se měly objevit ovládací prvky určené pro zobrazení dat získaných senzorem popř. určené pro zobrazení grafu získaného z těchto dat. Tyto údaje se mají zobrazit v popup okně, které se v PyWebIO vytvoří zavoláním funkce popup, které se předá titulek a taktéž seznam ovládacích prvků:
def show_data(sensor): popup(f"Data ze senzoru {sensor}", [ put_text(f"Data ze senzoru {sensor}"), ])
Musíme tedy rozšířit tabulku se seznamem vektorů o ovládací prvky, po jejichž výběru se zavolá funkce show_data popř. show_graph. To lze zajistit mnoha způsoby, například následovně:
for obj in objects: row = [obj.object_name, obj.last_modified, put_button("Graf", onclick=partial(show_graph, sensor=obj.object_name)), put_button("Data", onclick=partial(show_data, sensor=obj.object_name))] table.append(row)
S tímto výsledkem:

Obrázek 11: Upravený seznam senzorů s novými ovládacími prvky.

Obrázek 12: Prozatím prázdné popup okno s tabulkou dat.

Obrázek 13: Prozatím prázdné popup okno s grafem.
A pro úplnost si opět ukažme celý zdrojový kód tohoto demonstračního příkladu:
from pywebio import * from pywebio.input import * from pywebio.output import * from functools import partial from minio import Minio, ResponseError bucket_name = "sensors" minio_address = "localhost:9000" minio_access_key = "tester" minio_secret_key = "tester01" client = Minio( minio_address, minio_access_key, minio_secret_key, secure=False ) def show_data(sensor): popup(f"Data ze senzoru {sensor}", [ put_text(f"Data ze senzoru {sensor}"), ]) def show_graph(sensor): popup(f"Graf pro senzor {sensor}", [ put_text(f"Graf pro senzor {sensor}"), ]) def main(): put_info("Výsledky měření senzorů") found = client.bucket_exists(bucket_name) if not found: put_error("Data nelze přečíst - chybné připojení k Miniu") objects = client.list_objects(bucket_name, recursive=False) table = [['Senzor', 'Datum', 'Graf', 'Info']] for obj in objects: row = [obj.object_name, obj.last_modified, put_button("Graf", onclick=partial(show_graph, sensor=obj.object_name)), put_button("Data", onclick=partial(show_data, sensor=obj.object_name))] table.append(row) put_table(table) start_server(main, port=8080, debug=True)
16. Zobrazení popup okna s daty senzoru
Předposlední funkcí, kterou ještě musíme implementovat, je zobrazení tabulky s daty senzoru. Samotná data načteme do datového rámce knihovny Pandas a potom datový rámec „vykreslíme“ do HTML fragmentu. Posléze již postačuje tento fragment vložit do popup okna funkcí put_html:
def show_data(bucket_name, sensor): response = client.get_object(bucket_name, sensor) buff = StringIO(response.read().decode()) df = pd.read_csv(buff) popup(f"Data ze senzoru {sensor}", [ put_text(f"Data ze senzoru {sensor}"), put_html(df.to_html(border=0)), ])

Obrázek 14: Zobrazení obsahu datového rámce do popup okna, které je pochopitelně plně skrolovatelné.
from pywebio import * from pywebio.input import * from pywebio.output import * from functools import partial from io import StringIO from minio import Minio, ResponseError import pandas as pd bucket_name = "sensors" minio_address = "localhost:9000" minio_access_key = "tester" minio_secret_key = "tester01" client = Minio( minio_address, minio_access_key, minio_secret_key, secure=False ) def show_data(bucket_name, sensor): response = client.get_object(bucket_name, sensor) buff = StringIO(response.read().decode()) df = pd.read_csv(buff) popup(f"Data ze senzoru {sensor}", [ put_text(f"Data ze senzoru {sensor}"), put_html(df.to_html(border=0)), ]) def show_graph(sensor): popup(f"Graf pro senzor {sensor}", [ put_text(f"Graf pro senzor {sensor}"), ]) def main(): put_info("Výsledky měření senzorů") found = client.bucket_exists(bucket_name) if not found: put_error("Data nelze přečíst - chybné připojení k Miniu") objects = client.list_objects(bucket_name, recursive=False) table = [['Senzor', 'Datum', 'Graf', 'Info']] for obj in objects: row = [obj.object_name, obj.last_modified, put_button("Graf", onclick=partial(show_graph, sensor=obj.object_name)), put_button("Data", onclick=partial(show_data, bucket_name=bucket_name, sensor=obj.object_name))] table.append(row) put_table(table) start_server(main, port=8080, debug=True)
17. Zobrazení popup okna s grafem
V poslední variantě našeho programu zajistíme vykreslení grafu s údaji získanými z vybraného senzoru do operační paměti s následným zobrazením popup okna s tímto grafem. Graf vykreslujeme do operační paměti (přesněji řečeno do bufferu alokovaného v paměti) z toho důvodu, aby nebylo nutné na straně serveru vytvářet dočasný obrázek, který by se posléze přenesl na klienta (vytváření dočasných souborů není z mnoha hledisek efektivní a ani pro to není v tomto případě žádný praktický důvod).
Prázdný graf je zkonstruován následovně:
fig, ax = plt.subplots()
Do grafu posléze vykreslíme potřebné údaje, tj. jak průběh hodnot, tak i osy, legendu, regresní úsečku atd.:
ax.plot(messages, "b", poly1d_fn(np.arange(0, len(messages))), "y--") ... ... ...
Po provedení těchto operací posléze zajistíme vykreslení grafu do paměťového bufferu, jenž bude obsahovat rastrovou reprezentaci grafu. Ihned po vykreslení grafu uvolníme paměť alokovanou pro graf (což ovšem není vždy nutné provádět):
buf = io.BytesIO() fig.savefig(buf) fig.clear() plt.close(fig)
Graf, resp. přesněji řečeno rastrový obrázek s grafem, se vloží do popup okna na webové stránce příkazem put_image:
popup(f"Graf pro senzor {name}", [ put_text(f"Graf pro senzor {name}"), put_image(buf.getvalue()) ])
S tímto výsledkem:

Obrázek 15: Jeden z grafů vybraných uživatelem a zobrazených v popup okně.
Úplný zdrojový kód poslední varianty demonstračního příkladu vypadá následovně:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 from pywebio import * from pywebio.input import * from pywebio.output import * from functools import partial from io import StringIO from minio import Minio, ResponseError import io import pandas as pd import numpy as np import matplotlib import matplotlib.pyplot as plt matplotlib.use('agg') # required, use a non-interactive backend bucket_name = "sensors" minio_address = "localhost:9000" minio_access_key = "tester" minio_secret_key = "tester01" client = Minio( minio_address, minio_access_key, minio_secret_key, secure=False ) def read_data_frame(bucket_name, sensor): response = client.get_object(bucket_name, sensor) buff = StringIO(response.read().decode()) return pd.read_csv(buff) def show_data(bucket_name, sensor): name = sensor[0:-4] df = read_data_frame(bucket_name, sensor) popup(f"Data ze senzoru {name}", [ put_text(f"Data ze senzoru {name}"), put_html(df.to_html(border=0)), ]) def show_graph(bucket_name, sensor): name = sensor[0:-4] df = read_data_frame(bucket_name, sensor) # Linear regression time = df["Time"] messages = df["Value"] # Linear regression x = np.arange(0, len(messages)) coef = np.polyfit(x, messages, 1) poly1d_fn = np.poly1d(coef) # Create a figure containing a single axes. fig, ax = plt.subplots() # Create new graph ax.plot(messages, "b", poly1d_fn(np.arange(0, len(messages))), "y--") # Title of a graph ax.set_title("Sensor values") # Add a label to x-axis ax.set_xlabel("Time") # Add a label to y-axis ax.set_ylabel("Values") ax.legend(loc="upper right") # Save into buffer, not into file buf = io.BytesIO() fig.savefig(buf) fig.clear() plt.close(fig) popup(f"Graf pro senzor {name}", [ put_text(f"Graf pro senzor {name}"), put_image(buf.getvalue()) ]) def main(): put_info("Výsledky měření senzorů") found = client.bucket_exists(bucket_name) if not found: put_error("Data nelze přečíst - chybné připojení k Miniu") objects = client.list_objects(bucket_name, recursive=False) table = [['Senzor', 'Datum', 'Graf', 'Info']] for obj in objects: row = [obj.object_name, obj.last_modified, put_button("Graf", onclick=partial(show_graph, bucket_name=bucket_name, sensor=obj.object_name)), put_button("Data", onclick=partial(show_data, bucket_name=bucket_name, sensor=obj.object_name))] table.append(row) put_table(table) start_server(main, port=8080, debug=True)
18. Závěr
Z jednotlivých fragmentů, konkrétně ze skriptu pro zobrazení grafu a ze skriptu pro načtení dat z Minia, lze (s alespoň základní znalostí možností knihovny PyWebIO) sestavit celou „webovou aplikaci“ skutečně přibližně za půl hodiny (tedy mnohem rychleji, než trvalo sepsání tohoto článku :-). Nejedná se sice o zabezpečenou aplikaci určenou pro nasazení v prostředí divokého západu Internetu, ale může se jednat o základ poměrně robustní intranetové aplikace, navíc vytvořitelné skutečně pouze se znalostí Pythonu a jeho knihoven. Vyšší cíle, tj. například tvorbu webových aplikací typu Google Doc, si PyWebIO nedává.
19. Repositář s demonstračními příklady
Zdrojové kódy všech předminule, minule i dnes popsaných demonstračních příkladů určených pro programovací jazyk Python 3 byly uloženy do Git repositáře dostupného na adrese https://github.com/tisnik/most-popular-python-libs. V případě, že nebudete chtít klonovat celý repositář (ten je ovšem stále velmi malý, dnes má velikost zhruba několik desítek kilobajtů), můžete namísto toho použít odkazy na jednotlivé příklady, které naleznete v následující tabulce:
20. Odkazy na Internetu
- Low code Python web framework
https://www.pyweb.io/ - Repositář projektu
https://github.com/pywebio/PyWebIO/ - Getting Started
https://www.pyweb.io/tutorial.html - Dokumentace
https://pywebio.readthedocs.io/en/latest/ - Why PyWebIO?
https://github.com/pywebio/PyWebIO/wiki/Why-PyWebIO%3F - PyWebIO demos
https://pywebio-demos.pywebio.online/ - PyWebIO Chart Gallery
https://pywebio-charts.pywebio.online/ - Awesome Python
https://awesome-python.com/ - A complete guide to web development in Python
https://www.educative.io/blog/web-development-in-python - Python Web Development Tutorials
https://realpython.com/tutorials/web-dev/ - What is Flask Python
https://pythonbasics.org/what-is-flask-python/ - CherryPy
https://cherrypy.dev/ - Projekt Zenity
https://wiki.gnome.org/Projects/Zenity - Nástroj Dialog
http://invisible-island.net/dialog/ - Plotly
https://plotly.com/ - Bokeh
https://bokeh.org/ - pyecharts
https://github.com/pyecharts/pyecharts/blob/master/README.en.md - Tvorba grafů v Jupyter Notebooku s využitím knihovny Matplotlib
https://www.root.cz/clanky/tvorba-grafu-v-jupyter-notebooku-s-vyuzitim-knihovny-matplotlib/ - Alternatives to PyWebIO
https://stackshare.io/pywebio/alternatives - The fastest way to build and share data apps – Streamlit
https://streamlit.io/ - Dash Enterprise
https://plotly.com/dash/ - pglet
https://pglet.io/ - Stránky projektu MinIO
https://min.io/ - MinIO Quickstart Guide
https://docs.min.io/docs/minio-quickstart-guide.html - MinIO Go Client API Reference
https://docs.min.io/docs/golang-client-api-reference - Performance at Scale: MinIO Pushes Past 1.4 terabits per second with 256 NVMe Drives
https://blog.min.io/performance-at-scale-minio-pushes-past-1–3-terabits-per-second-with-256-nvme-drives/ - Benchmarking MinIO vs. AWS S3 for Apache Spark
https://blog.min.io/benchmarking-apache-spark-vs-aws-s3/ - MinIO Client Quickstart Guide
https://docs.min.io/docs/minio-client-quickstart-guide.html - Analýza kvality zdrojových kódů Minia
https://goreportcard.com/report/github.com/minio/minio - This is MinIO
https://www.youtube.com/watch?v=vF0lQh0XOCs - Running MinIO Standalone
https://www.youtube.com/watch?v=dIQsPCHvHoM - „Amazon S3 Compatible Storage in Kubernetes“ – Rob Girard, Principal Tech Marketing Engineer, Minio
https://www.youtube.com/watch?v=wlpn8K0jJ4U