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) othreading.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
oThreadSafe
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