angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Retourneer alleen de waarden van een lijst met records uit FastAPI

Door alleen waarden terug te sturen in plaats van woordenboeken minimaliseren we de grootte en tijd van gegevensoverdracht.

6 juli 2023
In API, FastAPI
post main image
https://www.pexels.com/nl-nl/@hellokellybrito/

In Python is alles een klasse, wat betekent dat modelgegevens lijken op een woordenboek. Maar woordenboeken hebben sleutels. En als je een lijst met veel woordenboeken uit FastAPI retourneert, is de grootte van de gegevens, sleutels en waarden, meestal veel meer dan twee keer zo groot als de grootte van de waarden. Een grotere omvang en meer tijd betekent dat onze toepassing niet erg efficiënt is, langzamer dan nodig. Het betekent ook dat het meer energie verbruikt, wat betekent dat het niet erg duurzaam is (klinkt goed ... ugh).

Hieronder presenteer en vergelijk ik twee manieren om gegevens terug te sturen van FastAPI:

  • Een lijst van woordenboeken
  • Een lijst van tuples met woordenboekwaarden

Om het nog spannender te maken, bestaat het antwoord uit een "meta"-gedeelte en een "data"-gedeelte. Je kunt dit zelf proberen, de code staat hieronder. Zoals altijd draai ik op Ubuntu 22.04.

De klasse ListResponse

Ik heb de ListResponse klasse al in een eerdere post genoemd. Wat we willen retourneren zijn twee items, 'meta' en 'data'.

    return {
        'meta': ListMetaResponse(
            page=1,
            per_page=10,
            count=count,
            total=total,
        ),
        'data': <list-of-dicts or list-of-tuples>,
    }

De klasse ListResponse maakt een enkel antwoordobject, met behulp van de klasse ListMetaResponse en het gegevensmodel. Om het te gebruiken:

    response_model=ListResponse(Item)

1. Een lijst met woordenboeken teruggeven

Raadpleeg het FastAPI document 'Response Model - Return Type' voor het model, zie onderstaande links.
We gebruiken het volgende model en de volgende items:

class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: list[str] = []

items = [
    {
        'name': 'Portal Gun', 
        'price': 42.0
    },
    {
        'name': 'Plumbus', 
        'price': 32.0
    },
]

Het retourneren van de gegevens is heel eenvoudig; ik zal je hier niet mee lastigvallen.

2. Een lijst met tuples met de waarden teruggeven.

In dit geval gebruiken we een tuple voor de waarden van een woordenboek. We kunnen geen lijst gebruiken omdat er geen Python typeringsondersteuning is voor positionele items in een lijst. De waarden van het woordenboek moeten op vaste posities in de tuple worden geplaatst. Dit betekent dat het model is, zie hierboven:

Tuple[str, Optional[str], float, Optional[float], Optional[list[str]]]

Om de waarden in de tuples te krijgen is afhankelijk van je toepassing, waar komt de data vandaan. Hier ga ik uit van de 'items' zoals hierboven weergegeven, dit zijn 'incomplete' woordenboeken. Dit kunnen we op de volgende manier verwerken tot een lijst van tuples :

    # extract values from items
    def get_item_values(item):
        d = {
            'name': None,
            'description': None,
            'price': None,
            'tax': None,
            'tags': None,
        } | item
        return tuple(d.values())
    items_values = list(map(get_item_values, items))

Als de 'items' uit een database komen, kun je alle velden in de query selecteren. Het resultaat zal een lijst van tuples zijn en deze bewerking is niet nodig.

De gegevens van JSON vergelijken

1. Lijst van woordenboeken

Voer in een andere terminal uit om de JSON gegevens te krijgen:

curl http://127.0.0.1:8888/items

Resultaat:

{"meta":{"page":1,"per_page":10,"count":2,"total":2},"data":[{"name":"Portal Gun","description":null,"price":42.0,"tax":null,"tags":[]},{"name":"Plumbus","description":null,"price":32.0,"tax":null,"tags":[]}]}

En alleen het gegevensgedeelte:

[{"name":"Portal Gun","description":null,"price":42.0,"tax":null,"tags":[]},{"name":"Plumbus","description":null,"price":32.0,"tax":null,"tags":[]}]

Aantal bytes van de gegevens: 148.

2. Lijst van tuples met de waarden

Om de gegevens van JSON te krijgen:

curl http://127.0.0.1:8888/items-values

Resultaat:

{"meta":{"page":1,"per_page":10,"count":2,"total":2},"data":[["Portal Gun",null,42.0,null,null],["Plumbus",null,32.0,null,null]]}

En alleen het gegevensgedeelte:

[["Portal Gun",null,42.0,null,null],["Plumbus",null,32.0,null,null]]

Aantal bytes van de gegevens: 68.

Dit is een reductie van 54%!

De code

Hieronder staat de code voor het geval je het wilt proberen. Maak een virtual environment en vervolgens:

pip install fastapi
pip install uvicorn

Start de toepassing:

python main.py

Om de items weer te geven, typ je in je browser:

http://127.0.0.1:8888/items

Om de items-waarden te tonen, typ je in je browser:

http://127.0.0.1:8888/items-values

De code:

# main.py
import datetime
from functools import lru_cache
from typing import Any, List, Optional, Tuple, Union

from fastapi import FastAPI, status as fastapi_status
from pydantic import BaseModel, Field, create_model as create_pydantic_model

import uvicorn

# see also:
# https://fastapi.tiangolo.com/tutorial/response-model
class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    tags: list[str] = []

items = [
    {
        'name': 'Portal Gun', 
        'price': 42.0
    },
    {
        'name': 'Plumbus', 
        'price': 32.0
    },
]


class ListMetaResponse(BaseModel):
    page: int = Field(..., title='Pagination page number', example='2')
    per_page: int = Field(..., title='Pagination items per page', example='10')
    count: int = Field(..., title='Number of items returned', example='10')
    total: int = Field(..., title='Total number of items', example='100')


class ListResponse(BaseModel):
    def __init__(self, data_model=None):
        pass

    # KeyError when using the Pydantic model dynamically created by created_model in two Generic Model as response model #3464
    # https://github.com/tiangolo/fastapi/issues/3464
    @lru_cache(None)
    def __new__(cls, data_model=None):
        if hasattr(data_model, '__name__'):
            data_model_name = data_model.__name__
        else:
            data_model_name = '???'
        print(f'data_model_name = {data_model_name}')    
        return create_pydantic_model(
            data_model_name + 'ListResponse',
            meta=(ListMetaResponse, ...),
            data=(List[data_model], ...),
            __base__=BaseModel,
        )


app = FastAPI()


@app.get('/')
def index():
    return 'Hello index'


@app.get(
    '/items', 
    name='Get items',
    status_code=fastapi_status.HTTP_200_OK,
    response_model=ListResponse(Item)
)
async def return_items() -> Any:
    return {
        'meta': ListMetaResponse(
            page=1,
            per_page=10,
            count=len(items),
            total=len(items),
        ),
        'data': items
    }


@app.get(
    '/items-values', 
    name='Get item values',
    status_code=fastapi_status.HTTP_200_OK,
    response_model=ListResponse(Tuple[str, Optional[str], float, Optional[float], Optional[list[str]]]),
)
async def return_items_values() -> Any:
    # extract values from items
    def get_item_values(item):
        d = {
            'name': None,
            'description': None,
            'price': None,
            'tax': None,
            'tags': None,
        } | item
        return tuple(d.values())
    items_values = list(map(get_item_values, items))
    print(f'items_values = {items_values}')

    return {
        'meta': ListMetaResponse(
            page=1,
            per_page=10,
            count=len(items),
            total=len(items),
        ),
        'data': items_values,
    }


if __name__ == "__main__":
    uvicorn.run("main:app", host='127.0.0.1', port=8888, reload=True)

Samenvatting

Het verminderen van de hoeveelheid over te dragen gegevens maakt je applicatie efficiënter en responsiever. Python Typen ondersteunt geen positionele items in een lijst, waardoor we tuples gebruiken. In veel gevallen, zoals het selecteren van gegevens uit een database, hebben we geen bewerking nodig om de waarden te extraheren, omdat de geretourneerde rijen al tuples zijn.

Links / credits

FastAPI - Response Model - Return Type
https://fastapi.tiangolo.com/tutorial/response-model

Pydantic
https://docs.pydantic.dev/latest

Laat een reactie achter

Reageer anoniem of log in om commentaar te geven.

Opmerkingen

Laat een antwoord achter

Antwoord anoniem of log in om te antwoorden.