[Cloud Running – I] Creando un dashboard en QuickSight con los datos de Strava

A lo largo de los últimos meses he estado trabajando casi a diario utilizando QuickSight, una herramienta de AWS que nos permite visualizar datos de manera sencilla, completamente SAAS y con la capacidad de acceder a un montón de orígenes de datos, ah, es gratis como parte de la capa gratuita de AWS y tiene su interfaz localizada tanto en castellano como en otros idiomas.

QuickSight no es tan conocido como otras herramientas como Tableau o PowerBI, ya que está menos extendida y presenta una manera muy simple de trabajar con datos, así que quería aprovechar lo aprendido en estos últimos meses y compartirlo en el blog, ya que me parece una herramienta muy interesante sobre todo por la simplicidad de uso.

En este capítulo y a lo largo de esta serie que he llamado Cloud Running, veremos cómo podemos crear un análisis de QuickSight utilizando un conjunto de datos simple, revisando las limitaciones que podamos tener, creando campos calculados y generando nuestras dashboards, y mi plan es continuar la serie complicando el conjunto de datos, jugando con actualizaciones, y finalmente trabajando con orígenes de datos adicionales.

El resultado final de este artículo será el dashboard, o panel como lo denomina QuickSight en su traducción al castellano, que te muestro a continuación:

Subiendo nuestro conjunto de datos de ejemplo

Todo en QuickSight empieza por el conjunto de datos. Un conjunto de datos no deja de ser una información tabulada que analizaremos posteriormente, que puede ir desde ficheros separados por comas, bases de datos relacionales, o fuentes de datos utilizando APIs como Twitter, como podemos ver a continuación en el menú de Administrar datos -> Nuevo conjunto de datos:

Para este ejemplo usaremos un fichero separado por comas que tiene este aspecto:

Este fichero lo obtuve de Strava, que permite una opción de descargar toda nuestra información en un fichero ZIP como parte del proceso de eliminación de nuestra cuenta. Podemos obtener el fichero sin eliminar la cuenta, y es suficientemente completo como para usarlo en este ejemplo.

Una vez subimos nuestro fichero separado por comas a QuickSight, veremos la pantalla de edición de nuestro conjunto de datos:

En esta pantalla podemos cambiar el nombre de los campos, el tipo de dato, seleccionar el separador de nuestro conjunto de datos, así como generar campos calculados, que veremos más adelante.

Un apunte importante: Una de las claves de la rapidez de QuickSight es su motor en memoria SPICE, que permite cargar un conjunto de datos en memoria y acceder a él con gran rapidez, de modo que, al manipular nuestro conjunto de datos no necesitamos acceder a los datos originales, sino que estamos accediendo a una proyección de dichos datos. Esto significa que no siempre tendremos los datos más actualizados, aunque discutiremos actualización en el próximo artículo.

Una vez estemos satisfechos con nuestro conjunto de datos, el siguiente paso es crear un análisis, lo cual nos presenta una pantalla como la siguiente:

Para empezar a generar gráficos, tan solo tenemos que pulsar en los campos que queremos combinar, y el tipo de gráfico que queremos mostrar. Para crear gráficos adicionales, podemos hacer click en el botón + situado en la barra de herramientas:

Si tenemos seleccionada la opción de AutoGraph marcada por el rayo, QuickSight generará un tipo de gráfico adecuado a cada tipo de datos.

Por ejemplo, seleccionar un único campo como distance nos generará un campo de totales:

Una selección de type nos puede generar un gráfico de barras:

Y finalmente la combinación de los campos distance y date nos generaría una serie de tiempo:

Personalizando nuestros gráficos

Además de utilizar la función AutoGraph, podemos seleccionar el tipo de gráfico que queremos, así como los valores que queremos para las diferentes dimensiones, que varía en función del tipo de gráfico:

Tenemos además diferentes opciones de personalización, que podemos ver haciendo click en los diferentes desplegables con forma de V.

A la hora de personalizar las dimensiones, por ejemplo, en los campos de fecha podemos cambiar la granularidad de la agregación o el formato en el que se muestra la información. En el campo valor, podemos utilizar suma, media, contar, contar excluyendo duplicados, max y min, entre otros, así como personalizar el número de decimales y la escala en la que mostramos nuestros datos, teniendo miles, millones, miles de millones y billones, lo que permite mostrar el K que vemos en el gráfico original.

Además de las dimensiones podemos personalizar los gráficos, seleccionando Formato del elemento visual en el desplegable con forma de V. En el caso que nos ocupa, podemos personalizar valores como el intervalo a mostrar, qué hacer cuando faltan datos, el numero de líneas que tienen los ejes, o las etiquetas de los propios datos.

Enriqueciendo nuestros datos con campos calculados

Otra de las posibilidades que nos aporta QuickSight es el uso de campos calculados tanto a nivel de conjunto de datos como de análisis.

Importante: Las funciones que están a nivel de conjunto de datos son diferentes de las que encontramos a nivel de análisis!

Además de las operaciones aritméticas como suma, resta, multiplicación y división, tenemos un conjunto de funciones lógicas como ifelse, matemáticas como round, floor o ceil y de manejo de cadenas de caracteres como trim, replace, concat u otras.

Un ejemplo de estas funciones, ha sido el tiempo total en minutos, que he hecho con la siguiente fórmula simple: {elapsed_time} / 60 y el ritmo (en min/km), que he utilizado el campo calculado anteriormente: {elapsed time in minutes}/{distance}*1000

A nivel de análisis, el conjunto de funciones que podemos utilizar es diferente, ya que incluye, funciones que podemos aplicar al conjunto total, y no fila a fila, entre los que podemos encontrar count, avg, max, min, percentile así como funciones de manejo de fechas, entre otras.

Limitaciones

Una de las limitaciones con las que me he encontrado a la hora de hacer este ejemplo es el soporte limitado para campos de tipo tiempo. Si bien es cierto que podemos manipular fechas, en el ejemplo que estaba buscando, uno de los valores que quería conseguir era ritmo en minutos por kilómetro, utilizando formato de minutos y segundos, algo que en estos momentos no podemos conseguir en QuickSight.

Creando un dashboard o panel

Los análisis nos permiten editar los campos, moverlos y desplazarlos a nuestro antojo, y compartirlos con otros miembros de nuestro equipo para modificarlo. Sin embargo, habrá situaciones en las que queramos un modo de “solo lectura” para compartir con otros perfiles de nuestra empresa.
Para ello podemos publicar un dashboard o panel, donde podemos publicar uno nuevo o reemplazar existentes. Una vez publicado, lo podemos hacer disponible a todos los miembros de la cuenta o bien invitar solamente a personas específicas.

Otras características

QuickSight tiene más características de las que hemos visto en este artículo pero que se me quedan fuera de este, entre las que destacan:

  • Filtros, que nos permiten ver un subconjunto de datos en un gráfico, en un conjunto de gráficos o en todo el análisis.
  • Parámetros, que permiten modificar nuestro análisis utilizando una lista de valores predeterminados o dinámicos, y que podemos asociar a filtros y controles para modificar nuestro análisis de manera sencilla.
  • Acciones de URL, que permiten enlazar una página web externa utilizando un formato que especifiquemos, por ejemplo, podríamos enlazar el ID de actividad de Strava para que nos llevara directamente a la URL de la misma.
  • Historia en la cual podemos definir varias etapas del mismo análisis, útil cuando queremos tener varios tipos de filtros aplicados en cada una de las historias, y queremos hacer una narrativa de cómo han cambiado los números por trimestre fiscal, por ejemplo.

Conclusiones

QuickSight como hemos visto es una herramienta bastante versátil para análisis de datos, completamente online, y que nos permite crear conjuntos de datos, utilizar campos calculados a nivel de conjuntos de datos y de análisis, crear gráficos y personalizarlos a nuestro gusto, así como cómo guardar y compartir las vistas que creamos.

En el próximo artículo de la serie, siguiendo con la temática de Running, veremos qué pasa cuando tenemos nuevos datos a lo largo del tiempo, y cómo podemos hacer que estos cambios se vean reflejados en QuickSight.

Lecturas adicionales

Una manera diferente de hacer tests en Java con Specnaz

La popularización de lenguajes como Ruby y JavaScript de lado de servidor, ha generado en los últimos años nuevos frameworks de pruebas centrados en lo que se denomina pruebas de especificación, en la cual probamos nuestro código utilizando siempre los casos de uso de dominio, de una manera visual, herramientas como como RSpec, Mocha, o Jest, que siguen una estructura muy similar a la mostrada a continuación.

Supongamos que queremos probar una estructura de tipo Stack:

describe "A Stack"
    describe "pop"
        it "should get the item from the top of the stack"
            //CODE

        it "should get null if stack is empty"
            //CODE			

Esta manera de hacer pruebas contrasta con el estilo que podemos ver en lenguajes como Java en JUnit:

class StackTests

    void test_when_pop_get_item_from_top
        //CODE
    
    void test_when_pop_get_null_if_empty
        //CODE

Aunque son ejemplos muy sencillos, al aumentar la complejidad de nuestra base de código, aumentan también los casos de uso que manejamos, y por tanto el número de tests. Para ello, el estilo de las pruebas de especificación nos permiten agregar casos adicionales reduciendo duplicidad y mejorando la legibilidad.

En el artículo de hoy veremos un framework que nos permitirá hacer este tipo de pruebas en Java, llamado Specnaz y creado por Adam Ruka.

Para este ejemplo utilizaré la kata bowling definida aquí: http://kata-log.rocks/bowling-game-kata.

Nuestro primer test

Para nuestro primer test definimos la primera condición de nuestro juego, que es que un juego vacío devuelva 0 como primer resultado.

...
public class BowlingSpec extends SpecnazJUnit {
    {
        describes("A Game", it -> {
            Game game = new Game();
            it.should("show 0 as score when first created", 
                                () -> assertThat(game.getScore()).isEqualTo(0));
        });
    }
}

Nuestro objeto "Game" tendrá este aspecto inicialmente:

...
public class Game {
    public int getScore() {
        return 0;
    }
}

Inicializadores

Una de las cosas que podemos hacer en nuestros tests, es crear una inicialización antes de cada test, así como una limpieza, esto lo podemos conseguir utilizando el comando de beginsEach como se muestra en el ejemplo:

...
public class BowlingSpec extends SpecnazJUnit {
    {
        describes("A Game", it -> {
            Game game = new Game();
            it.beginsEach(game::reset);
            it.should("show 0 as score when first created", 
                                () -> assertThat(game.getScore()).isEqualTo(0));
        });
    }
}

En vez de crear una instancia nueva de Game, en este ejemplo creamos un método de inicialización dentro del objeto Game que reinicia el estado de la partida. Debido a que estamos en el contexto de una lambda, no podemos redefinir el valor de una variable externa, con lo cual ejecutar el constructor repetidamente no funcionaría

Creando ramas en nuestros tests

Una de las características más interesantes que nos aportan este tipo de tests es la posibilidad de crear ramas con las diferentes condiciones de prueba sobre las que ejecutamos nuestros tests. Veamos un ejemplo:

describes("A Game", it -> {
    ...
    it.describes("when throwing a spare", () -> {
        it.should("account for the number of pins knocked down by next roll", () -> {
            game.roll(4);
            game.roll(6);
            game.roll(4);
            game.roll(0);
            assertThat(game.getScore()).isEqualTo(18);
        });
    });
});

Dentro de cada grupo de "describes" podemos además definir nuestras propias secuencias de inicio y fin de cada test, creando un árbol de casos de uso y evitando repetición innecesaria.

Usando parámetros

En el caso de la kata bowling, una manera muy útil de agrupar los tests es utilizando parámetros, y para ello podemos utilizar una construcción de Specnaz llamada SpecnazParamsJUnit en el caso de que estemos utilizando JUnit como motor de ejecución de tests.

En este ejemplo podemos ver además cómo especificamos los parámetros, y cómo en la cabecera should podemos asignar valores a parámetros:

public class BowlingParamsSpec extends SpecnazParamsJUnit {
    {
        describes("A Parametrized Game", it -> {
            Game game = new Game();
            it.beginsEach(game::reset);

            it.should("properly calculate the gameplays of %1 equal %2", (List<Integer> rolls, Integer expected) -> {
                rolls.forEach(game::roll);
                assertThat(game.getScore()).isEqualTo(expected);
            }).provided(
                    p2(Arrays.asList(2, 3), 5),
                    p2(Arrays.asList(4, 6, 4, 0), 18),
                    p2(Arrays.asList(2, 4, 6, 3), 15),
                    p2(Arrays.asList(10, 3, 6), 28),
                    p2(Arrays.asList(10, 10, 10, 0, 0), 60),
                    p2(Arrays.asList(0, 0, 10, 10, 10), 60),
                    p2(Arrays.asList(10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10), 300)
            );
        });
    }
}

El resultado en nuestra ventana de tests será el siguiente:

should properly calculate the gameplays of [2, 3] equal 5
should properly calculate the gameplays of [4, 6, 4, 0] equal 18
should properly calculate the gameplays of [2, 4, 6, 3] equal 15
should properly calculate the gameplays of [10, 3, 6] equal 28
should properly calculate the gameplays of [10, 10, 10, 0, 0] equal 60
should properly calculate the gameplays of [0, 0, 10, 10, 10] equal 60
should properly calculate the gameplays of [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10] equal 300

Manejando excepciones

No es raro que tengamos que encontrarnos con situaciones más o menos excepcionales cuando creamos nuestros tests, y eso incluye ciertos casos de prueba. Para probar excepciones podemos recurrir a shouldThrow que nos permite controlar el tipo de excepción que lanzamos:

it.shouldThrow(IllegalArgumentException.class, "when trying to roll more than 10 bowls in a single roll", () -> {
   game.roll(11);
});

El resultado incluirá además el detalle del tipo de excepción que estamos intentando controlar en el mensaje que se muestra por la consola:

should throw IllegalArgumentException when trying to roll more than 10 bowls in a single roll

Tanto el ejemplo con parámetros como el ejemplo que acabamos de ver nos libran de cometer errores en la cabecera de los tests, ya que se define la excepción que estamos probando así como los parámetros, de tal manera que si cambiaran, cambiaría el nombre del test.

Conclusiones

Como hemos visto durante el artículo, Specnaz es un framework de pruebas de especificación que nos permite definir nuestras pruebas de una manera robusta, evitando duplicidades y permitiendo escribir con detalle qué estamos probando y bajo qué especificaciones.

Además de los ejemplos de este artículo, en su repositorio de github se pueden ver ejemplos de uso con herramientas como mockito para generar dobles de test y poder simular el comportamiento de otras clases, ya que en raras ocasiones probaremos código que no tenga dependencias.

Puedes aprender más detalles en los siguientes enlaces:

Aprendiendo AWS: Lambda usando VSCode y Python

Continuando con la serie sobre AWS, en este caso he decidido jugar un poco con una de las plataformas del momento, Lambda. Para ello he construido una API con Python para leer y escribir datos de una base de datos, en local y con diferentes capas de test.

¿Qué es Lambda?

Lambda es un producto de AWS que permite ejecutar trozos de código (funciones) de manera aislada de otras aplicaciones. Esto aporta una capa extra de abstracción con todo lo bueno y malo que ello implica, y además únicamente se factura por el tiempo en el que la función se está ejecutando.

Se puede interactuar con Lambda utilizando el editor en la nube que ofrecen, subir el código a S3 usando un formulario, o utilizar sistemas como SAM local para automatizar todo el proceso, y este último es el que se ha usado en este artículo.

El objetivo

Para este pequeño proyecto, el plan consistía en crear al menos un par de funciones que expusieran una API REST, escribir en una base de datos, definir todo lo posible mediante código y configuración y que el despliegue se realizara utilizando la consola.

El ejemplo es un banco con cinco funciones, crear, depositar, retirar, ver saldo, y hacer una transferencia.

Escribiendo la primera función

El lenguaje utilizado en este caso ha sido Python, así que parte del reto ha sido aprender a escribir utilizando la sintaxis del lenguaje de la serpiente:

def setup(event, context):
    payload = json.loads(event['body’])
    account = payload['id']
    bank = Bank()
    bank.save_account({"id": account, "balance": 0})
    return {
        "statusCode": 201
    }

En Lambda, y para el caso particular de Python, se define una función como un método que acepta dos parámetros, event que contiene los datos que se le pasan a la función, y context que contiene información relativa a la ejecución de la misma.

En este ejemplo se puede ver los valores que se pasan a esta función dentro de event

{'httpMethod': 'PUT', 'body': '{"id": "test_alice"}', 'resource': '/account', 'requestContext': {'resourcePath': '/account', 'httpMethod': 'PUT', 'stage': 'prod', 'identity': {'sourceIp': '127.0.0.1:61479'}}, 'queryStringParameters': {}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Connection': 'keep-alive', 'Content-Length': '20', 'Host': 'localhost', 'User-Agent': 'python-requests/2.18.4', 'X-Forwarded-Port': '', 'X-Forwarded-Proto': ''}, 'pathParameters': None, 'stageVariables': None, 'path': '/account', 'isBase64Encoded': False}

Para poder continuar, es necesario interpretar los parámetros. En este caso, el valor body es un JSON con la información de la cuenta, y para ello se crea un objeto a partir de la cadena de texto utilizando json.loads(event['body’])

Esto permite interactuar con los datos que se pasan a la función, que en este caso lo que hace es guardar la nueva cuenta en la clase Bank creada para abstraer toda la información relativa a la base de datos.

Finalmente, la función ha de devolver un objeto que contenga, al menos un campo con el estado HTTP en statusCode, que en este caso es 201 (Created).

Utilizando la misma lógica se puede hacer las operaciones de depósito, retirada, saldo y transferencia, aunque para ello puede resultar interesante abstraer la función que recibe el evento de la lógica de la función, usando una función que se llamará dispatch:

def dispatch(event, context):
    requestContext = event["requestContext"]
    resourcePath = requestContext["resourcePath"]
    method = requestContext["httpMethod"]

    if(method == "GET"):
        payload = event['queryStringParameters']
    else:
        payload = json.loads(event['body'])
    try:

        if(resourcePath == "/account"):
            if(method == "PUT"):
                setup(payload)
                return {
                    "statusCode": 201
                }

            elif(method == "GET"):
                return {
                    "statusCode": 200,
                    "body": get_balance(payload)
                }
        ...

    except Exception:
        return {
            "statusCode": 500
        }

Esta función permite leer los parámetros, interpretar la ruta y llamar a la función específica de la misma, así como hacer un pequeño tratamiento de errores.

Integrando con DynamoDB

Una vez que están todas las funciones definidas, el siguiente paso es escribir la clase Bank que será la que escriba información en la base de datos.
Mantener estas responsabilidades separadas permite, en primer lugar, una mejor capacidad de pruebas, y en segundo lugar, hace al código de la función independiente del proveedor de bases de datos.

import boto3
import os
import uuid

class Bank:

    def __init__(self):
        endpoint = os.environ.get("ENDPOINT")
        if(endpoint is None):
            self.dynamodb = boto3.resource('dynamodb')
        else:
            self.dynamodb = boto3.resource('dynamodb', endpoint_url=endpoint)

        self.accounts = self.dynamodb.Table("Accounts")
        self.transactions = self.dynamodb.Table("Transactions")

    def get_account(self, account_name):
        try:
            result = self.accounts.get_item(Key={'id': account_name})
            return result['Item']
        except:
            return None

    def log_transaction(self, transaction):
        transaction["id"] = str(uuid.uuid1())
        self.transactions.put_item(Item=transaction)

    def save_account(self, account):
        self.accounts.put_item(Item=account)

El único detalle destacable de esta clase, es que comprueba si existe un endpoint para DynamoDB definido, esto permitirá utilizar la misma clase para los tests.

Probando la función

Una vez escrito el código de las funciones y la conexión con la base de datos, el siguiente paso es probar que todo funciona como se espera.

Configurando la base de datos

Para probar la interacción con la base de datos se puede, o bien configurarla utilizando el sdk de aws y el comando aws configure o bien utilizando una base de datos local, que podemos arrancar utilizando el siguiente comando:

java -Djava.library.path=./DynamoDBLocal_lib -jar DynamoDBLocal.jar --inMemory

Esto proporciona una base de datos de DynamoDB en local lista para utilizar. La creación de tablas, en este ejemplo, se realiza como parte de los test unitarios:

table = dynamodb.create_table(
    TableName="Accounts",
    KeySchema=[
        {
            'AttributeName': 'id',
            'KeyType': 'HASH'
        }],
    AttributeDefinitions=[
        {
                'AttributeName': 'id',
                'AttributeType': 'S'
        }],
    ProvisionedThroughput={
        'ReadCapacityUnits': 5,
        'WriteCapacityUnits': 5
})
            table.meta.client.get_waiter('table_exists').wait(TableName='Accounts')

Probando las funciones

Con la base de datos funcionando y las tablas creadas, llega el momento de escribir las diferentes pruebas, como esta, que comprueba que un ingreso ha sido efectivo:

...

def test_deposit(self):
    function.setup({'id': "Bob"})
    function.deposit({'id': "Bob", "amount": 10.0})
    self.assertEqual(function.get_balance({'id': "Bob"}), 10)

Para ejecutar los tests es tan sencillo como escribir python function-tests.pydesde la consola de comandos.

Configurando Lambda

Para convertir el código de las funciones en una API que se pueda consultar vía HTTP se ha recurrido a SAM (Serverless Application Model) local, un conjunto de herramientas para definir, probar y desplegar funciones Lambda.

La plantilla

Una plantilla de SAM no es más que un subconjunto de una plantilla de CloudFormation, que en un artículo anterior comentaba que es una manera de automatizar la creación y actualización de recursos dentro de AWS.

Para este ejemplo la plantilla tiene el siguiente aspecto:

AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Description: 'A simple banking app built on lambdas'

Resources:
  Bank:
    Type: AWS::Serverless::Function
    Properties:
      Handler: function.dispatch
      CodeUri: .
      Runtime: python3.6
      Events:
        CreateAccount:
          Type: Api
          Properties:
            Path: '/account/new'
            Method: put
        GetBalance:
          Type: Api
          Properties:
            Path: '/account'
            Method: get
        Deposit:
          Type: Api
          Properties:
            Path: '/account/deposit'
            Method: post
        Transfer:
          Type: Api
          Properties:
            Path: '/account/transfer'
            Method: post
        Withdraw:
          Type: Api
          Properties:
            Path: '/account/withdraw'
            Method: post
      Policies:
        - Version: '2012-10-17'
          Statement:
            - Effect: Allow
              Action:
                - 'dynamodb:PutItem'
                - 'dynamodb:GetItem'
              Resource: 'arn:aws:dynamodb:*:*:*'

En esta plantilla se define:
– El handler, o función que responderá a la llamada
– Los eventos que dispararán la llamada (varios eventos pueden disparar el mismo handler)
– Las políticas que se aplican (en esta caso necesitamos permisos de lectura y escritura para la base de datos).

Validando y probando la plantilla con SAM

Una vez definidos todos los valores para la plantilla, se puede ejecutar SAM para comprobar que la plantilla es válida:

sam validate

El cliente SAM permite, además, probar la definición de la plantilla de manera local:
sam local start-api

Este comando inicializa un servidor en el puerto 3000 con el que se pueden probar las funciones desde un cliente HTTP (en este ejemplo he utilizado Postman) y ver como la API responde a las diferentes llamadas.

Pruebas de integración

Para comprobar que todo funciona bien antes de subir la plantilla a AWS, he creado un conjunto de tests un tanto diferentes, esta vez para comprobar que los datos se transforman correctamente, llamando al cliente de localhost.

def test_deposit(self):
    r = requests.get('http://localhost:3000/account?id=test_bob')
    initial_bob_balance = float(r.text)

    r = requests.post('http://localhost:3000/account/deposit', data =  json.dumps({'id':'test_bob', 'amount': 100}))
    self.assertEqual(200, r.status_code)

    r = requests.get('http://localhost:3000/account?id=test_bob')
    new_balance = float(r.text)

    self.assertEqual(new_balance, initial_bob_balance + 100)

Para ejecutar los tests es tan sencillo como escribir python integration-tests.pydesde la consola de comandos.

Desplegando la plantilla

Finalmente queda empaquetar la función y desplegarla en la consola de AWS. En primer lugar se empaqueta la función en un fichero zip, se guarda en una carpeta (bucket) de S3 y se actualizan las referencias en el fichero que se ha denominado package.yaml:

sam package --template-file template.yaml --s3-bucket my-bucket --output-template-file packaged.yaml

En segundo lugar se invoca a CloudFormation para crear todos los recursos definidos en el paquete, incluyendo el código fuente y el conjunto de permisos:

sam deploy --template-file packaged.yaml --stack-name my-stack-name --capabilities CAPABILITY_IAM

Una vez se haya desplegado el paquete, se puede ir a la consola de AWS y ver cómo se ha desplegado la función:

Captura de pantalla 2018-05-12 a las 21.00.03

Al hacer click, se puede ver además los diferentes endpoints que API Gateway ha creado. así como las diferentes opciones:

Captura de pantalla 2018-05-13 a las 9.54.07

Con esto la función creada está lista para ser ejecutada.

En resumen

Eh este artículo se ha definido una función Lambda de manera local, utilizando tantas capas de abstracción como hayan sido necesarias. Se han creado diferentes tipos de test para probar los diferentes puntos de integración, y finalmente se ha empaquetado y desplegado la función desde la línea de comandos.

El código está disponible en GitHub.

Sin embargo, se han quedado algunas cosas fuera del alcance del artículo, y quedan como sugerencia al lector para continuar experimentando:

  • Inyección de dependencias y Mocking para hacer tests unitarios
  • Integración con un sistema como Jenkins para ejecutar tests
  • Crear la base de datos utilizando la plantilla de SAM
  • Uso de frameworks como Serverless que agregan una capa de abstracción incluso mayor, aislando de las implementaciones de AWS, Azure, Google Cloud o IBM.

¿Y tú, has hecho alguna prueba con Lambdas u otra plataforma Serverless?

Herramientas utilizadas

  • SAM local para probar la función en local, así como para desplegarla en AWS.
  • Visual Studio Code para editar el código y para interactuar con la API de SAM local usando su consola integrada.
  • DynamoDB Local para emular una base de datos de DynamoDB.
  • Boto 3 el SDK oficial de AWS para Python.
  • Postman para probar las diferentes APIs tanto locales como remotas.

Más documentación y ejemplos

Otros tipos de inyección de dependencias

No es la primera vez que hablo en este blog sobre inyección de dependencias, el patrón que nos permite pasar todas las dependencias que una clase necesita en el constructor en vez de como argumentos en métodos o utilizar clases estáticas.

En el caso de Java, donde últimamente paso la mayoría de mi tiempo, contamos con Spring y Guice como las maneras más conocidas de inyectar dependencias, veamos un ejemplo de Spring.

@Component
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class ClassWithDependencies {
    private final DependentClass dependentInstance;

    public void doSomethingWithDependency(){
        dependentInstance.doSomething();
    }
}

@Component
public class DependentClass {
    public void doSomething(){
        System.out.println(&quot;Doing something&quot;)
    }
}

En este ejemplo, con las anotaciones @Component de Spring y @RequiredArgsConstructor de Lombok (un pre-procesador que nos permite agregar setters, getters y constructores a Java) podemos definir ClassWithDependencies como una clase que recibe DependentClass como parámetro del constructor, y luego operar sobre ella.

En artículos anteriores hemos hablado de otros sistemas como el de ASP.net vNext o incluso crear el nuestro propio. Creando un motor de inyección de dependencias con C#

Es importante destacar que en este ejemplo concreto, la inyección construye una nueva instancia de la clase cada vez que se inyecta, siendo la más sencilla de las opciones de inyección.

Una instancia por ejecución (Singleton)

Sin embargo, podemos personalizar aún más el tipo de inyección que hacemos. Volviendo a nuestro ejemplo anterior, supongamos que DependentClass contiene información de la máquina (memoria, procesador, etc) en la que se ejecuta, y por tanto no cambiará a lo largo de la vida de nuestra aplicación.

En este caso, una nueva instancia de cada clase sería innecesario o incluso costoso, dependiendo del tipo de inyección, para lo cual, en la definición del componente, especificamos qué tipo de inyección queremos

@Component
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class ClassWithDependencies {
    private final MachineContext machineContext;

    public void doSomethingWithDependency(){
        machineContext.getProcessor();
    }
}

@Component
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public class MachineContext {
    public String getProcessor() {
        return &quot;Intel&quot;;
    }
}

Comparemos esto con definir un patrón singleton de manera manual:

public class ClassWithDependencies {
    private final MachineContext dependentInstance;

    public void doSomethingWithDependency(){
        MachineContext.getInstance().getProcessor();
    }
}

public class MachineContext {
    private static MachineContext instance;

    public String getProcessor() {
        return &quot;Intel&quot;;
    }

    private MachineContext() {
    }

    public static MachineContext getInstance(){
        if(instance == null){
            instance = new MachineContext();
        }
        return instance;
    }
}

La principal diferencia de usar Singleton en inyección respecto al singleton estándar que podemos ver en el ejemplo anterior es con inyección de dependencias tenemos las ventajas de singleton y mantenemos la capacidad de probar nuestro sistema, ya que en una prueba de ClassWithDependencies podríamos simular o utilizar un mock de MachineContext (por ejemplo si queremos probar cómo se comporta nuestra aplicación en distintos contextos).

Una instancia por petición (Request)

Una variante de Singleton especialmente para web, ocurre cuando el contexto depende de la petición o la sesión. Para este ejemplo, tenemos una función que se dedica a llevar un registro de las excepciones, y queremos, para cada excepción, registrar además el Id del usuario al que le ocurrió la excepción.

@Component
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class GlobalExceptionHandler {
    private final Request request;

    public void handleException(Exception ex){
        logger.error(request.getUserId(), ex.getStacktrace())
        machineContext.getProcessor();
    }
}

@Component
@Getter
@Setter
@Scope(WebApplicationContext.SCOPE_REQUEST)
public class Request {
    private String userId;
}

Este formato nos permite rellenar la clase Request en la capa del controlador de de nuestra aplicación, y en la otra directamente preguntar por el estado, sin necesidad de tener que pasar el objeto request a lo largo de toda la cadena de llamadas hasta la captura de esta excepción.

Con ello, evitamos que nuestras clases tengan una referencia al objeto únicamente por tener que pasarlo a la siguiente capa, se simplifica el número de parámetros que manejamos y podemos referirnos al contexto siempre que lo necesitemos.

Conclusiones

En este artículo hemos repasado inyección de dependencias de manera tradicional y además hemos visto las ventajas de utilizar construcciones como Singleton y Request.

Y tú, usas estas construcciones cuando desarrollas aplicaciones?

Scala desde la perspectiva de C# y JavaScript, tercera parte

En los artículos anteriores de esta serie, veíamos una pequeña introducción a Scala, y hacíamos una kata para comprobar que habíamos entendido la sintaxis. En este artículo veremos dos construcciones del lenguaje que resultan bastante interesantes, llamadas traits y case classes.

Traits

Podemos entender los traits como una mezcla entre interfaces y clases abstractas, ya que podemos definir un contrato como haríamos con una interfaz, y por otra parte podemos definir también una implementación.

A diferencia de Java, Scala nos permite implementar varias traits, y podemos utilizarlas para agregar características adicionales a nuestro código. Veamos un ejemplo.

En este caso vamos a definir una clase base llamada Employee, que tiene un salario, y luego vamos a definir varios traits, algunos solamente con valores, y otros con pequeñas comprobaciones.

  • El trait Temporary calcula el mes final del contrato
  • El trait Authority nos permite saber si un empleado tiene gente a su cargo
  • El trait Remote nos permite establecer una localización y una zona horaria
  • Finalmente, el trait InOffice nos permite establecer un despacho.

Con estos cuatro traits podemos componer las clases que forman a nuestros empleados, donde tenemos a Developer, a Manager y a Intern:

Como vemos en el ejemplo, podemos utilizar los traits para definir características adicionales a nuestras clases, y podemos también agregar lógica dentro de las mismas, mezclando los conceptos de interfaz y clase abstracta.

Case classes

La otra cara de la moneda son las Case Classes, que podemos encontrar cuando definimos jerarquías, pero sobre todo las podemos encontrar cuando tenemos un número específico de entidades y lo que realmente cambia es lo que hacemos con ellas. Recuperando el mismo caso de empleados, becarios y managers, vamos a redefinirlo como Case Classes:

En este caso hemos simplificado la manera de definir los diferentes datos, y solamente utilizaremos la fecha inicial para Dev y Manager, y el tiempo total de estancia para Intern. En este caso, podemos asumir que nuestro conjunto de datos está fijo.

Sin embargo, ¿qué pasaría si necesitáramos agregar información de nóminas a nuestra aplicación? Si recurriéramos a la herencia, deberíamos implementar un método “salary” o similar, para cada una de las clases, sin embargo, como estamos usando “case classes” podemos crear un único método que las compruebe todas, y se comporte de manera consistente:

En este caso, el método nos devolverá distintos resultados en función de la clase que estemos aplicando y los datos que contenga la misma, permitiendo una separación entre las clases que contienen los datos de los métodos para manipularlos.

Este enfoque tiene sus inconvenientes, y es que si tenemos que agregar un nuevo tipo de dato (por ejemplo, External) tendremos que cambiar todos los métodos auxiliares para soportar el nuevo caso, así que se reduce a elegir en función de las necesidades de nuestro proyecto.

Conclusiones

Construcciones como Case Classes o Traits nos permiten establecer restricciones de una manera sencilla, las traits nos permiten mezclar conceptos de interfaces y clases, y las case classes nos permiten separar los datos de nuestra clase de las transformaciones que hacemos con los mismos.

En C# tenemos a nuestra disposición los métodos de extensión, con los que podemos lograr un comportamiento similar que con las case classes, y que hemos visto con anterioridad en este artículo “Clases abstractas VS interfaces + métodos de extensión“.

En el próximo artículo de la serie veremos más construcciones y, como siempre, más ejemplos.

Para más información:

Importando el calendario de nuestro evento favorito

El próximo 27 y 28 de noviembre estaré un año más en el Codemotion, un evento que se celebra en Madrid reúne comunidades de todo tipo y del que hemos hablado en otros artículos de años anteriores.

En ellas desarrolladores de .NET, Java y la JVM, Ruby, Python, JavaScript, Objective-C, Swift, PHP y otros, se reunen para enseñarnos lo mejor de todos los mundos. Yo ya tengo mi entrada, si no tienes la tuya ya estás tardando

Sin embargo hay algo que sigo sin entender. Al igual que en otros eventos, tenemos disponible la agenda completa en la web, y probablemente el primer día nos entregarán una copia EN PAPEL que acabará en una papelera varios días después del evento, si no lo perdemos antes.

No sería mucho más conveniente poder tener el calendario oficial del evento en un formato que lo pudiera entender nuestro Google Calendar, Outlook, iCal o cliente que utilicemos, y poder usar nuestro móvil para orientarnos por el evento? Pues bien, eso es lo que he hecho, y es lo que veremos en este artículo:

Primer paso, obtener los datos

Para obtener los datos originales tenemos muchas maneras. Una de ellas es utilizar un servicio como Kimono que nos permite crear una API REST a partir de una página ya existente, lo cual nos puede resultar muy útil para extraer información.

Otra opción consiste en investigar un cómo, y qué datos carga la página. En el caso de Codemotion, la agenda se carga de manera asíncrona, así que tras investigar el panel de red de Chrome podemos ver un fichero en formato JSON que contiene, para nuestro disfrute, la agenda completa a nuestra disposición, así que, misión cumplida.

Segundo paso, comprender el formato de salida

Para poder generar un fichero que sea compatible con los diferentes clientes de email, una de las opciones es utilizar el formato icalendar, que define un evento de la siguiente manera:

BEGIN:VEVENT
SUMMARY:Document like a hero using Asciidoctor
DTSTART:20151127T160000Z
DTEND:20151127T164500Z
DESCRIPTION:In 2013, the Spring team decided to migrate the Starting Guides on spring.io ...
END:VEVENT

Analicemos los diferentes componentes, dejando de lado las cabeceras begin y end del evento:

  • Summary: Título que se mostrará en la cita del calendario
  • Dtstart: Fecha y hora de inicio de la cita en la zona horaria UTC, en el siguiente formato: Año Mes Día (T) Hora Minutos Segundos (Z)
  • Dtend: Fecha y hora de fin de la cita, en el mismo formato anteriormente comentado
  • Description: Detalles del evento

Si hemos utilizado el calendario de manera corporativa echaremos de menos temas como invitados, alarmas, etc, aunque todos ellos forman parte del estándar y podemos utilizarlos sin problemas. Para reunir varios eventos en un fichero iCalendar hemos de establecer unas cabeceras y un pie de página de la siguiente manera:

BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//hacksw/handcal//NONSGML v1.0//EN
...
END:VCALENDAR

En este código, definimos nuestro fichero iCalendar, la versión del mismo, y el identificador del generador (que en este caso lo he tomado directamente del ejemplo de la wikipedia). Cerramos el fichero con el marcador End.

Con esta información, ya podemos generar nuestro calendario.

Tercer paso, generar el calendario

Para este ejemplo, como utilizaba un fichero en formato json, me pareció mucho más sencillo utilizar JavaScript y Node.js para generar el fichero ics.

Una de las cosas que nos aporta JavaScript, es que, al ser dinámicamente tipado, podemos abstraer la consola en un objeto creado por nosotros, y asignarlo o no a la consola en función de nuestra necesidad.

Como detalle a tener en cuenta está el uso de saltos de línea, ya que hemos de utilizar \n, de otra manera, se generará una nueva línea en el fichero de texto resultante y tendremos un error al importarlo.

Además, aunque parezca obvio, podemos tener más de un evento en un mismo fichero ics.

Cuarto paso, probarlo

Por último, y no por ello menos importante, es necesario confirmar que nuestro calendario se ha generado correctamente, para lo que podemos utilizar servicios como iCalendar validator. Finalmente recomiendo utilizar alguna aplicación de escritorio (como iCal en Mac) para hacer un par de importaciones y comprobar que todos los eventos se generan correctamente.

Conclusiones

Podemos generar un calendario utilizando el formato ics y teniendo una fuente de origen de datos. Si nuestra fuente está en formato json, podemos utilizar JavaScript de manera sencilla para movernos por el mismo, y generar nuestro fichero de resultado, que, por cierto, tienes disponible públicamente en Github.

Yo ya tengo mi calendario, y tú?

Enlaces adicionales:

Scala desde la perspectiva de C# y JavaScript, segunda parte

En la primera parte de esta serie vimos una primera introducción a Scala como lenguaje de programación definiendo algunas características de su sintaxis. En esta segunda parte pasaremos a la práctica utilizando una Code Kata.

En las artes marciales, se denomina kata a una representación, individual o colectiva, de un conjunto de movimientos. En disciplinas como el Karate se suelen enseñar en exhibiciones. El objetivo de las mismas es repetir los movimientos una y otra vez hasta que los mismos surjan de manera natural.

En programación, denominamos code kata a una definición de un problema relativamente simple, que resolvemos una y otra vez cuando estamos aprendiendo un lenguaje o una técnica como TDD. En este caso elegiremos una kata llamada Karate Chop, que define lo siguiente:

Dada una lista ordenada y un número, devolver la posición del mismo en la lista o -1 si no se encuentra, utilizando búsqueda binaria para ello.

El objetivo en este caso era ver si podía aprender lo mínimo necesario para poder escribir una solución a esta Kata utilizando Scala y utilizando la sintaxis de programación estructurada o orientada a objetos a la que estoy acostumbrado. Tras varias iteraciones agregando tests y condiciones, llegué al siguiente resultado:

Para hacer las pruebas se suele usar un framework como ScalaTest, o recurrir a JUnit o TestNG de Java, que, como hemos mencionado anteriormente, son compatibles. En mi caso me limité a crear una pequeña función auxiliar que simplemente comparaba el resultado de la función y escribía por pantalla si había discrepancias:

Finalmente, para comprobar que el código “funciona” he usado el siguiente conjunto de casos de prueba:

Escribiendo esta función aprendí varias cosas sobre Scala:

  • No necesitamos escribir el tipo de dato, ya que tenemos las palabras reservadas var para variables y val para objetos inmutables. Scala infiere el tipo de dato en tiempo de compilación por nosotros de la misma manera que lo hace C#, de manera que tenemos un sistema de tipos sólido y más sencillo de escribir.

  • Como curiosidades de sintaxis, el tipo de respuesta de una función lo definimos al final de la misma, en la definición de los parámetros también se declara primero el nombre y después el tipo, y el acceso a arrays se realiza mediante paréntesis en vez de corchetes.

#Conclusiones

Lo más importante que podemos ver en este ejemplo, es que no necesitamos cambiar nuestra manera de programar para comenzar a usar Scala, y que podemos ir agregando características funcionales de lenguaje a nuestro código poco a poco.

En el siguiente artículo de la serie volveremos a la teoría, retomando por donde lo habíamos dejado, viendo características como las Case Classes, el reconocimiento de patrones, y otros ejemplos de código, más funcionales que podemos encontrar en GitHub.

Mientras tanto, aquí están algunos enlaces sobre coding katas que he usado para hacer este artículo: