Cómo crear un servicio - UltraTesla/UTesla GitHub Wiki

El proceso para crear un servicio es sumamente sencillo, de hecho, no es nada diferente a crear una clase. Los servicios en Ultra Tesla son manejados de acuerdo a los atributos y métodos que éstos contengan; métodos que son mejor llamarlos Métodos Especiales.

SUPPORTED_METHODS

Es una propiedad que será sumamente usada, ya que es la encargada de "decirle" al núcleo de UTesla qué métodos podrá usar el usuario, sin embargo, el usuario no sabrá sobre lo que es un método, él lo llamará: acción.

Una acción como se mencionó anteriormente es en realidad un método cualquiera, sin embargo, para que sea llamado acción tiene que tener unos requisitos simples: Estar habilitado por SUPPORTED_METHODS y ser un método.

SUPPORTED_METHODS debe retornar una cadena (recomendable cuando solo se quiere habilitar una acción), una tupla o una lista (cuando hay varias acciones).

Por ejemplo:

class Handler:
    def show(self):
        pass

La clase Handler contiene un método, pero aunque el usuario supiera que existe, el núcleo no permitiría su uso, para ello simplemente debemos crear un atributo llamado SUPPORTED_METHODS y habilitarlo.

class Handler:
    def show(self):
        pass

    @property
    def SUPPORTED_METHODS(self):
        return "show"

Ahora show es una acción y podrá ser usaba por el cliente.

Otro ejemplo pero usando más acciones sería:

class Handler:
    def show(self):
        pass

    def get(self):
        pass

    def write(self):
        pass

    @property
    def SUPPORTED_METHODS(self):
        return ("show", "get")

En el ejemplo anterior se habilitó show y get, por lo tanto son acciones, pero write no, por lo que es un método cualquiera y no podrá ser usado por un cliente.

SET_CONTROLLER

Este es un método que al igual que el anterior será sumamente usado. El núcleo de UTesla no modifica la clase o el objeto de ésta al ser importada para no crear una incongruencia o efectos indeseados, por lo que la manera de comunicar la petición actual es a través de la composición.

Básicamente este método sólo recibe un argumento que es un objeto con información y métodos de la petición actual.

Tomando el último ejemplo de la sección anterior:

class Handler:
    def show(self):
        pass

    def get(self):
        pass

    def write(self):
        pass

    def SET_CONTROLLER(self, controller, /):
        print([x for x in dir(controller) if not (x.startswith("_"))])

    @property
    def SUPPORTED_METHODS(self):
        return ("show", "get")

Lo anterior imprimiría en la consola donde se esté ejecutando UTesla todos los atributos del controlador.

Un método muy importante del controlador es write que envía (o escribe) datos hacia el cliente. Por ejemplo:

class Handler:
    async def show(self):
        await self.controller.write("Hello World!")

    def get(self):
        pass

    def write(self):
        pass

    def SET_CONTROLLER(self, controller, /):
        # Se define el controlador para ser usado en los otros métodos
        self.controller = controller

    @property
    def SUPPORTED_METHODS(self):
        return ("show", "get")

INITIALIZER

Este método es muy importante tenerlo en cuenta ya que cuando está definido puede indicarle al núcleo si seguir con la operación del servicio o no.

Por ejemplo:

class Handler:
    async def show(self):
        await self.controller.write("Hello World!")

    def get(self):
        pass

    def write(self):
        pass

    def INITIALIZER(self):
        number = self.controller.data

        if ((number + 1) != 2):
            return False

        return True

    def SET_CONTROLLER(self, controller, /):
        # Se define el controlador para ser usado en los otros métodos
        self.controller = controller

    @property
    def SUPPORTED_METHODS(self):
        return ("show", "get")

En el ejemplo anterior para que pueda seguir operando con lo que el cliente deseó se debe cumplir con cierta condición; en este caso la condición es simplemente que el usuario debe escribir un número que al ser sumado con 1 dé 2.

Se debe retornar un Booleano (True o False); True cuando se quiera seguir operando; False cuando no.

NO_TOKEN_REQUIRED

Como su mismo nombre indica, es una propiedad que se encarga de transmitirle al núcleo qué acciones no requieren de un token de acceso.

Esto es especialmente útil cuando se crea un servicio para generar token's, no obstante hay que ser precavido y saber cuándo necesitarlo y cuándo no. Un ejemplo muy bueno sería el servicio generate_token que se encarga de realizar algunas tareas donde no se requiere de un token de acceso (al menos en la mayoría de acciones).

Por ejemplo:

class Handler:
    async def show(self):
        await self.controller.write("Hello World!")

    def get(self):
        pass

    def write(self):
        pass

    @property
    def NO_TOKEN_REQUIRED(self):
        return "show"

    def INITIALIZER(self):
        number = self.controller.data

        if ((number + 1) != 2):
            return False

        return True

    def SET_CONTROLLER(self, controller, /):
        # Se define el controlador para ser usado en los otros métodos
        self.controller = controller

    @property
    def SUPPORTED_METHODS(self):
        return ("show", "get")

Ahora un cliente puede hacer uso de la acción show sin ajustar un token de acceso.

Nota: Al igual que SUPPORTED_METHODS esta propiedad puede contener una cadena, una lista o una tupla.

IS_ALLOW

Esta propiedad es especialmente útil cuando se está arreglando un servicio o simplemente se desea deshabilitar por alguna razón.

Por ejemplo:

class Handler:
    async def show(self):
        await self.controller.write("Hello World!")

    def get(self):
        pass

    def write(self):
        pass

    @property
    def IS_ALLOW(self):
        return False

    @property
    def NO_TOKEN_REQUIRED(self):
        return "show"

    def INITIALIZER(self):
        number = self.controller.data

        if ((number + 1) != 2):
            return False

        return True

    def SET_CONTROLLER(self, controller, /):
        # Se define el controlador para ser usado en los otros métodos
        self.controller = controller

    @property
    def SUPPORTED_METHODS(self):
        return ("show", "get")

Ahora ningún cliente podrá usar ese servicio hasta que IS_ALLOW retorne True o 1 o simplemente no se coloque.

__init__

Sí, este método también lo toma en cuenta el núcleo y sí, también es un método que Python usa especialmente, aunque el núcleo tiene una utilidad mucho mejor aunque para nada diferente a lo que se haría en la vida real: ¡definir variables!

Tanto nosotros (los que desarrollamos el servicio) como los usuarios pueden definir variables (si así lo diseñamos). Por ejemplo:

class Handler:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    async def show(self):
        result = self.x + self.y

        await self.controller.write("Resultado: %d" % (result))

    def get(self):
        pass

    def write(self):
        pass

    @property
    def IS_ALLOW(self):
        return True

    @property
    def NO_TOKEN_REQUIRED(self):
        return "show"

    def INITIALIZER(self):
        number = self.controller.data

        if ((number + 1) != 2):
            return False

        return True

    def SET_CONTROLLER(self, controller, /):
        # Se define el controlador para ser usado en los otros métodos
        self.controller = controller

    @property
    def SUPPORTED_METHODS(self):
        return ("show", "get")

El cliente ahora necesitaría definir 'x' y 'y' para poder usar el servicio.

Parámetros

La sección anterior mostraba el poder del __init__ de Python en UTesla, pero siendo realistas «¿todas las acciones necesitan tener 'x' y 'y' presentes?», la respuesta es simplemente: No; entonces, ¿cómo usaría esas variables donde en verdad sean requeridas?...

¡Sí!, ¡Usando parámetros en las mismas acciones!. Por ejemplo:

class Handler:
    async def show(self, x, y):
        result = x + y

        await self.controller.write("Resultado: %d" % (result))

    def get(self):
        pass

    def write(self):
        pass

    @property
    def IS_ALLOW(self):
        return True

    @property
    def NO_TOKEN_REQUIRED(self):
        return "show"

    def INITIALIZER(self):
        number = self.controller.data

        if ((number + 1) != 2):
            return False

        return True

    def SET_CONTROLLER(self, controller, /):
        # Se define el controlador para ser usado en los otros métodos
        self.controller = controller

    @property
    def SUPPORTED_METHODS(self):
        return ("show", "get")

Así de simple, ahora el usuario definiría esos parámetros para determinada acción y no para el servicio en sí.

Anotaciones

Otra peculiaridad de UTesla son las anotaciones en Python que aunque sean usadas por humanos para indicar a otros humanos qué tipo de dato usar como valor en determinadas variables, pero como estamos hablando de humano-máquina o sea, usuario-servicio necesitamos tener en cuenta el tipo de dato real que tenemos que usar. Las anotaciones en UTesla ayudan al núcleo a saber qué tipo de dato se espera de un valor y en caso de no ser el esperado, se trata de convertir y en caso de que falle, se le informa al usuario.

Usando el ejemplo de la anterior sección:

class Handler:
    async def show(self, x: int, y: int):
        result = x + y

        await self.controller.write("Resultado: %d" % (result))

    def get(self):
        pass

    def write(self):
        pass

    @property
    def IS_ALLOW(self):
        return True

    @property
    def NO_TOKEN_REQUIRED(self):
        return "show"

    def INITIALIZER(self):
        number = self.controller.data

        if ((number + 1) != 2):
            return False

        return True

    def SET_CONTROLLER(self, controller, /):
        # Se define el controlador para ser usado en los otros métodos
        self.controller = controller

    @property
    def SUPPORTED_METHODS(self):
        return ("show", "get")

Ahora se esperaría que el cliente envíe un entero y no otro, aunque por supuesto, que si es una cadena, pero en ella contiene un número, se convertiría (también aplica para los booleanos).

Si se está acostumbrado a usar la librería typing, olvidela cuando se crea una acción, en su lugar debe optar por usar verdaderos tipos de datos o crear uno, como por ejemplo:

onlyint.py

class OnlyInt(object):
    def __init__(self, v):
        if (isinstance(v, (int, bool))):
            self.v = v

        else:
            raise TypeError("Tipo de dato incorrecto")

    def __repr__(self):
        return "OnlyInt(%d)" % (self.v)

    def __add__(self, o):
        return self.v + o

    def __sub__(self, o):
        return self.v - o

Este ejemplo burdo pero puede que ilustrativo permite sumar y restar y no acepta un tipo diferente que no sea un entero o un booleano.

import onlyint
n = onlyint.OnlyInt(10)
print(n + 2)

Imprimiría:

12

Notas:

  • Las anotaciones en UTesla son usadas en las acciones y el método __init__
  • Cuando no se usan el núcleo esperaría a que fuera cualquier tipo de dato

Prioridad de métodos especiales

Hay que tener en cuenta no solo el funcionamiento de los métodos especiales, también hay saber la prioridad de cada uno de ellos.

  • __init__: Este método de Python como se mencionó anteriormente inicializa y define las variables a utilizar por el resto del servicio, por lo que es el prmero en ser ejecutado.
  • NO_TOKEN_REQUIRED: Propiedad que indica qué acciones no requieren un token de acceso.
  • IS_ALLOW: Propiedad que indica si un servicio está o no habilitado.
  • SUPPORTED_METHODS: Propiedad que indica qué acciones están habilitadas.
  • INITIALIZER: Método que indica si se puede o no continuar.