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 --name app
$ cd fastapi
Versiona el projecte:
$ git init
Modifica la secció [tool.poetry]
del fitxer pyproject.toml
:
[tool.poetry]
package-mode = false
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 app/main.py
:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_message():
return {"message": "Hello World!"}
El codi té dos elements fonamentals:
-
L'aplicació que es crea amb
FastAPI()
i que és guarda en la varibleapp
. -
La funció "externa"
/
declarada amb el "decorator"@app.get("/")
, que s'implementa amb la funció interna Pythonread_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.