Poke Queue

Generacion asincrona de reportes.

Index

Classifier Banner



Objetivo General

Es extender y adaptar esta aplicación base, añadiendo nuevas funcionalidades y demostrando tu habilidad para modificar y mejorar un sistema existente que debe permanecer desplegado y funcional en Azure.



Contexto Tecnologico

  1. -Frontend: Next.js (React)

  2. -Backend: API Python con FastAPI desplegada en Azure App Service

  3. -Proceso Asíncrono: Azure Functions (Python Queue Trigger)

  4. -Base de Datos: Azure SQL Database

  5. -Almacenamiento: Azure Blob Storage (para los CSV),

  6. -Azure Queue Storage (para mensajes)

  7. -API Externa: PokeAPI

  8. -Infraestructura como Código (IaC): Terraform (implícito por la mención del repositorio)


Tarea 1: Implementar Eliminación Completa de Reportes

El primer paso para implementar la eliminacion fue crear el correspondiente endpoint:

{domain}/api/request/{id} <- METHOD_POST (Para el registro)

Ese endpoint toma el id y hace una busqueda en la base de datos verificando si el registro existe Si el registro existe entonces ejecuta un metodo que ejecuta un query donde se elimina el registro, este metodo tambien envia un mensaje al queue DeleteQueue luego azure worker o azure function llamado serverless-pokeq-dev toma ese mensaje y procede con la eliminacion del blob file, el cual se hizo mediante el metodo .delete_blob() que esta incluido en el SDK de Azure.



Tarea 2: Enriquecer Reporte con Detalles del Pokémon

Al principio el funcionamiento de la API era el siguiente:


Se hacia una peticion GET a la API de PokeAPI

https://pokeapi.co/api/v2/type/{type} <- METHOD_GET

en donde el parametro {type} es el tipo de pokemon del cual nosotros queremos la lista

La consulta a este endpoint nos devuelve la siguiente informacion:

  1. 1. Nombre del pokemon
  2. 2. URL del pokemon
  3. 3. slot

Para el nuevo funcionamiento: Se necesitaba extraer datos de la API de Pokémon desde dos estructuras diferentes:

  1. Lista básica: Con nombres y URLs de Pokémon
https://pokeapi.co/api/v2/type/{type} <- METHOD_GET

  1. Endpoint detallado: Con stats y abilities tras hacer peticiones HTTP
https://pokeapi.co/api/v2/pokemon/{id} <- METHOD_GET

Inicialmente se cree funciones que mantenían la estructura de un diccionario de la forma:

data_dict = {
    "name": "Pokemon",
    "stats": {"hp": 39, "attack": 52, ...},
    "abilities": [{"blaze": "blaze"}, ...]
}

Sin embargo para poder dejarlo listo de un dataframe es necesario poder ‘Aplanar’ esta estructura

Recoleccion de toda la data


Proceso:

  • Itera sobre lista de Pokémon
  • Aplica extracción individual a cada elemento
  • Maneja fallos individuales mediante un Try Catch sin romper el proceso completo

Aplanamiento de Datos


Problema: DataFrames/CSV no manejan bien estructuras anidadas


Los stats estaban guardados en un dict llamada “stats”. La solución fue romper el diccionario y poner cada stat directamente como una columna separada en la tabla. Es como si tuvieras una carpeta llamada “Calificaciones” con hojas sueltas adentro. En lugar de tener la carpeta, sacas cada hoja y las pones directamente sobre el escritorio, cada una con su propio lugar.

  • Stats: stats.hphp (columna directa)
# Antes
pokemon_data['stats'] = {"hp": 39, "attack": 52, "defense": 43}

# Después - Se promocionan al nivel principal
for stat_item in pokemon_data['stats']:
    stat_name = stat_item['stat']['name']    # 'hp'
    base_stat = stat_item['base_stat']       # 39
    flat_data[stat_name] = base_stat         # flat_data['hp'] = 39

Para Abilities: Expandir horizontalmente


Las abilities eran una lista de elementos complejos (cada ability tenía nombre, si era oculta, y un número de slot). La solución fue crear múltiples columnas para cada ability: una columna para el nombre de la primera ability. Luego lo mismo para la segunda ability, y así sucesivamente.

  • Abilities: Array complejo → múltiples columnas (ability_1_name, ability_1_name, etc.)
# Antes
"abilities": [
    {"name": "blaze", "is_hidden": false, "slot": 1},
    {"name": "solar-power", "is_hidden": true, "slot": 3}
]

# Después - Una columna por cada atributo de cada ability
abilities = extract_abilities(pokemon_data['abilities'])
for i, ability in enumerate(abilities, 1):
    flat_data[f'ability_{i}_name'] = ability['name']        # ability_1_name: blaze
    flat_data[f'ability_{i}_name'] = ability['name']        # ability_1_name: blaze

Manejo de Abilities Variables

Problema: Pokémon tienen diferente número de habilidades

Solución:

# Columnas dinámicas por habilidad
ability_1_name,
ability_2_name
# + columnas resumen
all_abilities: "blaze, solar-power"
total_abilities: 2

Organización de Columnas

Se estableció un orden lógico:

  1. Identificación: name, url
  2. Stats de combate: hp, attack, defense, etc.
  3. Resumen de abilities: total_abilities, all_abilities
  4. Detalles de abilities: ability_1_name, ability_2_name, etc.

name -> url -> hp -> attack -> defense -> special-attack -> special-defense -> speed -> total_abilities -> all_abilities -> ability_1_name -> ability_2_name -> ability_3_name -> etc

DataFrame

El DataFrame nos permite organizar los datos de una manera tabular, lo que nos beneficia porque queremos exportar un CSV

Con los datos previamente aplanados y las columnas tambien ordenadas solo ulilizamos la libreria Pandas creamos el dataframe y poblarlo a partir de nuestra lista.

Exportación

Para la exportacion se utilizo el metodo previamente creado y solo fue cuestion de pasarle el nuevo blob_file

Manejo de Errores

  • Peticiones HTTP con try-catch
  • Continúa procesando aunque falte un Pokémon
  • Mensajes informativos de progreso

Tarea 3: Reportes con Muestreo Aleatorio

Permite al usuario especificar un número máximo de registros aleatorios a incluir en el reporte.

Frontend


Utilizando la libreria radix-ui se utilizo el componente label-input para obtener el tamaño del sample desde el lado del clientes

Backend


Se modifico el endpoint http://{domain}/api/requests/{type}/{sample_size} para obtener mediante un path variable el numero de registros requeridos por el usuario Se modifico el modelo en pydantic de manera que este tuviera un nuevo campo llamado sample_size que es un numero entero mayor o igual a cero (0) Se hizo la modificacion correspondiente en la base de datos y tambien en el procedimiento almacenado para seguir utilizando esa misma funcionalidad solamente con pequeños ajustes

Worker - Azure Function


Mediante la funcion get_request(id) obtenemos un json con los datos de la solicitud, incluyendo el sample size, aprovechamos esto para para validar si el tamaño deseado de la muestra es menor a numero de registros que contiene el tipo de pokemon requerido por el usuario, si llega a ser menor entonces utilizamos la funcion ‘Recortamos’ el arreglo con la lista de pokemones antes de que se agreguen todos los detalles que utilizaremos en el dataframe de la siguiente manera: python sample_pokemon_list = pokemon_list[:random.randint(1, list_len)] si la muestra requerida por el usuario llega a ser mayor que la lista obtenia desde la API de Pokemon entonces devolveremos todos los pokemones del mismo tipo que esten disponibles en la API

Resultado final en el CSV

name,url,hp,attack,defense,special-attack,special-defense,speed,total_abilities,all_abilities,ability_1_name,ability_2_name,ability_3_name
charmander,https://pokeapi.co/api/v2/pokemon/4/,39,52,43,60,50,65,2,"blaze, solar-power",blaze,solar-power,
charmeleon,https://pokeapi.co/api/v2/pokemon/5/,58,64,58,80,65,80,2,"blaze, solar-power",blaze,solar-power,
charizard,https://pokeapi.co/api/v2/pokemon/6/,78,84,78,109,85,100,2,"blaze, solar-power",blaze,solar-power,
instagram icon
instagram
youtube icon
youtube
github icon
github
linkedIn icon
linkedIn