En el extremo opuesto de los lenguajes dinámicamente tipados, se encuentran los lenguajes estáticamente tipados, como C, C++, Java, Rust y Go. En estos lenguajes, el tipo de una variable se determina en el momento de la compilación y no puede cambiar durante la ejecución. El sistema de tipado estático realiza la verificación de tipos antes de la ejecución, detectando los errores en la fase de compilación. De este modo, el compilador previene que el programa se ejecute si existen inconsistencias con los tipos.

A diferencia de lenguajes como JavaScript, Python se clasifica como un lenguaje de tipo "fuerte" dentro de los lenguajes dinámicamente tipados. Sin embargo, a pesar de esta clasificación, Python solo advierte acerca de operaciones ilegales en tiempo de ejecución, no antes de la ejecución del código. Un ejemplo claro de esto sería el intento de sumar un diccionario con un número, lo que en Python generaría un error del tipo TypeError. Esto ocurre solo cuando el código se ejecuta, lo que significa que, como desarrollador, no tienes una advertencia previa acerca de la violación de las reglas del sistema de tipos.

Sin embargo, para mitigar esta falta de control antes de la ejecución, Python introdujo en la versión 3.5 las anotaciones de tipo (type hints), una característica que permite al desarrollador especificar el tipo de las variables en su código. A pesar de que estas anotaciones no afectan el comportamiento del programa en tiempo de ejecución, sí ofrecen varios beneficios clave.

El uso de anotaciones de tipo facilita un desarrollo más rápido, ya que cualquier programador que lea el código sabrá de inmediato el tipo de cada variable. Esto es especialmente útil en proyectos grandes donde la claridad en cuanto a los tipos de datos ayuda a evitar errores. Asimismo, las anotaciones permiten un mejor conocimiento sobre los métodos y propiedades disponibles para cada variable, lo que reduce el riesgo de realizar modificaciones incorrectas en tipos de datos dentro de grandes bases de código.

Además, los editores de código y los entornos de desarrollo integrados (IDE) como Visual Studio Code ofrecen un soporte excelente para la autocompletación cuando se utilizan anotaciones de tipo. Esto reduce significativamente la carga cognitiva del desarrollador, ya que puede confiar en las sugerencias automáticas sin necesidad de buscar información adicional sobre los tipos de datos de las variables.

Otra ventaja importante de las anotaciones de tipo es la generación automática de documentación, como la que ofrece FastAPI. FastAPI utiliza las anotaciones de tipo para generar documentación interactiva y completamente funcional para las API REST, lo que facilita la creación y mantenimiento de sistemas robustos. Este sistema también incluye herramientas de verificación de tipos, como Mypy, que realizan un análisis estático del código y notifican al desarrollador de posibles problemas antes de que el código se ejecute.

Es importante destacar que, si bien las anotaciones de tipo se recomiendan especialmente para proyectos grandes, su uso también es beneficioso en proyectos más pequeños, sobre todo cuando se trabaja con tecnologías como FastAPI o Pydantic. Estas herramientas, profundamente integradas con Python, requieren que el desarrollador esté familiarizado con los conceptos básicos de anotaciones de tipo, pues son fundamentales para asegurar que los flujos de datos mantengan los tipos correctos al entrar y salir del sistema.

En proyectos más complejos, donde los errores de tipo pueden ser difíciles de detectar sin un sistema de control adecuado, las anotaciones de tipo actúan como una solución formal para indicar antes de la ejecución qué tipo de valor se espera de una variable. Esta solución es manejada por herramientas de verificación de tipos como Mypy, que proporcionan un mecanismo para detectar errores de tipo que de otro modo podrían ser muy difíciles de encontrar en grandes bases de código.

En cuanto a la implementación práctica de las anotaciones de tipo en Python, su sintaxis es sencilla y permite hacer anotaciones en funciones y variables. Por ejemplo, se puede definir una función como la siguiente, que toma dos parámetros, name (de tipo str) y times (de tipo int), y no devuelve ningún valor (indicado por -> None):

python
def print_name_x_times(name: str, times: int) -> None: for _ in range(times): print(name)

Si se intenta llamar a esta función con un tipo incorrecto, como pasar un entero en lugar de una cadena, herramientas como Mypy detectarán el error:

bash
mypy chapter3_01.py

El resultado será un mensaje de error que indica que los tipos no son compatibles. Este tipo de retroalimentación, que ocurre antes de la ejecución del programa, ayuda enormemente a detectar problemas y mantener la calidad del código.

Además, las anotaciones de tipo también permiten una flexibilidad adicional, como la capacidad de especificar que una variable puede aceptar varios tipos diferentes. Por ejemplo, en lugar de restringir una variable a un solo tipo, es posible permitir que acepte tanto un int como un str, lo cual es útil en situaciones donde los datos de entrada pueden variar.

Este enfoque de tipado flexible, pero seguro, permite mantener la naturaleza dinámica del lenguaje Python sin sacrificar la precisión y la robustez que ofrecen los sistemas de tipado estático. Las anotaciones de tipo combinan lo mejor de ambos mundos: la flexibilidad del tipado dinámico y la seguridad del tipado estático, mejorando significativamente la calidad y mantenibilidad del código a largo plazo.

¿Cómo configurar una aplicación React con Vite y Tailwind CSS?

El estado actual de la aplicación en desarrollo está marcado por un enfoque innovador que se aleja de las configuraciones tradicionales. A diferencia de otros entornos de desarrollo, Vite separa los módulos de la aplicación en dos categorías: dependencias y código fuente. Utiliza esbuild para el agrupamiento rápido de dependencias y sirve el código fuente utilizando los Módulos ECMAScript (ESM) nativos. Esta metodología resulta en tiempos de inicio y actualización del servidor significativamente más rápidos, lo que incrementa la productividad durante el desarrollo. Además, Vite es compatible con una gran variedad de proyectos, tales como Svelte, Preact, Solid.js, entre otros. A continuación, se detallan los pasos para crear una aplicación React utilizando Vite.

Para comenzar, el primer paso es crear una aplicación sencilla en la que se desarrollará este flujo de trabajo. En un directorio de tu elección, por ejemplo, "chapter5", configura el entorno de trabajo con el comando cd y luego ejecuta el siguiente comando para crear una plantilla de React con Vite:

bash
npm create vite@latest frontend -- --template react

A diferencia de herramientas como Create React App, Vite requiere que instales manualmente todas las dependencias de Node.js. Cambia el directorio de trabajo a /frontend utilizando el siguiente comando:

bash
cd frontend

A continuación, instala las dependencias con el comando:

bash
npm install

Una vez completado este proceso, tendrás un proyecto React debidamente inicializado, listo para ser desarrollado. Aunque es posible iniciar tu proyecto con el comando npm run dev, es recomendable aprovechar esta oportunidad para instalar un marco de CSS, como Tailwind CSS, que facilitará el trabajo con estilos sin tener que lidiar con los estilos predeterminados de Vite. Ejecuta los siguientes comandos para instalar y configurar Tailwind CSS:

bash
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

El primer comando instala Tailwind y las dependencias necesarias, mientras que el segundo crea el archivo tailwind.config.js, el cual será utilizado para ajustar la configuración de Tailwind según las necesidades del proyecto.

Para configurar Tailwind en tu proyecto, reemplaza el contenido del archivo tailwind.config.js con lo siguiente:

javascript
/** @type {import('tailwindcss').Config} */ export default { content: [ "./index.html", "./src/**/*.{js,ts,jsx,tsx}", ], theme: { extend: {}, }, plugins: [], }

Es recomendable eliminar los estilos por defecto que Vite crea en el archivo src/index.css y sustituirlos por las directivas de Tailwind:

css
@tailwind base;
@tailwind components; @tailwind utilities;

Ahora, tu aplicación React está lista con una configuración básica de Tailwind CSS. También es recomendable eliminar el archivo App.css, ya que no lo necesitarás en este proyecto. A continuación, modifica el archivo App.jsx para mostrar una página sencilla. Sustituye el contenido de App.jsx con el siguiente código:

javascript
export default function App() {
return ( <div className="bg-purple-800 text-white min-h-screen p-4 flex flex-col justify-center items-center"> Hello FARM stack! </div> ); }

Luego, en la terminal, inicia tu proyecto React con el siguiente comando:

bash
npm run dev

Si abres un navegador y navegas a http://localhost:5173/, que es el puerto predeterminado de Vite, verás una pantalla púrpura con el texto "Hello FARM stack!" centrado en la página.

En el fondo de esta pantalla, hay código y muchos paquetes que han sido generados por Vite. Puedes examinar este código dentro de la carpeta frontend que Vite ha creado para ti. La carpeta node_modules contiene todas las dependencias de tu proyecto, y no es necesario modificarla, salvo que se necesiten tareas de depuración avanzadas. En la carpeta public, encontrarás algunos archivos genéricos como logotipos PNG y el archivo favicon.ico, los cuales no utilizarás en este proyecto. Esta carpeta se utiliza para almacenar activos estáticos que Vite no procesa, como imágenes y fuentes. Puedes dejarla tal como está o utilizarla más tarde para almacenar archivos que serán servidos directamente a los usuarios sin modificaciones por parte de Vite.

Dentro del directorio src, existe un archivo HTML importante llamado index.html. Este archivo contiene un elemento div con el atributo id="root", el cual es el lugar donde React cargará toda la aplicación. La mayor parte del código de la aplicación se desarrollará dentro del directorio src. El archivo App.jsx que representa la aplicación completa se renderizará dentro de este div en index.html. Esta estructura es necesaria para el enfoque declarativo que React proporciona durante el desarrollo.

En cuanto a la organización del código, es posible adoptar diferentes enfoques según el caso de uso. Por ejemplo, podrías crear carpetas adicionales para componentes o páginas, o agrupar funcionalidades por características específicas. React ofrece múltiples opciones para estilizar aplicaciones, desde las tradicionales hojas de estilo CSS o SASS hasta soluciones modernas como los componentes estilizados. Además, los principales marcos de UI/CSS como Material UI, Bootstrap y Semantic UI tienen versiones de React. En este libro, utilizaremos Tailwind CSS, una opción que muchos desarrolladores prefieren por su enfoque no intrusivo, ideal para definir estilos básicos y simples, pero también capaz de lograr diseños de alta precisión a partir de archivos de diseño como Figma o Adobe XD.

Tailwind CSS es un marco de trabajo orientado a utilidades que traduce las reglas CSS en clases que pueden ser usadas directamente en el marcado HTML. Añadiendo estas clases a los elementos HTML, puedes crear documentos completamente estilizados sin necesidad de escribir CSS adicional. Las clases que componen tu archivo App.jsx incluyen:

  • bg-purple-800: para establecer un fondo morado

  • text-white: para hacer el texto blanco

  • min-h-screen: para hacer que la altura sea de pantalla completa

  • p-4: para agregar un margen interior

  • flex: para convertir el contenedor en un flexbox

  • flex-col: para establecer la dirección del flexbox en vertical

  • justify-center: para alinear los elementos al centro en el eje principal

  • items-center: para centrar los elementos en el eje transversal

La propiedad className corresponde a la sintaxis de JavaScript (JSX), el lenguaje utilizado por React para crear HTML. Visual Studio Code ofrece autocompletado automáticamente al escribir las clases, lo que facilita mucho el proceso de desarrollo.

Con esta configuración básica de React y Tailwind, ya estás listo para comenzar a desarrollar una interfaz de usuario funcional. Si deseas practicar un poco más con Tailwind CSS, intenta crear una página de altura completa con bordes discontinuos y algunos títulos. La siguiente sección se centrará en los componentes fundamentales de React mediante JSX.

¿Cómo integrar autenticación y manejo de usuarios en una API REST con FastAPI?

Para desarrollar una API REST que gestione eficientemente la autenticación de usuarios y el procesamiento de datos, como el manejo de vehículos en una base de datos, es fundamental integrar una lógica robusta de autenticación. Esta integración no solo garantiza la seguridad del sistema, sino que también asegura que solo usuarios autenticados puedan realizar operaciones sensibles, como crear, modificar o eliminar recursos. A continuación, se detalla cómo puedes configurar esta estructura en FastAPI, aprovechando servicios como Cloudinary para el manejo de imágenes y JWT (JSON Web Tokens) para la autenticación.

En primer lugar, la gestión de imágenes, como las fotos de los vehículos, es una de las partes esenciales del sistema. Al implementar una API REST que permita almacenar vehículos con imágenes, se debe permitir que el usuario envíe tanto los datos del vehículo como la imagen correspondiente. Esto se puede hacer utilizando un endpoint tipo POST que reciba estos datos. Para probar este endpoint, se puede usar la documentación interactiva que FastAPI proporciona en la ruta 127.0.0.1:8000/docs, donde el usuario puede seleccionar una imagen y enviarla al campo correspondiente. Es importante no omitir ningún campo, como la marca o el modelo, de lo contrario, se obtendrá un error.

Sin embargo, este tipo de operaciones pueden ser explotadas por usuarios maliciosos si no se implementa un sistema de autenticación. Un usuario no autenticado podría subir imágenes o datos no deseados, lo que llenaría rápidamente el almacenamiento disponible en el servidor, o incluso podría causar un uso indebido de la API. Para evitar estos problemas, es crucial añadir un modelo de usuario que permita la autenticación mediante JWT.

La autenticación debe ser implementada cuidadosamente, comenzando con la creación de una clase encargada de gestionar la seguridad. Esta clase se encargará de cifrar contraseñas, generar tokens JWT y verificar que los usuarios tengan los permisos adecuados para realizar ciertas operaciones en la API. A continuación se presenta un ejemplo de implementación de una clase de autenticación:

python
class AuthHandler: security = HTTPBearer() pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") secret = "FARMSTACKsecretString" def get_password_hash(self, password): return self.pwd_context.hash(password) def verify_password(self, plain_password, hashed_password): return self.pwd_context.verify(plain_password, hashed_password) def encode_token(self, user_id, username): payload = { "exp": datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(minutes=30), "iat": datetime.datetime.now(datetime.timezone.utc), "sub": {"user_id": user_id, "username": username} } return jwt.encode(payload, self.secret, algorithm="HS256") def decode_token(self, token): try: payload = jwt.decode(token, self.secret, algorithms=["HS256"]) return payload["sub"] except jwt.ExpiredSignatureError: raise HTTPException(status_code=401, detail="Signature has expired") except jwt.InvalidTokenError: raise HTTPException(status_code=401, detail="Invalid token")
def auth_wrapper(self, auth: HTTPAuthorizationCredentials = Security(security)):
return self.decode_token(auth.credentials)

Una vez que se ha implementado la clase de autenticación, se pueden crear rutas que permitan a los usuarios registrarse, iniciar sesión y realizar operaciones según su estado de autenticación. Es importante incluir un mecanismo de control de acceso para garantizar que solo los usuarios autenticados puedan interactuar con los recursos protegidos de la API.

El siguiente paso es la creación del modelo de usuario. Esto se logra mediante la definición de un modelo UserModel que contenga información básica sobre el usuario, como su nombre de usuario y la contraseña cifrada:

python
class UserModel(BaseModel):
id: Optional[PyObjectId] = Field(alias="_id", default=None) username: str = Field(..., min_length=3, max_length=15) password: str = Field(...)

También es recomendable crear modelos adicionales para las operaciones de inicio de sesión y obtención de datos del usuario actual. Esto garantiza que el sistema sea flexible y que pueda adaptarse a distintos tipos de acceso a los datos de los usuarios.

En el contexto de los vehículos, se debe modificar el modelo de vehículo para asociarlo con un usuario. Esto se puede hacer añadiendo un campo user_id al modelo CarModel, lo que permitirá que cada vehículo esté relacionado con un usuario específico. De este modo, solo el propietario del vehículo (es decir, el usuario autenticado) podrá modificar o eliminar el vehículo.

python
class CarModel(BaseModel):
id: Optional[PyObjectId] = Field(alias="_id", default=None)
brand:
str = Field(...) make: str = Field(...) year: int = Field(..., gt=1970, lt=2025) cm3: int = Field(..., gt=0, lt=5000) km: int = Field(..., gt=0, lt=500 * 1000) price: int = Field(..., gt=0, lt=100000) user_id: str = Field(...) picture_url: Optional[str] = Field(None)

A continuación, se deben crear las rutas correspondientes para las operaciones de los usuarios, como el registro y el inicio de sesión. Estas rutas estarán protegidas por el sistema de autenticación, y solo los usuarios autenticados podrán acceder a ellas para realizar operaciones sobre los vehículos.

Por último, aunque la implementación de seguridad y la asociación de los vehículos con los usuarios es crucial, no se debe olvidar el manejo adecuado de errores y la validación de entradas. Es fundamental verificar que los datos enviados por los usuarios sean correctos y seguros, y que las imágenes subidas no contengan contenido malicioso que pueda comprometer el sistema.

Con todo esto, la API no solo ofrecerá una estructura segura para gestionar vehículos, sino que también permitirá a los usuarios interactuar con el sistema de manera controlada y autenticada. Sin embargo, siempre es recomendable seguir ampliando y mejorando la seguridad, incluyendo la validación de la entrada de datos, protección contra ataques de inyección, y la implementación de reglas más estrictas de control de acceso.

¿Cómo gestionar la autenticación de usuarios y la integración de servicios en FastAPI?

La clase de autenticación encapsula la lógica necesaria para la autenticación de usuarios, similar a lo mostrado en el capítulo anterior, "Autenticación y Autorización", y crea el correspondiente APIRouter para gestionar a los usuarios, incluyendo el registro, inicio de sesión y verificación. El archivo authentication.py será idéntico al que se usó previamente para mantener la simplicidad.

En dicho archivo, que se ubicará en la raíz del proyecto, se gestiona la codificación y decodificación de JWT (JSON Web Tokens), el cifrado de contraseñas y la inyección de dependencias, tal como se explicó en el capítulo anterior sobre la construcción de un backend con FastAPI. Aquí proporcionamos el contenido del archivo para tu conveniencia:

python
import datetime
import jwt from fastapi import HTTPException, Security from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from passlib.context import CryptContext class AuthHandler: security = HTTPBearer() pwd_context = CryptContext( schemes=["bcrypt"], deprecated="auto" ) secret = "FARMSTACKsecretString" def get_password_hash(self, password): return self.pwd_context.hash(password) def verify_password(self, plain_password, hashed_password): return self.pwd_context.verify(plain_password, hashed_password) def encode_token(self, user_id, username): payload = { "exp": datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(minutes=30), "iat": datetime.datetime.now(datetime.timezone.utc), "sub": {"user_id": user_id, "username": username}, } return jwt.encode(payload, self.secret, algorithm="HS256") def decode_token(self, token): try: payload = jwt.decode(token, self.secret, algorithms=["HS256"]) return payload["sub"] except jwt.ExpiredSignatureError: raise HTTPException(status_code=401, detail="Signature has expired") except jwt.InvalidTokenError: raise HTTPException(status_code=401, detail="Invalid token") def auth_wrapper(self, auth: HTTPAuthorizationCredentials = Security(security)): return self.decode_token(auth.credentials)

Este archivo es crucial, pues contiene la lógica para la codificación y decodificación de tokens, lo que permite autenticar y verificar a los usuarios de manera eficiente. Para manejar las rutas relacionadas con los usuarios, creamos un user.py dentro de la carpeta /routers. Este archivo expondrá tres endpoints: uno para registrar usuarios, otro para iniciar sesión y otro para verificar a los usuarios utilizando un token Bearer en el encabezado.

A continuación, se presenta un ejemplo de la implementación del enrutador para los usuarios:

python
from fastapi import APIRouter, Body, Depends, HTTPException
from fastapi.responses import JSONResponse from authentication import AuthHandler from models import CurrentUser, LoginUser, RegisterUser, User auth_handler = AuthHandler() router = APIRouter() @router.post("/register", response_description="Register user", response_model=CurrentUser) async def register(newUser: RegisterUser = Body(...), response_model=User): newUser.password = auth_handler.get_password_hash(newUser.password) query = { "$or": [{"username": newUser.username}, {"email": newUser.email}]} existing_user = await User.find_one(query) if existing_user is not None:
raise HTTPException(status_code=409, detail=f"{newUser.username} or {newUser.email} already exists")
user =
await User(**newUser.model_dump()).save() return user

Este fragmento muestra cómo registramos un nuevo usuario, verificamos que el nombre de usuario y el correo electrónico no estén en uso y luego guardamos el nuevo usuario en la base de datos. Utilizamos Beanie ODM para manejar la consulta de la base de datos MongoDB y la creación de un nuevo usuario de manera asincrónica. Beanie simplifica el manejo de bases de datos NoSQL como MongoDB gracias a su integración con Pydantic y su enfoque asincrónico.

A continuación, implementamos la ruta de inicio de sesión:

python
@router.post("/login", response_description="Login user and return token")
async def login(loginUser: LoginUser = Body(...)) -> str:
user =
await User.find_one(User.username == loginUser.username) if user and auth_handler.verify_password(loginUser.password, user.password): token = auth_handler.encode_token(str(user.id), user.username) response = JSONResponse(content={"token": token, "username": user.username}) return response else: raise HTTPException(status_code=401, detail="Invalid username or password")

En este caso, la funcionalidad de inicio de sesión utiliza el método find_one de Beanie para buscar al usuario en la base de datos. Si el nombre de usuario y la contraseña coinciden, se genera un token JWT que se devuelve como respuesta.

Por último, se añade la ruta /me, que permite verificar los datos del usuario autenticado:

python
@router.get("/me", response_description="Logged in user data", response_model=CurrentUser)
async def me(user_data=Depends(auth_handler.auth_wrapper)):
currentUser =
await User.get(user_data["user_id"]) return currentUser

Este endpoint recupera los datos del usuario actual utilizando el ID que se encuentra en el token JWT. Utiliza la función auth_wrapper, que es responsable de decodificar el token y extraer la información necesaria.

Es importante destacar que, en este sistema, hemos utilizado JWT para la autenticación de usuarios. Este enfoque ofrece una serie de ventajas, como la descentralización de la autenticación, ya que el servidor no necesita almacenar el estado de sesión del usuario. Los tokens pueden ser fácilmente validados de manera independiente, lo que resulta beneficioso en aplicaciones distribuidas y microservicios.

A lo largo de la implementación, se emplea la tecnología Beanie para manejar las interacciones con la base de datos, lo que facilita el trabajo con MongoDB de una manera eficiente y moderna. Gracias a su integración con Pydantic, Beanie permite una gestión más clara y estructurada de los datos.

A medida que continúes desarrollando el sistema, es esencial recordar que la seguridad de las contraseñas es una prioridad. Siempre debes asegurarte de utilizar un algoritmo de cifrado seguro como bcrypt para almacenar las contraseñas de los usuarios, y validar los tokens de manera adecuada para evitar vulnerabilidades en la autenticación.

Además, la flexibilidad del sistema permite que, en el futuro, puedas integrar nuevos servicios o funcionalidades sin dificultad, como el manejo de archivos o la integración con otros sistemas externos. En este contexto, las API de terceros juegan un papel fundamental, ya que permiten expandir las capacidades de la aplicación, como la carga de imágenes o la interacción con servicios de inteligencia artificial.