Un servidor web es pot configurar com un servidor de recursos que s'accedeixen mijançant un conjunt de funcions "externes" o endpoints (API)

Introducció

FastAPI ét permet crear una API REST amb JSON.

Entorn de treball

TODO canviar poetry per old requirements.txt

Crea un projecte nou amb poetry:

$ poetry new fastapi-rest --name app

Modifica la secció [tool.poetry] del fitxer pyproject.toml:

[tool.poetry]
package-mode = false

Crea el fitxer poetry.toml:

[virtualenvs]
in-project = true

En el projecte https://gitlab.com/xtec/python/fastapi-rest tens un exemple.

Versiona el projecte:

$ cd fastapi-rest && git init

Afegeix una dependència amb fastapi:

$ poetry add "fastapi[standard]"
Creating virtualenv server-JjgVsD1k-py3.12 in /home/box/.cache/pypoetry/virtualenvs
Using version ^0.112.2 for fastapi

Updating dependencies
...

Aplicació

Crea el fitxer main.py:

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_message():
    return {"message": "Hello World!"}

El codi té dos elements fonamentals:

  1. L'aplicació que es crea amb FastAPI() i que és guarda en la varible app.

  2. La funció "externa" / declarada amb el "decorator" @app.get("/"), que s'implementa amb la funció interna Python read_message().

Activa l'entorn virtual:

$ poetry shell

Arrencar un servidor de desenvolupament:

$ fastapi dev server/main.py
INFO     Using path server/main.py  
...

L'order fastapi dev llegeix el fitxer main.py, detecta que hi ha una app FastAPI, i arrenca un servidor amb Uvicorn.

Per defecte, fastapi dev està configurat amb "auto-reload" per desenvolupament local.

Tens més informació a FastAPI CLI docs.

Obre una altre terminal, i interactua amb el servidor amb httpie:

$ http localhost:8000
HTTP/1.1 200 OK
content-length: 26
content-type: application/json
date: Thu, 29 Aug 2024 12:39:58 GMT
server: uvicorn

{
    "message": "Hello World!"
}

Pots veure a content-type que el servidor retorna un objecte json.

Read

Un servidor web es composa de diferents funcions "externes" definides mitjançant un "path" (API).

Les funcions més habituals són les que implementen una sol.licitut GET.

Per exemple, modifica la funció externa "/" a "/message":

@app.get("/message")
def read_message():
    return {"message": "Hello World!"}

Ara ja no existeix una funció "externa" / encara que la funció interna sigui la mateixa:

$ http localhost:8000
HTTP/1.1 404 Not Found
content-length: 22
content-type: application/json
date: Thu, 29 Aug 2024 12:44:12 GMT
server: uvicorn

{
    "detail": "Not Found"
}

La funció "externa" ara té el path /message:

$ http localhost:8000/message
HTTP/1.1 200 OK
content-length: 26
content-type: application/json
date: Thu, 29 Aug 2024 12:46:04 GMT
server: uvicorn

{
    "message": "Hello World!"
}

En canvi pots canviar el nom de la funció "Python" i la funció "externa" /message segueix funcionant igual:

@app.get("/message")
def message_hello_world():
    return {"message": "Hello World!"}

Tal com pots verificar:

$ http localhost:8000/message
HTTP/1.1 200 OK
content-length: 26
content-type: application/json
date: Thu, 29 Aug 2024 12:53:33 GMT
server: uvicorn

{
    "message": "Hello World!"
}

Les funcions externes són l'API del servidor!

Obre aquesta adreça amb el navegador: http://localhost:8000/docs.

En aquesta pàgina pots veure la documentació interactiva de l'API (proporcionada per Swagger UI) que es genera de manera automàtica.

Tal com hem explicat abans una funció "externa" ha d'estar implementada per una funció "interna" Python que pot tenir el nom que tu vulguis.

Inclús dos funcions "externes" poden estar implementades per la mateixa funció interna tal com es mostra en aquest exemple:

@app.get("/msg")
@app.get("/message")
def message_hello_world():
    return {"message": "Hello World!"}

Pots veure que el servidor respon a /msg i /message de manera identica:

$ http -b localhost:8000/msg
{
    "message": "Hello World!"
}
$ http -b localhost:8000/message
{
    "message": "Hello World!"
}

Té sentit? Això depèn del programador no de FastAPI.

Paràmetres

La majoria de les funcions, per ser útils, han d’estar parametritzades: han de poder rebre uns paràmetres i produïr un resultat diferent en funció d’aquests paràmetres.

Les funcions "externes" estan definides per un path.

Pots utilitzar una part del path per identificar la funció, i l'altre part del path el pots fer servir per definir els paràmetres d’entrada.

Per exemple, si tens una funció que mostra el perfil de cada empleat pots fer servir el path /employee/{id}, on /employee és el nom extern de la funció i /{id} és l’argument de la funció.

employees = {1: "David", 2: "Dora"}

@app.get("/employee/{id}")
def employee(id: int):
    name = employees[id]
    return {"name": name, "job": "clerk"}

FastAPI sap que una element del path és una variable perquè està definit entre claudàtors {}

Com que la funció "externa" té un argument /{id}, la funció employee()" que implementa la funció "externa" ha de tenir també un argument id de tipus int.

Aquesta funció pot ser invocada externament amb els paths /employee/1 o /employee/2, on 1 i 2 són l’argument id de la funció.

$ http -b localhost:8000/employee/1
{
    "job": "clerk",
    "name": "David"
}

Però que passa si passo un paràmetre que no és un int?

$ http localhost:8000/employee/david
HTTP/1.1 422 Unprocessable Entity
content-length: 149
content-type: application/json
date: Thu, 29 Aug 2024 13:27:29 GMT
server: uvicorn

{
    "detail": [
        {
            "input": "david",
            "loc": [
                "path",
                "id"
            ],
            "msg": "Input should be a valid integer, unable to parse string as an integer",
            "type": "int_parsing"
        }
    ]
}

El servidor torna l'error 422 Unprocessable Entity i un missatge JSON on explica l'error.

I que passa si passo el id d'un treballador que no existeix?

$ http localhost:8000/employee/5
HTTP/1.1 500 Internal Server Error
content-length: 21
content-type: text/plain; charset=utf-8
date: Thu, 29 Aug 2024 13:29:52 GMT
server: uvicorn

Internal Server Error

És produeix en error 500 Internal Server Error perquè estem accedint al dic amb l'operador [] sense verificar que existeix una entrada per la id que ens han passat com argument.

El que hem de fer és modificar el codi per tornar un error 404 Not Found:

from fastapi import FastAPI, HTTPException

app = FastAPI()

employees = {1: "David", 2: "Dora"}

@app.get("/employee/{id}")
def employee(id: int):

    if id not in employees.keys():
        raise HTTPException(status_code=404, detail="Employee not found")
    
    return {"name": employees[id], "job": "clerk"}

Pots veure que ara la implementació de la funció "externa" /employee/{id} funciona correctament:

$ http localhost:8000/employee/5
HTTP/1.1 404 Not Found
content-length: 31
content-type: application/json
date: Thu, 29 Aug 2024 13:35:19 GMT
server: uvicorn

{
    "detail": "Employee not found"
}

Activitats

1.- Crea una funció "externa" /capital/{country} que retorni la capital del país.

TODO

Create

Si vols enviar dades estructurades al servidor per crear un recurs pots utilitzar una sol.licitut POST.

A continuació tens dos funcions REST (read i update):

class Item(BaseModel):
    id: int
    name: str
    price: float


items = {
    1: Item(id=1, name="iPhone 15", price=699.36),
    2: Item(id=2, name="Samsung S23", price=619.00),
}


@app.get("/items/{id}")
def read_item(id: int):

    if id not in items.keys():
        raise HTTPException(status_code=404, detail="Item not found")

    return items[id]


@app.post("/items/")
def create_item(item: Item):
    item.id = max(items) + 1
    items[item.id] = item
    return item

Crea un nou item:

http -b POST localhost:8000/items/ id=0 name="Pixel 0" price=899.00
{
    "id": 3,
    "name": "Pixel 0",
    "price": 899.0
}

Pots veure que el servidor torna l'item amb la id que li ha assignat el servidor.

Si fas una consulta amb aquest id el servidor et torna l'item que has creat:

$ http -b localhost:8000/items/3
{
    "id": 3,
    "name": "Pixel 0",
    "price": 899.0
}

També pots veure que ara la teva API inclou un JSON - Schema per Item:

Activitat

Implementa els mètodes "update" i "delete".

Query

Activitat

1.- Modifica el projecte https://gitlab.com/xtec/python/fastapi-rest ...

2.- Crea una imatge i pujala a Docker Hub.