Mostrando entradas con la etiqueta script. Mostrar todas las entradas
Mostrando entradas con la etiqueta script. Mostrar todas las entradas

jueves, 21 de agosto de 2025

WEBPROPv2 (II). Configurar el objeto app de FastHTML (mnu_fh.py). Exception handllers. Beforeware. Mounting folders

0. Introducción

Se ha visto que el módulo que  tiene el "main" es mnu_main.py que ejecuta el servidor web uvicorn al que se el pasa entre otros parámetros el objeto "app".

Pues en este post veremos como se configura el objeto app para que utilice recursos como js o css, utilice rutas estáticas, redirija excepciones y utilice una función para que antes de ir a una ruta o "endpoint" compruebe que el usuario tiene permiso para ello.

1. Configuración del objeto app

Se realiza en el módulo menu_fh.py :

  • Crea la lista de "headers" (líneas 21-43):
    • Utiliza la codificación UTF-8
    • Par un diseño "responsive" utiliza la etiqueta "meta" para asignar toda la anchura del dispositivo y hacer una escalado del 100%
    • Adjunta o enlaza librerias js o css:
      • Font Awesome
      • El css propio para el tree.menu
      • Libreria bootstrap
      • Librería Ag-Grid
  • Crea un manejador de las excepciones 400, 403 y 404 (líneas 45-49)
  • Se define una funcion tipo "Beforeware" que se ejecutará en todos los "endpoints". Se especifica su funcionamiento más adelante (Líneas 52-88)
  • Se montan las carpetas "static" y "downloads" a un path de la app (líneas 93-94)
  • Se añaden estos objetos al objeto app (líneas 86-90)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#1. Imports

from fastapi.staticfiles import StaticFiles
from fasthtml import common as fh


#------Imprescindible para poder importar de otras carpetas (de basicutils)
import sys
from pathlib import Path
path_root1 = Path(__file__).parents[1] # Damos un salto de directorio hacia arriba-> ximoutilsmod
sys.path.append(str(path_root1))
path_root0 = Path(__file__).parents[0] # El mismo directorio
sys.path.append(str(path_root0))
# ------Fin imprescindible

from basicutils import xmother
from session import xmsession


#1. Defining Headers (Styles, Scripts, etc)
my_headers = [
	fh.Meta(charset='UTF-8'),

	# Responsive design:  Uses the width of the device and renders the page at 100% zoom (no auto zoom)
	fh.Meta(name='viewport', content='width=device-width, initial-scale=1'),
	
	# Link libraries of js and css

	#  1. Font Awesome
	fh.Link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css'),
	
	#  2. My Menu-tree library
	fh.Link(rel='stylesheet', href='../static/css/tree-menu.css'),
	
	#  3. Bootstrap
	fh.Link(href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css", rel="stylesheet"),
        fh.Script(src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js"),
        fh.Script(src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js"),
	
	#  4. Ag-Grid
	fh.Script(src="https://cdn.jsdelivr.net/npm/ag-grid-community/dist/ag-grid-community.min.js"),
	
]

exception_handlers={
        400: lambda req, exc: fh.Titled("400: Bad Request: La petició no és correcta."),
	403: lambda req, exc: fh.Titled("403: Forbidden: No té permís a esta pàgina."),
	404: lambda req, exc: fh.Titled("404: Not Found: No existeix esta pàgina!"),
}

#2. Beforeware function to be executed in all endpoints
async def my_before_function(req, tab_id, sess):
	'''
	Beforeware function: checks that the user is authenticated
	'''
	print('Des de beforeware, la URL es:', req.url)
	# 1. Validem que haja tab_id i que l'usuari estiga en la sessió de la BD
	if tab_id is None: tab_id= req.query_params.get('tab_id', None)

	if tab_id is None:	
		try:
			tab_id= (await req.form()).get('tab_id', None)
		except Exception as e:
			print("Error al llegir el tab_id del form:", e)
			tab_id = None
	if tab_id is None:	
		try:
			tab_id= (await req.json()).get('tab_id', None)
		except Exception as e:
			print("Error al llegir el tab_id del JSON:", e)
			tab_id = None	

	my_response,myerror=xmsession.validate(tab_id=tab_id, req=req)
	if (len(myerror.strip())>1):
		print ("redirecting from my_before_function " )
		return my_response

#OJO: Aquelles rutes que comecen per "/softprop/nchk/" no seran comprovades l'usuari
my_skip=[r'/favicon\.ico', r'/static/.*', r'.*\.css', 
		 r'/softprop/nchk/.*',
		 '/softprop/login', '/softprop/validate_login', 
		]
my_beforeware= fh.Beforeware(my_before_function, skip=my_skip)

#3. Creating FastAPI App
app = fh.FastHTML(
	hdrs=my_headers, 
	before=my_beforeware,
	secret_key=xmother.new_id(25), 
	exception_handlers=exception_handlers)

#3. Mounting the static folder for accessing the menu.css
app.mount("/static"   , fh.StaticFiles(directory="static"   ), name="static"   )
app.mount("/downloads", fh.StaticFiles(directory="downloads"), name="downloads")

2. Exception handlers

Es un parámetro del objeto app que és una lista de diccionarios que se le indica el código hhtp del error y lo que se tiene que mostrar. En este caso se muestra una pantalla cuyo título es la descripción del error.

El código se muestra muestra el detalle:

exception_handlers={
        400: lambda req, exc: fh.Titled("400: Bad Request: La petició no és correcta."),
	403: lambda req, exc: fh.Titled("403: Forbidden: No té permís a esta pàgina."),
	404: lambda req, exc: fh.Titled("404: Not Found: No existeix esta pàgina!"),
}


3. La función "beforeware"

Un beforeware es una función que intercepta las peticiones HTTP antes de que lleguen al manejador (es decir, antes del código que atiende /ruta/...), para:
  • Validar autenticación/autorización.
  • Añadir datos a la petición.
  • Redirigir, denegar o registrar información.
  • Omitirse en rutas específicas si se configura.
En este caso, el beforeware comprueba si el usuario está autenticado mediante un tab_id.

Veamos que es lo que contiene el código:
  • Definimos una función asíncrona que recibe como parámetros la "request", el "tab_id" y la "sessión" (Línea 2)
  • Esta función recibe como parámetro el tab_id, y si no se le pasa dicho parámetro lo recoge de la "request" (Línea 8)
  • Si no lo encuentra lo busca primeramente en el FORM i si no en el BODY (Líneas 11-24)
  • Se valida mediante el tab_id con la llamada a la función xmsession.validate () (Línea 26)
  • Se definen aquellas rutas (endpoints) que no hace falta validar con el tab_id, que constituyen una lista de "strings" que puden incluir expresiones regulares. (Líneas 32-35)
  • Se asocia la función anterior como "beforeware global" que actuará en todos los endpoints excepto aquellos que se le indican en el parámetro "skip" (Línea 36)
  • Se le passa el beforeware con el parámetro "before" al objeto "app" (Línea 41)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#2. Beforeware function to be executed in all endpoints
async def my_before_function(req, tab_id, sess):
	'''
	Beforeware function: checks that the user is authenticated
	'''
	print('Des de beforeware, la URL es:', req.url)
	# 1. Validem que haja tab_id i que l'usuari estiga en la sessió de la BD
	if tab_id is None: tab_id= req.query_params.get('tab_id', None)

	# 2. If tab_id is not in the query parameters, we try to get it from the FORM 
	if tab_id is None:	
		try:
			tab_id= (await req.form()).get('tab_id', None)
		except Exception as e:
			print("Error al llegir el tab_id del form:", e)
			tab_id = None
	
	# 3. If tab_id is still None, we try to get it from the JSON body
	if tab_id is None:	
		try:
			tab_id= (await req.json()).get('tab_id', None)
		except Exception as e:
			print("Error al llegir el tab_id del JSON:", e)
			tab_id = None	

	my_response,myerror=xmsession.validate(tab_id=tab_id, req=req)
	if (len(myerror.strip())>1):
		print ("redirecting from my_before_function " )
		return my_response

#OJO: Aquelles rutes que comecen per "/softprop/nchk/" no seran comprovades l'usuari
my_skip=[r'/favicon\.ico', r'/static/.*', r'.*\.css', 
		 r'/softprop/nchk/.*',
		 '/softprop/login', '/softprop/validate_login', 
	]
my_beforeware= fh.Beforeware(my_before_function, skip=my_skip)

#3. Creating FastAPI App
app = fh.FastHTML(
	hdrs=my_headers, 
	before=my_beforeware,
	secret_key=xmother.new_id(25), 
	exception_handlers=exception_handlers)

4. La función xmsession.validate para ver si el usuario tiene permiso

En el módulo xmsession se tiene esta función que :
  • Se crea una ruta de redirección para el caso de error que es "/softprop/login" para que ingrese las credenciales al sistema.
  • El primer error que lnaza es no pasar-le un tab_id
  • Si el tab_id no está presente en las cookies que duran 3600 segundos lanza error
  • Compueba que el tab_id esté en la sessión guardada en la BD 
  • Si hay error lo devuelve a la pantalla de login


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
def validate(tab_id:str, req)->tuple:
	redirection='/softprop/login'
	my_error=""

	# 1. Si es la primera vegada, no tenim tab_id i envia un script per a arreplegar-lo
	if tab_id is None: 
		my_error="No s'ha inclòs el tab_id en el request"
			
	#2. Check if the tab_id is in the cookies
	#   Cookies that has finished the max_age=3600 seconds are not present in the session
	if len(my_error)==0:
		cookies = req.cookies
		if tab_id not in cookies.keys(): 
			my_error="El tab_id no està present en el cookies, pot estar caducat"

	#3. Verifiquem que l'usuari existisca i estiga actiu
	if len(my_error)==0:
		with xmdb.session_local() as db:
			user, my_error=test_session(req, db, tab_id)
	
			if len(user.strip())==0 and len(my_error.strip())==0:
				my_error="Usuari no trobat"
			
			# 4. Guardem la petició de l'usuari per a fer una auditoria
			else:
				user_request=RequestStore(
					description=str(req.url)+'|'+datetime.now(timezone.utc).strftime('%Y%m%d%H%M%S%f'),
					user=user,
					tab_id=tab_id,
					url = str(req.url),
					method=req.method,
					user_agent=get_user_agent(req),
					user_ip=get_user_ip(req),
					is_nginx=get_is_nginx(req)
				)
				user_request=db.merge(user_request)
				db.flush()		
				db.commit()

	# Si hi ha un error redireccionem altra vegada al login
	if len(my_error.strip())>0:		
		a=fh.RedirectResponse(redirection, status_code=303)
		print('enviem a  /softprop/login perque: ' + my_error)	
		return a, my_error  

	return '',''








martes, 19 de noviembre de 2024

Python (XII) Ejecutar un fichero sh (bash de linux) que utilice un entorno virtual y ejecute un programa python. Ojo con los parámetros tipo "regex pattern"!!!

1. Introducción

Pasos:

  1. Desde el programa python creamos un array que contenga la ruta del script (bash de linux) como primer componente. Cada componente restante estará formada por la cadena "clave=valor", donde las comillas dobles forman parte del parámetro. Ojo los espacios en blanco pueden dar problemas, para ello se pueden sustituir por algúna cadena , en mi caso he utilizado "¬spc¬"(sin colillas dobles)
  2. Desde el script de linux verificamos que recibimos obligatoriamente los parámetros "folder", "module","funcion" y "venv". Siendo "folder" la carpeta donde tenemos que situarnos, "venv" la carpeta dentro de folder donde está el entrono virtual, "module" el módulo a llamar y "function" la función del módulo a ejecutar. En el script cambiamos a la carpeta definida en la variable "folder" y activamos el entorno virtual de la variable "venv" y ejecutamos  pyton xmexec.py "module=miModulo" "function=miFuncion" ..... 
  3. El módulo xmexec.py que se encuentra en la carpeta "folder" se encarga de recibir los parámetros, arreglar los mismos sustituyendo "¬spc¬" por espacios en blanco, cargando el módulo indicado en el parámetro "module" y ejecutando la función indicada  "function".

2. Llamada al script desde python

Para ello supongamos que tenemos un diccionario con el nombre del script, del módulo, función a ejecutar, entorno virtual y otros parámetros.
Sustituimos los espacios en blanco por "¬spc¬" y construimos el array para llamar a "subproces.run".
Observar que el modulo se puede definir como:
  • "carpeta.modulo" ó
  • "carpeta/modulo.py"
siendo "carpeta" la carpeta que contien a "modulo.py" que vamos a llamar

El script a llamar está en la carpeta "static/scripts" y se llama "execShell.sh"


import subprocess

# Diccionario que contiene los parámetros PARA EL SHELL SCRIPT
execDict={
	'shell':'static/scripts/execShell.sh',
	'folder':'../02.llibreries',
	'venv':'/static/scripts/llibreries.sh',
	'module':'eni/xmexpeni.py',
	#'module':'eni.xmexpeni',
	'function':'expedientENI',
	'param1':'1814787N',
	'param2':12,
	'param3':'TD02',
	'param4':'INDEX_ACTES_PLE',
	'param5':'ENIExpTemplate.xml',
}

if __name__ == '__main__':
	# Recogemos la ruta del shell y lo quitamos del diccionario
	myShell=execDict['shell']
	del execDict['shell']
	execProgs=[myShell]
	
	#Creamos la cadena con los parámetros paraejecuar
	for key,value in execDict.items():
		value1=value.replace(" ","¬spc¬")
		st=f'"{key}={value1}"'
		execProgs.append(st) #Exec progs.

	print(str(subprocess.run(execProgs, capture_output=True)))


3. Llamada al programa xmexec.

Desde el script vamos a llamar al programa pyhon xmexec, paa ello:
  1. Verificamos que recibimos los parámetros "folder", "venv", module" y "function"
  2. Nos situamos a la carpeta del parámetro "folder"
  3. Activamos el entorno virtual de la carpeta indicada en el parámetro "venv"
  4. Ejecutamos "python" al que pasamos el propgrama xmexec.py y el resto de parámetros (excepto "venv" y "folder") 
#!/bin/bash

#--------------------------
# 0. Exemple de crida
# ./static/scripts/llibreries.sh \
#  "folder=../02.llibreries" "venv=venv02" \
#  "module=eni.xmexpeni" "function=expedientENI" \
#  "docsFolderPath=docs/actes2022/" \
#  "templatesPath=templates/ENI/" \
#  "filter=*ord*aprov*sio*_signed.pdf" \
#  "anyPle=2022" \
#  "expCodi=1814787N" \
#  "organo=L01462384" \
#  "tDocu=TD02" \
#  "prefPle=INDEX_ACTES_PLE" \
#  "plantillaXML=ENIExpTemplate.xml" \
#  "docxIndex=indexTemplate.docx"
#
#
# O també podriem executar en background
# Run the Python script in the background
#  nohup python myscript.py > /dev/null 2>&1 &
#
#---------------------------

remove_quotes() {
    local arg="$1"
	# Remove leading double quote if present
    arg="${arg#\"}"
	# Remove leading double quote if present
    arg="${arg%\"}"
    echo "$arg"
}

#------------------------------------
# 1. Recogemos los parametros
#------------------------------------

# Inicialize variables
folder_value=""
venv_value=""
module_value=""
other_params=""

# Función para mostrar uso del script
usage() {
  echo "Uso: $0 'folder=RUTA'  'venv=carpeta entorno virtual' 'module=carpeta.modulo.py'  ... otros_parametros=VALOR ..."
  echo "Ejemplo:"
  echo "  $0 folder=/mi/carpeta env=producción venv=venv02 module=eni.xmexpeni config=ejemplo version=1.0"
  exit 1
}

# Verificar si se proporcionaron argumentos
if [ $# -lt 3 ]; then
  usage
fi

# Procesar cada argumento
for argIni in "$@"; do
  # Remover comillas
  arg=$(remove_quotes "$argIni")
  
  # Verificar si el argumento contiene un '='
  if [[ "$arg" == *=* ]]; then
    # Dividir el argumento en clave y valor
    key="${arg%%=*}"
    value="${arg#*=}"
	#echo "$key    AAAA   $value  AAAAA  $arg"
	
    case "$key" in
      folder)
        folder_value="$value"
        ;;
      venv)
        venv_value="$value"
        ;;
	  module) 
        module_value="$arg"
		;;
      *)
        # Concatenar otros parámetros
        other_params+=" \"$arg\""
        ;;
    esac
	#echo "$key    BBBBB   $value"
  else
    echo "Argumento inválido: $arg"
    usage
  fi
done

#echo "FOLDER=  $folder_value"
#echo "VENV= $venv_value"
#echo "MODULE= $module_value"

# Verificar que 'folder' y 'env' hayan sido proporcionados
if [[ -z "$folder_value" || -z "$venv_value" || -z "$module_value" ]]; then
  echo "Error: Se requieren al menos un argumento 'folder' 'venv'y 'module'."
  usage
fi


#--------------------------------------
# 2. Nos cambiamos a la carpeta del modulo
#-------------------------------------
pwd
cd "$folder_value"
pwd

#--------------------------------------
# 3. Activamos el entorno virtual
#-------------------------------------
source "$venv_value"/bin/activate


# Crear la cadena concatenada
final_string1="python xmexec.py \"${module_value}\" ${other_params}"
echo "--------------------------------------"
echo "FINAL STRING=$final_string1"
echo "--------------------------------------"
python xmexec.py \"${module_value}\" ${other_params}


4. Verificar que la carpeta del entrono virtual "venv" está creado dentro de la carpteta "folder"


5. Crear el programa python "xmexec.py" en la crpeta folder.

Este programa:
  1. Recoge los argumentos y cambia "¬spc¬ por espacio " "
  2. Carga el módulo "module" y
  3. Ejecuta la función con el diccionario que recoge todos los parámetrso menos "module" y "function" 
'''
Execute a function from a module
paramenters:
	"module= module name"
	"function= function name"		
	"param1 = value1"
	"param2 = value2"
	"param_n = value_n"

'''
import importlib
import sys

requiredProps=['module','function']

def arreglaArg(arg:str)->str:
	if arg.startswith('"') or arg.startswith("'"):
		arg=arg[1:] 
	if arg.endswith('"') or arg.endswith("'"): 
		arg=arg[:-1]
	arg=arg.replace('¬spc¬',' ')	
	return arg

if __name__ == '__main__':
	argDict={}
	for i, arg1 in enumerate (sys.argv):
		arg=arreglaArg(arg1)
		#print(i, "--", arg1, '===', arg)
		if i>0:
			aArg=arg.split('=')
			#print(aArg)
			argDict[aArg[0]]=aArg[1]
			

	keys=argDict.keys()
	for prop in requiredProps:
		if prop not in keys:
			raise Exception (prop + ' not in arguments')
		
	moduleName=argDict['module'].replace('.py','').replace("/",".")
	#print (moduleName)
	#module = __import__(argDict['module']) # No acaba d'anar be
	module = importlib.import_module(moduleName)
	#print (module)
	aFunc=argDict['function']
	func = getattr(module, aFunc)
	
	# Remove module and function props
	for prop in requiredProps: del argDict[prop]


	# Execute the function with arguments
	#print ("executing the function "+ aFunc)
	#print (argDict)
	#print ("=======================================================")
	func (**argDict)
	

6. Verificaciones

Verificar que :
  1. Exista el módulo dentro de la carpeta correcta
  2. Que el módulo tenga la funcion que vamos a llamar
  3. Que los parámetros de la fucnión tengan los mismos nombres y sean del mismo tipo que los parámetros que le pasamos.

7. Parámetros regex pattern


Si los parámetros "regex pattern" pueden estar definidos en un fichero u¡yaml o dentro del programa python

Por ejemplo en este fichero yaml hay 2 elementos tipo pattern.

pattern1: '^Ple \d{2} ord \d{2}-\d{2}-\d{4} .* \d{2} \d{2} \d{4}(?:_|\.)signed(?:_|\.)signed\.pdf$'

params: [1, '^Acta d{2} .* \d{2} \d{2} \d{4}.pdf$', 'otro valor']

Para el caso de der definidos en un progama python vale la pena  utilizar un string "rau" que se hace  anteponiendo r al string

pattern = r'^Acta d{2} .* \d{2} \d{2} \d{4}.pdf$'


Los posibles problemas que pueden aparecer son:
  1. Las interrogaciones "?"
  2. Los espacios en blanco
Las interrogaciones pueden dar problemas al convertirlas en una cadena dela URL, mientras que los espacios en blanco pueden dar problemas al pasarlos a un script como parámetro, pues los trocea. 

Para el caso de las interrogaciones, se puede pasar el parámetro codificado en base64 y a veces aún así puede fallar. A un caso se pueden cambiar por "¬qm¬" y luego deshacer el cambio. Para el caso de los espacios hemos optado por sustituirlos por "¬spc¬".
 



viernes, 12 de marzo de 2021

Ubuntu 20.04 Arancar un script como servicio. Caso particular de tomcat

1. Definir un servicio simple

Se ha consultado a https://linuxconfig.org/how-to-run-script-on-startup-on-ubuntu-20-04-focal-fossa-server-desktop


1.  Ir a /etc/systemd/system

2. Crear el fichero socket-server.service


[Unit]
After=network.service

[Service]
ExecStart=/home/informatica/MyPrograms/SOCKET_SERVER/startup_SocketServer.sh

[Install]
WantedBy=default.target
3. El script a ejecutar (startup_SocksetServer.sh) es

#!/bin/bash

# 1. show all arguments
echo $@

# 2. Mostra directori
myFolder=`dirname "$0"`
echo "The present working directory is $myFolder"



#3. Execute java program
/home/informatica/MyPrograms/jdk-15.0.2/bin/java -jar $myFolder/socketserver.jar S

4. Cambiar permisos

  1. $ sudo chmod 744 /home/informatica/MyPrograms/SOCKET_SERVER/startup_SocketServer.sh
    $ sudo chmod 664 /etc/systemd/system/socket-server.service
5. Arrancar el servicio

  1. $ sudo systemctl daemon-reload
    $ sudo systemctl enable socket-server.service