El controlador - UltraTesla/UTesla GitHub Wiki

El núcleo de Ultra Tesla no modifica ni la clase ni el objeto en tiempo de ejecución, pero necesita algo para poder comunicarse, por lo que recurre al uso de composición para "enviar" un objeto al método SET_CONTROLLER() del servicio que describe información del cliente, de la petición y también proporciona funcionalidades para manipularla.

Tanto el controlador (self.controller) como el objeto que indica información de la petición (self.controller.request) tienen métodos y propiedades que hacen lo mismo, pero con información diferente de cada uno. Se puede encontrar más información de los métodos y propiedades que se mostrarán a continuación en Cómo crear un cliente:

  • add_header
  • del_header
  • get_header
  • get_status
  • get_status_code
  • reset_status
  • set_status
  • set_status_code
  • write_status
  • write

El controlador (además de los mencionados anteriormente), recibe otros métodos y propiedades a tener en cuenta.

Antes de continuar, se va pretender que se está siguiendo el siguiente o un ejemplo parecido a lo largo del artículo:

class Handler:
    def get(self):
        pass

    def SET_CONTROLLER(self, controller, /):
        self.controller = controller

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

data

Esta propiedad es el último dato transferido por el cliente en una misma sesión.

Se puede acceder de la siguiente manera:

self.controller.data

body

Esta es la propiedad más especial a tener en cuenta, ya que en ella reside un generador asincrónico que permite transferir el control que tiene el núcleo de UTesla al servicio que lo desea.

Esto es especialmente útil cuando se tenga que tener buen rendimiento a la hora de transferir datos, especialmente con grandes cantidades o cuando el servicio requiera ser persistente en la transmisión (como un chat).

De hecho, no solo hay que tenerlo en cuenta porque permite tener el control sobre la recepción de datos, también porque el rendimiento es mucho mejor. Comparemos los siguientes códigos:

(Forma correcta)

class Handler:
    async def get(self):
        print(self.controller.data)

        async for data in self.controller.body:
            print(data)

    def SET_CONTROLLER(self, controller, /):
        self.controller = controller

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

(Forma incorrecta)

class Handler:
    async def get(self):
        print(self.controller.data)

    def SET_CONTROLLER(self, controller, /):
        self.controller = controller

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

El primer código es mucho más eficiente que el segundo porque el núcleo no realiza tantas comprobaciones (como interpretar Los Métodos Especiales, comprobar el servicio a ejecutar, si es correcto, ajustar encabezados, y un montón de otras cosas más); en cambio el primero se límita a ajustar encabezados y otra información del objeto de la petición (self.controller.request), pero nada que pueda ralentizar el servicio.

No quiere decir que el segundo código sea incorrecto del todo, pero depende del objetivo del servicio; como se mencionó anteriormente, el primer código es mejor cuando se requieran transferir cantidades enormes de datos o se requieran conexiones persistentes (como un chat), mientras que el segundo son cosas de "un solo disparo" o en otras palabras: que se usen una sola vez.

De hecho, el combinarlos puede resultar genial:

import os
import aiofiles

root = "/tmp"

class Handler:
    @staticmethod
    def get_correct_filename(filename):
        filename = os.path.basename(filename)

        if not (filename):
            raise ValueError("Nombre de archivo incorrecto")

        return os.path.join(root, filename)

    async def put(self, filename: str):
        try:
            filename = self.get_correct_filename(filename)

        except ValueError:
            await self.controller.write_status(
                -1, "Nombre de archivo incorrecto"
                
            )

        async with aiofiles.open(filename, "wb") as fd:
            await fd.write(self.controller.data)
            await fd.flush()
            os.fsync(fd.fileno())

            async for data in self.controller.body:
                await fd.write(data)
                await fd.flush()
                os.fsync(fd.fileno())

        await self.controller.write(None) # Indicamos que concluimos

    def get(self):
        pass

    def SET_CONTROLLER(self, controller, /):
        self.controller = controller

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

Eso escribiría todo lo que el cliente nos envíe.

Nota: Aunque se haya dicho «Transferir el control que tiene UTesla al servicio» no quiere decir que otro servicio ya no vaya a poder recibir datos; perfectamente lo pueden hacer dos o incluso más servicios, y eso aumentaría el rendimiento.

request

Un objeto de la clase Request() que contiene toda la información del cliente, como:

action (str)

La acción que está ejecutando el cliente. Se puede acceder de la siguiente manera:

self.controller.action

force (bool)

Cuando este valor sea positivo, significa que el cliente desea conectarse a un nodo específico de la red; negativo, cuando no. Se puede acceder de la siguiente manera:

self.controller.force

init_params (dict)

Los parámetros iniciales del servicio que el cliente definió. Se puede acceder de la siguiente manera:

self.controller.init_params

is_guest_user (bool)

Cuando sé es positivo, es un indicativo de que el usuario es un invitado. Se puede acceder de la siguiente manera:

self.controller.is_guest_user

is_packed (bool)

Cuando sé es positivo, el usuario hace que el servidor transfiera los datos usando msgpack; negativo cuando no.

Hay que tener en consideración este valor para transferir datos, ya que cuando no se usa msgpack tampoco se hará uso de lo que nos ofrece (como usar diccionarios, listas, booleanos, enteros, enteros flotantes, etc) entre el cliente y el servidor; es mejor usarlo cuando se transfieren datos de gran magnitud o datos que no requieren todas esas características.

node (Tuple[str, int])

Una tupla con la dirección y el puerto del nodo a conectar. Esta propiedad se debe usar a la misma con force (aunque depende de la infraestructura).

params (dict)

Los parámetros que está usando el cliente en la acción del servicio.

path (str)

La ruta del servicio

real_user (bytes)

El nombre real o el identificador de red del usuario.

token (str)

El token de acceso que está utilizando el usuario. Si alguna acción no requiere de un token de acceso, el valor es None.

token_hash (str)

El token de acceso que "conoce" la base de datos y es un "pequeño" extra de seguridad, ya que la manera en la que está formateado es: SHA3_256(UNHEX(TOKEN)).

user (str)

El nombre de usuario.

userid (int)

El identificador de usuario en la base de datos.

template

Este objeto de la clase CustomTemplate() ayuda a obtener una plantilla determinada por la clave intro de la sección Templates en el archivo de configuración config/UTesla.ini o inclusive generarla con las claves que residan en la sección antes mencionada, para ello cuenta con algunos métodos y propiedades muy útiles como:

generate_template

Este método permite generar una plantilla según las claves en la sección Templates del archivo de configuración config/UTesla.ini. Por ejemplo:

class Handler:
    def get(self):
        print(self.controller.template.generate_template(
            "El usuario %(username)s está usando el servicio %(path)s"
            
        ))

    def SET_CONTROLLER(self, controller, /):
        self.controller = controller

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

Esto imprimiría por la consola donde se esté ejecutando UTesla (y suponiendo que el servicio "/hello_world" y el usuario se llame "administrador"):

El usuario administrador está usando el servicio /hello_world

get_template

Esté método obtiene una plantilla formateada según el valor de la clave intro en la sección Template en el archivo de configuración config/UTesla.ini. Por ejemplo:

class Handler:
    def get(self):
        print(self.controller.template.get_template())

    def SET_CONTROLLER(self, controller, /):
        self.controller = controller

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

También acepta argumentos y claves variables que se usarán en el método siguiente.

set_function_template

Esté método acepta un argumento que es una función; esta función se usará para indicar si se desea o no generar la plantilla.

Esto se debe a cuestiones de rendimiento y determinadas situaciones como en combinación con la librería logging. Por ejemplo:

function = lambda x: x == 1

class Handler:
    def get(self):
        print(self.controller.template.get_template(1))

    def SET_CONTROLLER(self, controller, /):
        self.controller = controller
        self.controller.template.set_function_expression(function)

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

Este código imprimiría la plantilla pre-determinada sólo si cumple con la condición propuesta por la función, la cual es: x == 1 (si el valor pasado es igual a 1).

function = lambda x: x == 1

class Handler:
    def get(self):
        print(self.controller.template.get_template(0))

    def SET_CONTROLLER(self, controller, /):
        self.controller = controller
        self.controller.template.set_function_expression(function)

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

En cambio este útlimo no, porque no es igual a 1.

Como se mencionó anteriormente esto es especialmente útil combinarlo con la librería logging para generar la plantilla en determinadas situaciones, como la siguiente:

import logging

logger = logging.getLogger(__name__)
exp_func = lambda level: logger.isEnabledFor(level)

class Handler:
    def get(self):
        logger.info(
            "%s: %s",
            self.controller.template.get_template(logging.INFO),
            "Hola :D"
            
        )

    def SET_CONTROLLER(self, controller, /):
        self.controller = controller
        self.controller.template.set_function_expression(exp_func)

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

El siguiente código imprime algo como: <Plantilla pre-determinada>: Hola :D

Pero cuando el nivel ajustado no está a la altura del nivel INFO, no se imprime, por ende no se genera y mejor rendimiento cuando no se le requiera.

procs

Los procedimientos son unos pequeños ayudantes para realizar ciertas tareas, como cerrar un archivo un archivo cuando no se le requiera más o usar procesos en paralelo o incluso subprocesos. Todo ello se debe usar con cuidado y en determinadas situaciones que en verdad lo requieran.

Los procedimientos pueden ser tanto globales como locales, lo que quiere decir que mientras sean globales durarán toda la ejecución de UTesla o hasta que el proceso o subproceo concluyan su operación, pero si son locales durarán la sesión del usuario.

Procedures

Procedures es una tupla (hecha con namedtuple) para poder hacer uso de esos procedimientos; ésta tiene dos valores: locals y globals, y éstos a su vez contienen: ProcControl y ProcStream.

ProcControl

Permite crear procesos en paralelo o subprocesos. Éste tiene los siguientes métodos y propiedades:

getTarget

Obtiene el objetivo actual en uso.

El objetivo simplemente indica si se desea usar procesos (processes) o subprocesos (threads), aunque esto se puede cambiar e incluso ajustar en el momento de introducir uno nuevo.

Por ejemplo:

self.controller.procs.locals.ProcControl.getTarget() # processes
self.controller.procs.globals.ProcControl.getTarget() # processes

getInterval

Obtiene el intervalo por el cual se esperará a que un proceso termine de finalizar al enviarle una señal SIGTERM.

setTarget

Ajusta el objetivo.

create

Crea e introduce un nuevo callback a ejecutar. Este método acepta los siguientes argumentos:

  • function (Callable[..., Any]): La función a ejecutar
  • start (bool) : True: Indica si iniciar la función apenas se agregue.
  • *args, **kwargs: Los argumentos y argumentos clave variables para multiprocessing.Proccess (cuando el objetivo sea: processes) o threading.Thread (cuando el objetivo sea: threads).
  • target (Optional[str]) : None: Para indicar el objetivo a utilizar para este callback. Si es None se usa el global.

Ejemplo:

import asyncio

def foo():
    from time import sleep
    
    for i in range(10):
        print("Hola :D")
        sleep(2)

class Handler:
    async def get(self):
        self.controller.procs.locals.ProcControl.create(
            foo, target="processes"
            
        )

    def SET_CONTROLLER(self, controller, /):
        self.controller = controller

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

Eso imprimiría Hola :D cada 2 segundos por 10 iteraciones.

autoRemove

Remueve cada proceso y subproceso terminado.

stop

Termina el proceso o subproceso en ejecución. Acepta dos argumentos:

  • item (Union["multiprocessing.context", "ThreadSafe"]): Un objeto multiprocessing.context o ThreadSafe para terminar.
  • killProc (bool) : False: Válido solo para multiprocessing.context. Permite enviar una señal SIGKILL al proceso.

autoStop

Ejecutar stop para cada item agregado.

clear

Versión sincrónica de AsyncClear()

AsyncClear

Ejecuta autoStop() y autoRemove() juntos.

getProc

Permite obtener el objeto del item.

ProcStream

Este procedimiento permite tener abiertos persistentemente en una sesión o incluso de manera global, aunque si se desea, también se puede abrir un archivo como cualquier otro en el mismo código fuente del servicio, pero se tiene que hacer fuera de la clase (o sea, sobre el ámbito global de Python). Es muy útil cuando tengamos que crear un servicio que necesite leer recurrentemente un archivo o escribir en él. Tiene los siguientes métodos:

set_separator

Ajusta el separador (en caso de que se agregue un nombre del stream con un grupo)

add_stream

Agrega un nuevo flujo.

Recibe tres argumentos:

  • stream (object): Un objeto que contenga un método para poder ser cerrado (.close()). P. ej.: open()
  • name (str): El nombre del flujo
  • group (Optional[str]) : None: El nombre del grupo. Es un argumento clave que cuando está definido se agrega de la siguiente manera: <Nombre del grupo><El separador><El nombre del flujo>

async_close

Cierra un flujo abierto.

Recibe tres argumentos:

  • name (str): El nombre del flujo
  • group (Optional[str]) : None: El nombre del grupo
  • exception (bool): Si hay una excepción, que la muestre.

close

La versión sincrónica de async_close()

async_closeall

Ejecuta async_close() para todos los flujos.

closeall

La versión sincrónica de async_closeall

async_remove

Cierra (por lo tanto ejecuta: async_close) y lo elimina del diccionario donde se almacenan todos los flujos agregados.

remove

Versión sincrónica de async_remove

async_removeall

Ejecuta async_remove por cada flujo agregado.

removeall

Versión sincrónica de async_removeall

pool

TODO