Hlavní navigace

Validace datových struktur v Pythonu (dokončení)

19. 4. 2018
Doba čtení: 21 minut

Sdílet

Ve třetí a závěrečné části článku o knihovnách pro validaci složitých datových struktur v Pythonu si ukážeme použití modulu pytest-voluptuous, který pro validaci využívá minule popsanou knihovnu s nezapamatovatelným názvem Voluptuous.

Obsah

1. Validace datových struktur v Pythonu (dokončení)

2. Instalace modulu pytest-voluptuous s jeho krátkým otestováním v REPLu

3. První příklad – základy použití modulu pytest-voluptuous

4. Výsledek spuštění prvního příkladu

5. Validace složitějších datových struktur (slovníků a seznamů slovníků)

6. Výsledek spuštění druhého příkladu

7. Praktičtější příklad – validace dat získaných z REST API

8. Zjištění, jestli hodnota odpovídá UUID verze 4

9. Výsledek běhu třetího demonstračního příkladu

10. Validace složitější datové struktury získané přes REST API – první varianta

11. Základní validace hodnot uložených ve slovníku

12. Podrobná validace hodnot uložených ve slovníku

13. Validace slovníků uložených v jiném slovníku

14. Úplný zdrojový kód čtvrtého demonstračního příkladu

15. Výsledek běhu čtvrtého demonstračního příkladu

16. Použití klauzule Optional

17. Implicitní nastavení volitelných klíčů a konkrétní specifikace klíčů povinných

18. Pátý demonstrační příklad a výsledek jeho spuštění

19. Repositář s demonstračními příklady

20. Odkazy na Internetu

1. Validace datových struktur v Pythonu (dokončení)

V posledním článku o knihovnách sloužících pro validaci složitých datových struktur v Pythonu navážeme na část první a taktéž druhou. Připomeňme si, že v prvním článku jsme si popsali knihovny pojmenované Schemagic a Schema. Obě tyto knihovny jsou založeny na tom, že samotný popis validačních schémat je nadeklarován přímo v Pythonu, takže se případní uživatelé těchto knihoven (což jsou většinou programátoři) nemusí učit nový DSL (doménově specifický jazyk). Knihovna Schemagic je zvláštní tím, že kromě vlastní validace provádí i konverzi dat. Ve druhé části jsme si pak popsali knihovnu s nevyslovitelným názvem Voluptuous. Autoři této knihovny dbají na to, aby byla validace co nejjednodušší, tj. aby nebylo nutné tvořit složité třídy (k čemuž vede knihovna Schema) atd.

Dnes si popíšeme poslední nástroj nazvaný pytest-voluptuous. Jedná se o sadu tříd obalujících knihovnu Voluptuous takovým způsobem, aby bylo testování co nejsnazší. Například je přetížen operátor ==, který umožňuje, aby se data „porovnávala“ se schématem v příkazu assert atd. Připomeňme si, jak může vypadat velmi jednoduché schéma sloužící pro validaci slovníku obsahujícího tři klíče:

user = Schema({"name": str,
               "surname": str,
               "id": pos})

Pro samotnou validaci si vytvoříme pomocnou funkci:

def validate(schema, data):
    try:
        print("\n\n")
        print(schema)
        print(data)
        schema(data)
        print("pass")
    except Exception as e:
        print(e)

Validace může probíhat následovně:

validate(user, {"name": "Eda",
                "surname": "Wasserfall",
                "id": 1})
 
validate(user, {"name": "Eda",
                "id": 1})
 
validate(user, {"name": "Eda",
                "surname": "Wasserfall",
                "id": 0})

V dalším textu uvidíme, že se celá validace může ještě nepatrně zjednodušit a bude ji možné velmi snadno použít například v jednotkových testech.

2. Instalace modulu pytest-voluptuous s jeho krátkým otestováním v REPLu

Před spuštěním demonstračních příkladů si knihovnu pytest-voluptuous nainstalujeme, a to klasicky s využitím nástroje pip3 (nebo pip), protože tato knihovna je samozřejmě registrována i na PyPI (Python Package Index). Pro jednoduchost provedeme instalaci jen pro právě aktivního uživatele:.

$ pip3 install --user pytest-voluptuous
 
Downloading/unpacking pytest-voluptuous
  Downloading pytest_voluptuous-1.0.2-py2.py3-none-any.whl
Requirement already satisfied (use --upgrade to upgrade): pytest in /usr/local/lib/python3.4/dist-packages (from pytest-voluptuous)
Downloading/unpacking voluptuous>=0.9.0 (from pytest-voluptuous)
  Downloading voluptuous-0.11.1-py2.py3-none-any.whl
Installing collected packages: pytest-voluptuous, voluptuous
Successfully installed pytest-voluptuous voluptuous
Cleaning up...

Pro jistotu si vypíšeme základní informace o této knihovně, tj. zjistíme, zda je databáze nástroje pip konzistentní:

$ pip3 show pytest-voluptuous
 
---
Name: pytest-voluptuous
Version: 1.0.2
Location: /home/tester/.local/lib/python3.4/site-packages
Requires: voluptuous, pytest

Nyní si můžeme základní vlastnosti této knihovny odzkoušet přímo v interaktivní smyčce REPL programovacího jazyka Python. Knihovnu jsme instalovali pro Python 3.x, takže musíme spustit i odpovídající interpret:

$ python3

Spuštění interpretru:

Python 3.4.3 (default, Nov 28 2017, 16:41:13)
[GCC 4.8.4] on linux
Type "help", "copyright", "credits" or "license" for more information.

Import třídy S z modulu pytest_voluptuous (skutečně se používá pouze S a nikoli Schema):

>>> from pytest_voluptuous import S

Definice jednoduchého schématu, které odpovídá schématu uvedenému v první kapitole:

>>> user = S({"name": str, "surname": str, "id": int})

A konečně si můžeme ukázat, jak se provádí validace pomocí příkazu assert a přetíženého operátoru ==:

>>> assert user == {"name": "Eda", "surname": "Wasserfall", "id": 1}
 
>>> assert user == {"name": "Eda", "surname": "Wasserfall", "id": "xyzzy"}
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError
 
>>> assert user == {"name": "Eda", "surname": "Wasserfall"}
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError

Podrobnosti budou samozřejmě uvedeny v navazujících kapitolách.

3. První příklad – základy použití modulu pytest-voluptuous

Ukažme si nyní zdrojový kód prvního příkladu, v němž modul pytest-voluptuous použijeme. Začátek je jednoduchý – naimportujeme především třídu S z modulu pytest_voluptuous a pro další testy i třídy All a Length z modulu voluptuous.validators (tyto třídy použijeme později):

#!/usr/bin/env python
# vim: set fileencoding=utf-8
 
from pytest_voluptuous import S
from voluptuous.validators import All, Length

Dále definujeme validační schéma představující libovolně dlouhý seznam celých čísel, čísel s plovoucí řádovou čárkou či čísel komplexních (v obecně libovolném pořadí a počtu):

number_list = S([int, float, complex])

Následuje sada testů (už nyní si můžete tipnout, které dopadnou v pořádku a které nikoli). Jak je u knihovny Pytest zvykem, začínají testovací funkce prefixem „test“:

def test1():
    assert [1, 2, 3] == number_list
    assert [1, 2, 3.2] == number_list
 
 
def test2():
    assert [2j, 4j, 5j] == number_list
    assert [1+2j, 3+4j, 5j] == number_list
 
 
def test3():
    assert [1, "2", 3] == number_list
 
 
def test4():
    assert ["1", "2", "3"] == number_list
 
 
def test5():
    assert (1, 2, 3) == number_list

Druhé schéma představuje libovolně dlouhý seznam binárních číslic:

binary_numbers = S([0, 1])
 
def test6():
    assert binary_numbers == [0, 0, 0]
    assert binary_numbers == [1, 1, 0]
 
 
def test7():
    assert binary_numbers == [1, 2, 3]
Poznámka: úplný zdrojový kód tohoto příkladu naleznete na adrese https://github.com/tisnik/python-schema-checks/tree/master/pytest-voluptuous-demo1.

4. Výsledek spuštění prvního příkladu

Dnešní první demonstrační příklad nebudeme spouštět přímo intepretrem Pythonu, ale přes pytest, tedy takto:

$ pytest pytest-voluptuous-demo1.py

Výsledkem by měl být přibližně následující výstup (samozřejmě se mohou nepatrně lišit verze Pythonu i knihoven, ovšem testy by měly mít shodný průběh). Povšimněte si, že z celkem sedmi testů čtyři testy nalezly nevalidní data, přesně podle očekávání:

============================= test session starts ==============================
platform linux -- Python 3.4.3, pytest-3.3.1, py-1.5.2, pluggy-0.6.0
rootdir: /home/tester/temp/python/python-schema-checks/pytest-voluptuous-demo1, inifile:
plugins: voluptuous-1.0.2
collected 7 items
 
pytest-voluptuous-demo1.py ..FFF.F                                       [100%]
 
=================================== FAILURES ===================================
____________________________________ test3 _____________________________________
 
    def test3():
>       assert [1, "2", 3] == number_list
E       assert failed to validation error(s):
E         - 1: expected complex @ data[1]
 
pytest-voluptuous-demo1.py:21: AssertionError
____________________________________ test4 _____________________________________
 
    def test4():
>       assert ["1", "2", "3"] == number_list
E       assert failed to validation error(s):
E         - 0: expected complex @ data[0]
E         - 1: expected complex @ data[1]
E         - 2: expected complex @ data[2]
 
pytest-voluptuous-demo1.py:25: AssertionError
____________________________________ test5 _____________________________________
 
    def test5():
>       assert (1, 2, 3) == number_list
E       assert failed to validation error(s):
E         - : expected a list
 
pytest-voluptuous-demo1.py:29: AssertionError
____________________________________ test7 _____________________________________
 
    def test7():
>       assert binary_numbers == [1, 2, 3]
E       assert failed to validation error(s):
E         - 1: not a valid value @ data[1]
E         - 2: not a valid value @ data[2]
 
pytest-voluptuous-demo1.py:40: AssertionError
====================== 4 failed, 3 passed in 0.05 seconds ======================

5. Validace složitějších datových struktur (slovníků a seznamů slovníků)

V praxi se velmi často setkáme s tím, že je zapotřebí validovat nejenom „jednoduchý“ slovník, ale například i seznam (či n-tici) slovníků. Připomeňme si, že deklarace validačního kritéria pro jednoduchý slovník vypadá takto:

user = S({"name": str,
          "surname": str,
          "id": pos})

Validační kritérium pro seznam takových slovníků se zapíše následovně (povšimněte si, že se vlastně jedná o kombinaci kritéria známého z prvního demonstračního příkladu a kritéria zapsaného před tímto odstavcem:

users = S([S({"name": str,
             "surname": str,
             "id": pos})])

Takto zapsaná validační kritéria jsou použita v dnešním druhém demonstračním příkladu, jehož zdrojový kód vypadá následovně:

#!/usr/bin/env python
# vim: set fileencoding=utf-8
 
from pytest_voluptuous import S, Partial, Exact
from voluptuous import Invalid
from voluptuous.validators import All, Length

Pomocná validační funkce pro celá kladná (přirozená) čísla:

def pos(value):
    if type(value) is not int or value <= 0:
        raise Invalid("positive integer value expected, but got {v} instead".format(v=value))

Samotné validační schéma pro jediný slovník:

user = S({"name": str,
          "surname": str,
          "id": pos})
 
 
def test1():
    assert user == {"name": "Eda",
                    "surname": "Wasserfall",
                    "id": 1}
 
 
def test2():
    assert user == {"name": "Eda",
                    "id": 1}
 
 
def test3():
    assert user == {"name": "Eda",
                    "surname": "Wasserfall",
                    "id": 0}

Validační schéma pro seznam slovníků:

users = S([S({"name": str,
             "surname": str,
             "id": pos})])
 
def test4():
    assert users == [{"name": "Eda",
                      "surname": "Wasserfall",
                      "id": 1},
                     {"name": "Varel",
                      "surname": "Frištenský",
                      "id": 2}]
 
def test5():
    assert users == [{"name": "Eda",
                      "surname": "Wasserfall",
                      "id": 0},
                     {"surname": "Frištenský",
                      "id": 2}]
Poznámka: úplný zdrojový kód tohoto příkladu naleznete na adrese https://github.com/tisnik/python-schema-checks/tree/master/pytest-voluptuous-demo2.

6. Výsledek spuštění druhého příkladu

Opět si ukažme, jak bude vypadat výsledek spuštění dnešního druhého demonstračního příkladu. Opět použijeme příkaz pytest:

$ pytest pytest-voluptuous-demo2.py

Samotný výsledek pěti testů:

============================= test session starts ==============================
platform linux -- Python 3.4.3, pytest-3.3.1, py-1.5.2, pluggy-0.6.0
rootdir: /home/tester/temp/python/python-schema-checks/pytest-voluptuous-demo2, inifile:
plugins: voluptuous-1.0.2
collected 5 items
 
pytest-voluptuous-demo2.py .FF.F                                         [100%]
 
=================================== FAILURES ===================================
____________________________________ test2 _____________________________________
 
    def test2():
>       assert user == {"name": "Eda",
                        "id": 1}
E       AssertionError: assert failed to validation error(s):
E         - surname: required key not provided @ data['surname']
 
pytest-voluptuous-demo2.py:26: AssertionError
____________________________________ test3 _____________________________________
 
    def test3():
>       assert user == {"name": "Eda",
                        "surname": "Wasserfall",
                        "id": 0}
E       AssertionError: assert failed to validation error(s):
E         - id: positive integer value expected, but got 0 instead for dictionary value @ data['id']
 
pytest-voluptuous-demo2.py:31: AssertionError
____________________________________ test5 _____________________________________
 
    def test5():
>       assert users == [{"name": "Eda",
                          "surname": "Wasserfall",
                          "id": 0},
                         {"surname": "Frištenský",
                          "id": 2}]
E       AssertionError: assert failed to validation error(s):
E         - 0.id: positive integer value expected, but got 0 instead for dictionary value @ data[0]['id']
 
pytest-voluptuous-demo2.py:49: AssertionError
====================== 3 failed, 2 passed in 0.05 seconds ======================

7. Praktičtější příklad – validace dat získaných z REST API

Zkusme se nyní zaměřit již na praktičtěji orientované příklady. Nejprve si vyzkoušejme veřejně dostupnou webovou službu, která po požadavku typu GET poslaného na adresu https://httpbin.org/uuid vrátí JSON s jediným klíčem „uuid“, pod nímž je uložen řetězec odpovídající UUID verze 4. Chování této webové služby si samozřejmě můžeme velmi jednoduše ověřit:

$ curl https://httpbin.org/uuid

Výsledek může vypadat následovně (resp. přesněji řečeno takto s prakticky stoprocentní jistotou vypadat nebude, protože získáte odlišnou hodnotu :-):

{
  "uuid": "19bd32b7-2ee8-40f0-a275-935d87331e83"

Načtení struktury vrácené v JSONu v Pythonu:

response = requests.get("https://httpbin.org/uuid").json()

Jednu z možností otestování, zda řetězec odpovídá formátu UUID verze 4, je možné v Pythonu implementovat pomocí knihovny uuid:

def uuid4(string):
    val = UUID(string, version=4)
    if val.hex != string.replace('-', ''):
        raise Invalid("the string '{s}' is not valid UUID4".format(s=string))

8. Zjištění, jestli hodnota odpovídá UUID verze 4

Celý demonstrační příklad (v pořadí již třetí), který nejprve zjistí, zda dva řetězce odpovídají formátu UUID verze 4 a následně zvaliduje JSON vrácený webovou službou, může vypadat následovně.

Nezbytné import na začátku:

#!/usr/bin/env python
# vim: set fileencoding=utf-8
 
from pytest_voluptuous import S, Partial, Exact
from voluptuous import Invalid
from voluptuous.validators import All, Length
from uuid import UUID
import requests

Již popsaná funkce pro validaci UUID verze 4:

def uuid4(string):
    val = UUID(string, version=4)
    if val.hex != string.replace('-', ''):
        raise Invalid("the string '{s}' is not valid UUID4".format(s=string))

Schéma pro strukturu, kterou vrátí webová služba:

uuid_response_struct = S({"uuid": uuid4})

Vlastní sada tří jednotkových testů:

def test_uuid_1():
    assert {"uuid": "00ebf64b-c15e-4b5d-846a-28971dc05796"} == uuid_response_struct
 
 
def test_uuid_2():
    assert {"uuid": "00ebf64b-xxxx-4b5d-846a-28971dc05796"} == uuid_response_struct
 
 
def test_uuid_returned_by_the_service():
    response = requests.get("https://httpbin.org/uuid").json()
    assert response == uuid_response_struct
Poznámka: úplný zdrojový kód tohoto příkladu naleznete na adrese https://github.com/tisnik/python-schema-checks/tree/master/pytest-voluptuous-demo3.

9. Výsledek běhu třetího demonstračního příkladu

Opět se podívejme na to, jak vypadá výsledek běhu třetího příkladu. Druhý test odhalil nevalidní řetězec (což je v pořádku, tuto chybu očekáváme), ale samotná data z webové služby jsou korektní:

============================= test session starts ==============================
platform linux -- Python 3.6.3, pytest-3.3.2, py-1.5.2, pluggy-0.6.0
rootdir: /home/tester/temp/python/python-schema-checks/pytest-voluptuous-demo3, inifile:
plugins: voluptuous-1.0.2, cov-2.5.1
collected 3 items
 
pytest-voluptuous-demo3.py .F.                                           [100%]
 
=================================== FAILURES ===================================
_________________________________ test_uuid_2 __________________________________
 
    def test_uuid_2():
>       assert {"uuid": "00ebf64b-xxxx-4b5d-846a-28971dc05796"} == uuid_response_struct
E       AssertionError: assert failed to validation error(s):
E         - uuid: not a valid value for dictionary value @ data['uuid']
 
pytest-voluptuous-demo3.py:26: AssertionError
====================== 1 failed, 2 passed in 0.63 seconds ======================

10. Validace složitější datové struktury získané přes REST API – první varianta

Na stejném webu, jaký jsme použili v předchozím příkladu, naleznete i další zajímavý REST API endpoint, a to konkrétně na adrese https://httpbin.org/anything. Formát vygenerované odpovědi (opět poslané v JSON formátu) zjistíme příkazem curl:

$ curl https://httpbin.org/anything

Pro curl se vrátí následující struktura:

{
  "args": {},
  "data": "",
  "files": {},
  "form": {},
  "headers": {
    "Accept": "*/*",
    "Connection": "close",
    "Host": "httpbin.org",
    "User-Agent": "curl/7.35.0"
  },
  "json": null,
  "method": "GET",
  "origin": "111.111.111.111",
  "url": "https://httpbin.org/anything"
}

Pokud stejný REST API endpoint použijete v prohlížeči, dostanete odlišná data v sekci „headers“; například. Zbylé klíče ovšem budou shodné:

{
  "args": {},
  "data": "",
  "files": {},
  "form": {},
  "headers": {
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Encoding": "gzip, deflate",
    "Accept-Language": "en-US,en;q=0.5",
    "Connection": "close",
    "Dnt": "1",
    "Host": "httpbin.org",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36"
  },
  "json": null,
  "method": "GET",
  "origin": "111.111.111.111",
  "url": "https://httpbin.org/anything"
}

Podobně můžeme JSON s datovou strukturou získat přímo z Pythonu:

#!/usr/bin/env python
# vim: set fileencoding=utf-8
import requests
 
response = requests.get("https://httpbin.org/anything").json()
print(response)

V tomto případě získáme ještě více dat v sekci „headers“ (REST API odpovídá údaji, které on nás získal):

{
  "args": {},
  "data": "",
  "files": {},
  "form": {},
  "headers": {
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Encoding": "gzip, deflate, br",
    "Accept-Language": "en-US,en;q=0.5",
    "Cache-Control": "max-age=0",
    "Connection": "close",
    "Cookie": "_gauges_unique_day=1; _gauges_unique_month=1; _gauges_unique_year=1; _gauges_unique=1",
    "Host": "httpbin.org",
    "Referer": "https://httpbin.org/",
    "Upgrade-Insecure-Requests": "1",
    "User-Agent": "Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:57.0) Gecko/20100101 Firefox/57.0"
  },
  "json": null,
  "method": "GET",
  "origin": "213.175.37.10",
  "url": "https://httpbin.org/anything"
}

První test může jednoduše otestovat, zda jsme získali slovník s libovolnými hodnotami. Samotné validační kritérium vypadá takto:

anything_struct = S(dict)

Celý test i s nezbytnými importy:

#!/usr/bin/env python
# vim: set fileencoding=utf-8
 
from pytest_voluptuous import S, Partial, Exact
from voluptuous import Invalid
from voluptuous.validators import All, Length
import requests
import re
 
 
def test_the_anything_endpoint_1():
    anything_struct = S(dict)
    response = requests.get("https://httpbin.org/anything").json()
    assert response == anything_struct

11. Základní validace hodnot uložených ve slovníku

Výše uvedené validační kritérium pravděpodobně čtenáři tohoto článku příliš neocenili, takže se ho pokusme vylepšit. Prvním vylepšením bude explicitní vyjmenování všech klíčů, které se musí v datové struktuře nacházet. U všech klíčů navíc určíme (prozatím nepříliš přesně) i typy hodnot. Povšimněte si, že lze použít i None, což v JSONu odpovídá hodnotě null. A samozřejmě můžeme použít dict pro určení, že nějaký prvek je vloženým slovníkem:

def test_the_anything_endpoint_2():
    anything_struct = S({"args": dict,
                         "data": str,
                         "files": dict,
                         "form": dict,
                         "headers": dict,
                         "json": None,
                         "method": str,
                         "origin": str,
                         "url": str})
    response = requests.get("https://httpbin.org/anything").json()
    assert response == anything_struct

12. Podrobná validace hodnot uložených ve slovníku

Validační kritérium může být samozřejmě mnohem přesnější. Pro validaci hodnoty s URL uložené pod klíčem „url“ můžeme použít třídu Url naimportovanou přímo z knihovny Voluptuous:

from voluptuous import Url

Kritérium se změní následovně:

    anything_struct = S({"args": dict,
                         "data": str,
                         "files": dict,
                         "form": dict,
                         "headers": dict,
                         "json": None,
                         "method": str,
                         "origin": str,
                         "url": Url()})

Dále můžeme u některých hodnot určit větší počet možností. Například pod klíčem „method“ je uloženo jméno HTTP metody, což je sice řetězec, ovšem s omezeným povoleným počtem hodnot. Totéž může platit u klíče „json“, který může obsahovat buď null, nebo nějaký řetězec. V předchozích knihovnách se pro tento účel používala klauzule Or, nyní použijeme klauzuli Any:

from voluptuous import Any

Validační kritérium se nyní změní takto:

    anything_struct = S({"args": dict,
                         "data": str,
                         "files": dict,
                         "form": dict,
                         "headers": dict,
                         "json": Any(None, str),
                         "method": Any("GET", "POST", "PUT", "DELETE"),
                         "origin": origin,
                         "url": Url()})

13. Validace slovníků uložených v jiném slovníku

Pokud se podíváme podrobněji na sekci „headers“ vrácenou v JSONu z webové služby, zjistíme, že obsahuje vnořený slovník, jehož klíči i hodnotami jsou pouze řetězce:

  "headers": {
    "Accept": "*/*",
    "Connection": "close",
    "Host": "httpbin.org",
    "User-Agent": "curl/7.35.0"
  },

Jak se takový slovník validuje, již víme:

S({str:str})

Nyní pouze stačí toto validační kritérium vložit do hlavního validačního kritéria, a to následujícím způsobem:

anything_struct = S({"args": dict,
                     "data": str,
                     "files": dict,
                     "form": dict,
                     "headers": S({str:str}),
                     "json": Any(None, str),
                     "method": Any("GET", "POST", "PUT", "DELETE"),
                     "origin": origin,
                     "url": Url()})

14. Úplný zdrojový kód čtvrtého demonstračního příkladu

Všechny tři varianty postupně vytvářeného validačního kritéria jsou součástí dnešního čtvrtého demonstračního příkladu, jehož zdrojový kód vypadá následovně:

#!/usr/bin/env python
# vim: set fileencoding=utf-8
 
from pytest_voluptuous import S, Partial, Exact
from voluptuous import Invalid, Url, Any
from voluptuous.validators import All, Length
import requests
import re
 
 
def test_the_anything_endpoint_1():
    anything_struct = S(dict)
    response = requests.get("https://httpbin.org/anything").json()
    assert response == anything_struct
 
 
def test_the_anything_endpoint_2():
    anything_struct = S({"args": dict,
                         "data": str,
                         "files": dict,
                         "form": dict,
                         "headers": dict,
                         "json": None,
                         "method": str,
                         "origin": str,
                         "url": str})
    response = requests.get("https://httpbin.org/anything").json()
    assert response == anything_struct
 
 
def origin(value):
    if not re.fullmatch(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", value):
        raise Invalid("wrong input {i}, IP address expected".format(i=value))
 
 
def test_the_anything_endpoint_3():
    anything_struct = S({"args": dict,
                         "data": str,
                         "files": dict,
                         "form": dict,
                         "headers": S({str:str}),
                         "json": Any(None, str),
                         "method": Any("GET", "POST", "PUT", "DELETE"),
                         "origin": origin,
                         "url": Url()})
 
    response = requests.get("https://httpbin.org/anything").json()
    assert response == anything_struct
Poznámka: úplný zdrojový kód tohoto příkladu naleznete na adrese https://github.com/tisnik/python-schema-checks/tree/master/pytest-voluptuous-demo4.

15. Výsledek běhu čtvrtého demonstračního příkladu

Opět si ukažme, jak bude vypadat čtvrtý demonstrační příklad po svém spuštění:

============================= test session starts ==============================
platform linux -- Python 3.4.3, pytest-3.3.1, py-1.5.2, pluggy-0.6.0 -- /usr/bin/python3
cachedir: .cache
rootdir: /home/tester/temp/pytest-voluptuous-demo4, inifile:
plugins: voluptuous-1.0.2
collecting ... collected 3 items
 
pytest-voluptuous-demo2.py::test_the_anything_endpoint_1 PASSED          [ 33%]
pytest-voluptuous-demo2.py::test_the_anything_endpoint_2 PASSED          [ 66%]
pytest-voluptuous-demo2.py::test_the_anything_endpoint_3 PASSED          [100%]
 
=========================== 3 passed in 1.94 seconds ===========================

16. Použití klauzule Optional

V případě, že některý klíč (a k němu přiřazená hodnota) nemusí být ve slovníku uložen, je možné jméno klíče umístit do klauzule Optional. To znamená, že původní validační schéma:

user = S({"name": str,
          "surname": str,
          "id": pos})

je možné změnit takovým způsobem, aby ID uživatele nemuselo být z nějakého důvodu specifikováno:

user2 = S({"name": str,
           "surname": str,
           Optional("id"): pos})

17. Implicitní nastavení volitelných klíčů a konkrétní specifikace klíčů povinných

Někdy se setkáme i s opačnou možností – budeme chtít, aby většina klíčů byla jen volitelná, ovšem některé klíče naopak musí být zadány vždy (samozřejmě i s příslušnými navázanými hodnotami). V tomto případě máme dvě možnosti. Buď otrocky používat klauzuli Optional:

user3 = S({"name": str,
           "surname": str,
           Optional("id"): pos,
           Optional("address"): str,
           Optional("state"): str,
           Optional("zip"): int})

nebo naopak použít klauzuli Required u těch klíčů, které jsou povinné. Navíc ještě musíme při definici validačního kritéria nastavit parametr required na pravdivostní hodnotu False (tím se přepne chápání ostatních klíčů – budou nepovinné):

user4 = S({Required("name"): str,
           Required("surname"): str,
           "id": pos,
           "address": str,
           "state": str,
           "zip": int}, required=False)

S druhou možností se setkáme častěji.

18. Pátý demonstrační příklad a výsledek jeho spuštění

Obě klauzule Optional a Required použijeme v dnešním pátém a současně i posledním demonstračním příkladu:

CS24_early

#!/usr/bin/env python
# vim: set fileencoding=utf-8
 
from pytest_voluptuous import S, Partial, Exact
from voluptuous import Invalid, Optional, Required
from voluptuous.validators import All, Length
 
 
def pos(value):
    if type(value) is not int or value <= 0:
        raise Invalid("positive integer value expected, but got {v} instead".format(v=value))
 
 
user = S({"name": str,
          "surname": str,
          "id": pos})
 
 
def test1():
    assert user == {"name": "Eda",
                    "surname": "Wasserfall",
                    "id": 1}
 
 
def test2():
    assert user == {"name": "Eda",
                    "id": 1}
 
 
def test3():
    assert user == {"name": "Eda",
                    "surname": "Wasserfall",
                    "id": 0}
 
 
def test4():
    assert user == {"name": "Eda",
                    "surname": "Wasserfall"}
 
 
user2 = S({"name": str,
           "surname": str,
           Optional("id"): pos})
 
 
def test5():
    assert user2 == {"name": "Eda",
                     "surname": "Wasserfall",
                     "id": 1}
 
 
def test6():
    assert user2 == {"name": "Eda",
                     "id": 1}
 
 
def test7():
    assert user2 == {"name": "Eda",
                    "surname": "Wasserfall",
                    "id": 0}
 
 
def test8():
    assert user2 == {"name": "Eda",
                     "surname": "Wasserfall"}
 
 
user3 = S({"name": str,
           "surname": str,
           Optional("id"): pos,
           Optional("address"): str,
           Optional("state"): str,
           Optional("zip"): int})
 
 
def test9():
    assert user3 == {"name": "Eda",
                    "surname": "Wasserfall",
                    "id": 1}
 
 
def test10():
    assert user3 == {"name": "Eda",
                     "surname": "Wasserfall"}
 
 
def test11():
    assert user3 == {"name": "Eda",
                     "surname": "Wasserfall",
                     "zip": 12345}
 
 
user4 = S({Required("name"): str,
           Required("surname"): str,
           "id": pos,
           "address": str,
           "state": str,
           "zip": int}, required=False)
 
 
def test12():
    assert user4 == {"name": "Eda",
                    "surname": "Wasserfall",
                    "id": 1}
 
 
def test13():
    assert user4 == {"name": "Eda",
                     "surname": "Wasserfall"}
 
 
def test14():
    assert user4 == {"name": "Eda",
                     "surname": "Wasserfall",
                     "zip": 12345}

Výsledek běhu příkladu:

============================= test session starts ==============================
platform linux -- Python 3.4.3, pytest-3.3.1, py-1.5.2, pluggy-0.6.0
rootdir: /home/tester/temp/python/python-schema-checks/pytest-voluptuous-demo5, inifile:
plugins: voluptuous-1.0.2
collected 14 items
 
pytest-voluptuous-demo5.py .FFF.FF.......                                [100%]
 
=================================== FAILURES ===================================
____________________________________ test2 _____________________________________
 
    def test2():
>       assert user == {"name": "Eda",
                        "id": 1}
E       AssertionError: assert failed to validation error(s):
E         - surname: required key not provided @ data['surname']
 
pytest-voluptuous-demo5.py:26: AssertionError
____________________________________ test3 _____________________________________
 
    def test3():
>       assert user == {"name": "Eda",
                        "surname": "Wasserfall",
                        "id": 0}
E       AssertionError: assert failed to validation error(s):
E         - id: positive integer value expected, but got 0 instead for dictionary value @ data['id']
 
pytest-voluptuous-demo5.py:31: AssertionError
____________________________________ test4 _____________________________________
 
    def test4():
>       assert user == {"name": "Eda",
                        "surname": "Wasserfall"}
E       AssertionError: assert failed to validation error(s):
E         - id: required key not provided @ data['id']
 
pytest-voluptuous-demo5.py:37: AssertionError
____________________________________ test6 _____________________________________
 
    def test6():
>       assert user2 == {"name": "Eda",
                         "id": 1}
E       AssertionError: assert failed to validation error(s):
E         - surname: required key not provided @ data['surname']
 
pytest-voluptuous-demo5.py:53: AssertionError
____________________________________ test7 _____________________________________
 
    def test7():
>       assert user2 == {"name": "Eda",
                        "surname": "Wasserfall",
                        "id": 0}
E       AssertionError: assert failed to validation error(s):
E         - id: positive integer value expected, but got 0 instead for dictionary value @ data['id']
 
pytest-voluptuous-demo5.py:58: AssertionError
====================== 5 failed, 9 passed in 0.07 seconds ======================

19. Repositář s demonstračními příklady

Všechny demonstrační projekty, které jsme si v dnešním článku popsali, byly uloženy do Git repositáře, který naleznete na adrese https://github.com/tisnik/python-schema-checks. V tabulkách pod tímto odstavcem jsou pro úplnost vypsány odkazy na všechny doposud zmíněné projekty rozdělené podle použité knihovny. Z tohoto důvodu zde naleznete i projekty zmíněné minule a samozřejmě i předminule.

Schemagic

Projekt Stručný popis Cesta
schemagic-demo-1 základní vlastnosti knihovny Schemagic https://github.com/tisnik/python-schema-checks/tree/master/schemagic-demo-1
schemagic-demo-2 konverze prováděné při validaci https://github.com/tisnik/python-schema-checks/tree/master/schemagic-demo-2
schemagic-demo-3 vlastní validační funkce https://github.com/tisnik/python-schema-checks/tree/master/schemagic-demo-3
schemagic-demo-4 vylepšení předchozího příkladu https://github.com/tisnik/python-schema-checks/tree/master/schemagic-demo-4
schemagic-demo-5 validace slovníků https://github.com/tisnik/python-schema-checks/tree/master/schemagic-demo-5
schemagic-demo-6 validace slovníků podruhé https://github.com/tisnik/python-schema-checks/tree/master/schemagic-demo-6

Schema

Projekt Stručný popis Cesta
schema-demo-1 základní vlastnosti knihovny Schema https://github.com/tisnik/python-schema-checks/tree/master/schema-demo-1
schema-demo-2 validace slovníků a dalších typů https://github.com/tisnik/python-schema-checks/tree/master/schema-demo-2
schema-demo-3 validace slovníků a dalších typů https://github.com/tisnik/python-schema-checks/tree/master/schema-demo-3
schema-demo-4 validace slovníků a dalších typů https://github.com/tisnik/python-schema-checks/tree/master/schema-demo-4
schema-demo-5 validace slovníků a dalších typů https://github.com/tisnik/python-schema-checks/tree/master/schema-demo-5
schema-demo-6 validace slovníků a dalších typů https://github.com/tisnik/python-schema-checks/tree/master/schema-demo-6

Voluptuous

Projekt Stručný popis Cesta
voluptuous-demo-1 základní vlastnosti knihovny Voluptuous https://github.com/tisnik/python-schema-checks/tree/master/voluptuous-demo-1
voluptuous-demo-2 validace obsahu slovníků https://github.com/tisnik/python-schema-checks/tree/master/voluptuous-demo-2

pytest_voluptuous

Projekt Stručný popis Cesta
pytest-voluptuous-demo1 validace obsahu slovníků https://github.com/tisnik/python-schema-checks/tree/master/pytest-voluptuous-demo1
pytest-voluptuous-demo2 validace obsahu slovníků https://github.com/tisnik/python-schema-checks/tree/master/pytest-voluptuous-demo2
pytest-voluptuous-demo3 validace struktury získané z REST API https://github.com/tisnik/python-schema-checks/tree/master/pytest-voluptuous-demo3
pytest-voluptuous-demo4 validace složitější struktury získané z REST API https://github.com/tisnik/python-schema-checks/tree/master/pytest-voluptuous-demo4
pytest-voluptuous-demo5 použití klauzulí Optional a Required https://github.com/tisnik/python-schema-checks/tree/master/pytest-voluptuous-demo5

20. Odkazy na Internetu

  1. 7 Best Python Libraries for Validating Data
    https://www.yeahhub.com/7-best-python-libraries-validating-data/
  2. Universally unique identifier (Wikipedia)
    https://en.wikipedia.org/wi­ki/Universally_unique_iden­tifier
  3. UUID objects according to RFC 4122 (knihovna pro Python)
    https://docs.python.org/3­.5/library/uuid.html#uuid­.uuid4
  4. Object identifier (Wikipedia)
    https://en.wikipedia.org/wi­ki/Object_identifier
  5. Digital object identifier (Wikipedia)
    https://en.wikipedia.org/wi­ki/Digital_object_identifi­er
  6. voluptuous na (na PyPi)
    https://pypi.python.org/py­pi/voluptuous
  7. voluptuous (na GitHubu)
    https://github.com/alectho­mas/voluptuous
  8. pytest-voluptuous 1.0.2 (na PyPi)
    https://pypi.org/project/pytest-voluptuous/
  9. pytest-voluptuous (na GitHubu)
    https://github.com/F-Secure/pytest-voluptuous
  10. schemagic 0.9.1 (na PyPi)
    https://pypi.python.org/py­pi/schemagic/0.9.1
  11. Schemagic / Schemagic.web (na GitHubu)
    https://github.com/Mechrop­hile/schemagic
  12. schema 0.6.7 (na PyPi)
    https://pypi.python.org/pypi/schema
  13. schema (na GitHubu)
    https://github.com/keleshev/schema
  14. XML Schema validator and data conversion library for Python
    https://github.com/brunato/xmlschema
  15. xmlschema 0.9.7
    https://pypi.python.org/py­pi/xmlschema/0.9.7
  16. jsonschema 2.6.0
    https://pypi.python.org/py­pi/jsonschema
  17. warlock 1.3.0
    https://pypi.python.org/pypi/warlock
  18. Python Virtual Environments – A Primer
    https://realpython.com/python-virtual-environments-a-primer/
  19. pip 1.1 documentation: Requirements files
    https://pip.readthedocs.i­o/en/1.1/requirements.html
  20. unittest.mock — mock object library
    https://docs.python.org/3­.5/library/unittest.mock.html
  21. mock 2.0.0
    https://pypi.python.org/pypi/mock
  22. An Introduction to Mocking in Python
    https://www.toptal.com/python/an-introduction-to-mocking-in-python
  23. Unit testing (Wikipedia)
    https://en.wikipedia.org/wi­ki/Unit_testing
  24. Unit testing
    https://cs.wikipedia.org/wi­ki/Unit_testing
  25. Test-driven development (Wikipedia)
    https://en.wikipedia.org/wiki/Test-driven_development
  26. Pip (dokumentace)
    https://pip.pypa.io/en/stable/
  27. 5 Differences between clojure.spec and Schema
    https://lispcast.com/clojure.spec-vs-schema/
  28. Schema: Clojure(Script) library for declarative data description and validation
    https://github.com/plumatic/schema
  29. clojure.spec – Rationale and Overview
    https://clojure.org/about/spec

Byl pro vás článek přínosný?

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.