Configurando FastAPI para recibir y devolver camelCase | Programador Web Valencia

Configurando FastAPI para recibir y devolver camelCase

4 minutos

FastAPI

Hay un conflicto entre el formato de variables en Python (snake_case) y el formato de variables en JavaScript (camelCase). En este artículo, te enseñaré cómo configurar FastAPI para no contradecir al PEP8 y hacer feliz a los desarrolladores de JavaScript. O dicho de otra forma, cómo recibir y devolver Camel Case mientras trabajas con Snake Case en Python.

Vamos a partir de 2 necesidades: buscar usuarios y crear usuarios. Cada uno tendrán sus verbos, inputs y outputs.

Verbo Input Parámetros Output
GET Parámetros (query) firstName o lastName Lista de usuarios
POST Body (JSON) firstName, lastName, isActive Confirmación de usuario creado

Si tuvieramos que implementarlo en FastAPI, sin ningún tipo de modificación, tendríamos algo como esto:

from fastapi import FastAPI, Query
from pydantic import BaseModel
from typing import List

app = FastAPI()


@app.get('/users')
async def get_users(first_name: str, last_name: str):
    ...
    return [
        {
            'first_name': 'John',
            'last_name': 'Doe'
        },
        {
            'first_name': 'David',
            'last_name': 'Smith'
        }
    ]

class User(BaseModel):
    first_name: str
    last_name: str
    is_active: bool

@app.post('/users')
async def create_user(user: User):
    ...
    return {
        'message_status': 'User created successfully'
    }

Los retornos están escritos intencionadamente en Snake Case porque estamos simulando la salida de una base de datos, tal vez mediante un ORM.

Tenemos problemas:

  1. Los parámetros de entrada están en Camel Case. Tanto si son query params como si son body params, deberían estar en Camel Case.
  2. Los retornos están en Snake Case.

Resolvamos cada uno de estos problemas.

1. BaseModel a medida

Necesitaremos crear un BaseModel capaz de convertir las claves de un diccionario de Snake Case a Camel Case y viceversa.

Crea un archivo llamado CustomBaseModel.py y añade el siguiente código:

from humps import camelize
from pydantic import BaseModel


def to_camel(string):
    return camelize(string)


class CamelModel(BaseModel):
    class Config:
        alias_generator = to_camel
        population_by_name = True

También necesitaremos instalar la librería humps:

pip3 install pyhumps

Repasemos el código:

  • to_camel es una función que convierte una cadena de texto de Snake Case a Camel Case. Usamos la librería humps para hacer la conversión mediante la función camelize.
  • CamelModel es una clase que hereda de BaseModel. Tiene una clase interna llamada Config que sobreescribe el método alias_generator añadiendo la función to_camel como una función intermedia entre la clave original y la salida.population_by_name habilita la opción de los alias para las claves.

Ahora transformamos los inputs de la función en FastAPI a una clase independiente que herede de CamelModel:

from fastapi import FastAPI, Depends
from CustomBaseModel import CamelModel

app = FastAPI()

class UsersQuery(CamelModel):
    first_name: str
    last_name: str

@app.get('/users')
async def get_users(query: UsersQuery = Depends()):
    ...
    return [
        {
            'first_name': 'John',
            'last_name': 'Doe'
        },
        {
            'first_name': 'David',
            'last_name': 'Smith'
        }
    ]

¡Ya podemos enviar parámetros en Camel Case como query params! Puedes consultar la documentación autogenerada para comprobar que los parámetros se han convertido.

2. Convertir los retornos a Camel Case

Montaremos un decorador llamado @output_keys_to_camel que antes de devolver el resultado de la función, convertirá las claves de los diccionarios de Snake Case a Camel Case.

Crea un fichero llamado decorators.py y añade el siguiente código:

from functools import wraps

def convert_keys_to_camel_case(item: dict | list) -> dict | list:
    """
    Convert the keys of the dictionary to camel case (snake_case to camelCase) in all levels
    """
    if isinstance(item, dict):
        new_dict = {}
        for k, v in item.items():
            new_key = to_camel_case(k)
            new_dict[new_key] = convert_keys_to_camel_case(v)
        return new_dict
    elif isinstance(item, list):
        return [convert_keys_to_camel_case(item) for item in item]
    else:
        return item


def output_keys_to_camel(func):
    """
    Convert the keys of the dictionary to camel case (snake_case to camelCase) in all levels
    """

    @wraps(func)
    async def wrapper(*args, **kwargs):
        output = await func(*args, **kwargs)
        return convert_keys_to_camel_case(output)

    return wrapper
  • convert_keys_to_camel_case es una función que convierte las claves de un diccionario cualquiera de Snake Case a Camel Case. Se llama recursivamente hasta que se conviertan todas las claves.
  • output_keys_to_camel es el decorador. Antes de devolver el resultado de la función, pasa la salida por convert_keys_to_camel_case.

Ya podemos importarlo y utilizarlo.

from fastapi import FastAPI, Depends
from CustomBaseModel import CamelModel
from decorators import output_keys_to_camel # Nueva línea

app = FastAPI()

class UsersQuery(CamelModel):
    first_name: str
    last_name: str

@app.get('/users')
@output_keys_to_camel # Nueva línea
async def get_users(query: UsersQuery = Depends()):
    ...
    return [
        {
            'first_name': 'John',
            'last_name': 'Doe'
        },
        {
            'first_name': 'David',
            'last_name': 'Smith'
        }
    ]

Esta configurado, al menos con el verbo GET. Para el verbo POST, necesitarás hacer algo similar.

3. Convertir un POST Body (JSON), a Camel Case

Continuando el ejemplo anterior, volveremos a utilizar la clase CamelModel. La única diferencia respecto a GET es que en POST irá sin ningún tipo de floritura en la función.

class NewUserModel(CamelModel):
    first_name: str
    last_name: str
    is_active: bool

@app.post('/users')
@output_keys_to_camel
async def create_user(params: NewUserModel):
    ...
    return {
        'message_status': 'User created successfully'
    }

Todo unido quedaría de tal forma.

from fastapi import FastAPI, Depends
from CustomBaseModel import CamelModel
from decorators import output_keys_to_camel

app = FastAPI()

class UsersQuery(CamelModel):
    first_name: str
    last_name: str

@app.get('/users')
@output_keys_to_camel
async def get_users(query: UsersQuery = Depends()):
    ...
    return [
        {
            'first_name': 'John',
            'last_name': 'Doe'
        },
        {
            'first_name': 'David',
            'last_name': 'Smith'
        }
    ]


class NewUserModel(CamelModel):
    first_name: str
    last_name: str
    is_active: bool

@app.post('/users')
@output_keys_to_camel
async def create_user(params: NewUserModel):
    ...
    return {
        'message_status': 'User created successfully'
    }

¡Y listo! Ahora puedes recibir y devolver Camel Case mientras trabajas con Snake Case en Python.

Esta obra está bajo una Licencia Creative Commons Atribución-NoComercial-SinDerivadas 4.0 Internacional.

Atribución/Reconocimiento-NoComercial-SinDerivados 4.0 Internacional

¿Me ayudas?

Comprame un café
Pulsa sobre la imagen

No te sientas obligado a realizar una donación, pero cada aportación mantiene el sitio en activo logrando que continúe existiendo y sea accesible para otras personas. Además me motiva a crear nuevo contenido.

Comentarios

{{ comments.length }} comentarios

Nuevo comentario

Nueva replica  {{ formatEllipsisAuthor(replyComment.author) }}

Acepto la política de Protección de Datos.

Escribe el primer comentario

Tal vez también te interese...