miércoles, 20 de noviembre de 2024

Python geolocalizacion html y java script

 Veamos estos programas elprimero por ip y el segundo por android


<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Geolocalización del Usuario</title>
</head>
<body>
    <h1>Obtener Geolocalización</h1>
    <button onclick="obtenerUbicacion()">Obtener mi ubicación</button>
    <p id="resultado"></p>

    <script>
        function obtenerUbicacion() {
            const resultado = document.getElementById("resultado");

            if ("geolocation" in navigator) {
                resultado.textContent = "Obteniendo ubicación...";
                
                navigator.geolocation.getCurrentPosition(
                    function(posicion) {
                        const latitud = posicion.coords.latitude;
                        const longitud = posicion.coords.longitude;
                        resultado.innerHTML = `Latitud: ${latitud}<br>Longitud: ${longitud}`;
                    },
                    function(error) {
                        switch(error.code) {
                            case error.PERMISSION_DENIED:
                                resultado.textContent = "Usuario denegó la solicitud de geolocalización.";
                                break;
                            case error.POSITION_UNAVAILABLE:
                                resultado.textContent = "La información de ubicación no está disponible.";
                                break;
                            case error.TIMEOUT:
                                resultado.textContent = "Se agotó el tiempo de espera para obtener la ubicación.";
                                break;
                            case error.UNKNOWN_ERROR:
                                resultado.textContent = "Ocurrió un error desconocido.";
                                break;
                        }
                    }
                );
            } else {
                resultado.textContent = "La geolocalización no está soportada por este navegador.";
            }
        }
    </script>
</body>
</html>



<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Geolocalización Móvil</title>
</head>
<body>
    <h1>Obtener Ubicación del Dispositivo Móvil</h1>
    <button onclick="obtenerUbicacion()">Obtener mi ubicación</button>
    <p id="resultado"></p>

    <script>
        function obtenerUbicacion() {
            const resultado = document.getElementById("resultado");

            if ("geolocation" in navigator) {
                resultado.textContent = "Obteniendo ubicación...";
                
                navigator.geolocation.getCurrentPosition(
                    function(posicion) {
                        const latitud = posicion.coords.latitude;
                        const longitud = posicion.coords.longitude;
                        const precision = posicion.coords.accuracy;
                        resultado.innerHTML = `Latitud: ${latitud}<br>Longitud: ${longitud}<br>Precisión: ${precision} metros`;
                    },
                    function(error) {
                        switch(error.code) {
                            case error.PERMISSION_DENIED:
                                resultado.textContent = "Usuario denegó la solicitud de geolocalización.";
                                break;
                            case error.POSITION_UNAVAILABLE:
                                resultado.textContent = "La información de ubicación no está disponible.";
                                break;
                            case error.TIMEOUT:
                                resultado.textContent = "Se agotó el tiempo de espera para obtener la ubicación.";
                                break;
                            case error.UNKNOWN_ERROR:
                                resultado.textContent = "Ocurrió un error desconocido.";
                                break;
                        }
                    },
                    {
                        enableHighAccuracy: true,
                        timeout: 5000,
                        maximumAge: 0
                    }
                );
            } else {
                resultado.textContent = "La geolocalización no está soportada por este dispositivo.";
            }
        }
    </script>
</body>
</html>




martes, 19 de noviembre de 2024

Python 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, 15 de noviembre de 2024

tkinter python: se ejecutan los los eventos cuando se crea el botón por error. Parametro command

Este código ejecuta la función cuando se crea el botón:

tk.Button(root, text="Execute", command=execute_program(action))

Para solucionarlo según Spataner NO hay que asignar el command a una función con paréntesis. Por tanto quedaria así

  1. Para eliminar los paréntesis hay que crear una función que no tome argumentos y por tanto debe poder llamar a la función deseada con los argumentos pedidos
  2. Cambiar la función desesada por la nueva función

def myFunction():  execute_program(action)

tk.Button(root, text="Execute", command=myFunction)



jueves, 14 de noviembre de 2024

Python Crear un fichero bat (windows) que utilice un entorno virtual y ejecute un programa python

0. Introducción

Se pretende que la máquina cliente pueda ejecutar un programa en el servidor sin tener instalado python localmente. Para ello se realizan los siguientes pasos:
  1. Instalar WinPython que es un ejecutable que no requiere instalación de librerías.
  2. Crear un entorno virtual de WinPython
  3. Crea un fichero batch que active el entorno virtual y ejecute el programa python en cuestión 

1. Instalar WinPython

Se descarga en la carpeta compartida del servidor. En mi caso el fichero es Winpython64-3.12.4.1.exe

Ejecutamos dicho fichero, el cual crea a carpeta WPy64-31241

Dentro de dicha carpeta hay varias carpetas (n, notebooks, python-3.12.4.amd64, scripts, settings y t) y varios ficheros.

2. Crear entorno virtual

Supongamos que se ha compartido para ello una carpeta en el servidor y la hemos mapeado a la unidad Y, y queremos que el entorno virtaul esté en Y:\weenv

El ejecutable de python está en la carpeta python-3.12.4.amd64, Nos situamos en dicha carpeta y ejecutamos

Y:\WPy64-31241\python-3.12.4.amd64>python.exe -m venv Y:\wvenv

y contesta

Actual environment location may have moved due to redirects, links or junctions.
  Requested location: "Y:\wvenv\Scripts\python.exe"
  Actual location:    "\\srv-softprop2\Prog_PY\wvenv\Scripts\python.exe"

Activamos el entorno virtual e instalamos las dependencias:

Y:\venv\Scripts\activate.bat
pip install -r requirements.txt

Si no tenemos el requirements, podemos ejecutr una a una utilizando "pip install dependencia"

3. Crear el script que activa el entorno virtual y ejecuta dicho programa

Según ChatGPT tenemos este script (menu_soft.bat) que activando el entorno virtual venv ejecuta el programa python menu_soft.py

El "pause" que hay al final hay que quitarlo una vez comprabado que funcione el script.

Hay algunos ordenadores que no va bien dicho script y no he averiguado porqué.

@echo off

REM No podem fer un cd //server01/Prog_PY. Conseguir la carpeta del programa
set SCRIPT_FOLDER=%~dp0
pushd %SCRIPT_FOLDER%

REM Path to the virtual environment
set WINPYTHON_ACTIVATE=wvenv\Scripts\activate.bat

REM Path to Python.exe
set PYTHON_EXE=wvenv\Scripts\python.exe

REM Path to the Python program
set PYTHON_PROGRAM=menu_tk.py

REM Activate the virtual environment
call %WINPYTHON_ACTIVATE% 

REM Check if activation succeeded
if errorlevel 1 (
    echo Failed to activate virtual environment.
    exit /b 1
)

call %PYTHON_EXE% %PYTHON_PROGRAM%

REM Deactivate the virtual environment
deactivate

pause


4. Crear un acceso directo en el ordenador del cliente

Supongamos que el servidor tien de nombre "server01", y la carpeta compartida tiene el nombre "Prog_PY"

Entonces crearemos un enlace en el escritorio que apunta a:

//server01/Prog_PY/menu_soft.bat











martes, 12 de noviembre de 2024

VS Code: Recuperar ficheros borrados. Enganchar con Gitlab

 1. Recuperar ficheros borrados

Ir al explorador de archivos como muestra el dibujo y desplazarse hasta el último fichero que muestra y justo debajo, en la pequeña zona en blanco, hacer click izquierdo y "crtl z" y nos aparecerá por arte de magia el fichero borrado.

En la imagen el último fichero es "xmexec.py" y justo debajo hacemos  click izquierdo y "crtl z


2. Enganchar una carpeta de un proyecto VS a GitLab



domingo, 3 de noviembre de 2024

SqlAlchemy (I) : Conexión, crer DB, Tablas y columnas. Ejecutar SQL a pelo

Veamos este código python (db.py)

import sqlalchemy as db
import yaml

#1. Read data from the static/tables folder and Create tables
#1.a Read the tables information from file
def getTables(tablesFilePath: str='static/tables/prov.yml')->list:
    with open(tablesFilePath) as f: 
        tableDict=yaml.safe_load(f)	
    return tableDict

#1.b Create Columns of the Table
def getColumn(col: list)->db.Column:
    lst=[]
    aDict={}
    for i, elem in enumerate(col):
        if i==0: lst.append(elem)
        elif i==1: 
            sublst=elem.split('(')
            type =sublst[0]
            lenOrFkey='255'
            if len(sublst)>1:
                lenOrFkey=sublst[1].replace(')','')
            match type.lower():
                case 'int' | 'integer': lst.append(db.Integer())
                case 'float': lst.append(db.Float)
                case 'bool': lst.append(db.Boolean)
                case 'str' | 'string' | 'char' | 'varchar' : lst.append(db.String(int(lenOrFkey))) 
                case 'date': lst.append(db.Date())
                case 'timestamp': lst.append(db.TIMESTAMP())
                case 'json': lst.append(db.JSON())
                case 'foreignkey' : lst.append(db.ForeignKey(lenOrFkey))
    else: aDict |= elem
    column=db.Column(*lst, **aDict)    
    print (str(column))
    return column          
                
#1.c Create the tables from the information                
def getTable(table: dict, metadataObj: db.MetaData)->db.Table:
    return db.Table(table['name'], metadataObj, *[getColumn(col) for col in table['columns']])   


#2. Insert a list of elements in a table
#2.1 Using sqlAlchemist
def insert(table: db.Table | str, listDataDict: list[dict],conn: db.engine.base.Connection=None, commit:bool=False):
    """
    Insert a list of dictionaries in the given table.
    
    Parameters
    ----------
    table: db.Table | str
        Either a db.Table object or the name of the table
    listDataDict: list[dict]
        A list of dictionaries where each dictionary represents a row to be inserted
    conn: db.engine.base.Connection, optional
        The connection to use for the insert. If None, it will use the default engine.
    commit: bool, optional
        If True, will commit the transaction. Defaults to False.
    
    
    Returns
    -------
    A list of tuples of ids of the inserted rows.
    """
    mytable=db.Table('Student', metadataObj, autoload_with=conn.engine) if isinstance(table, str) else table
    query = db.insert(mytable).values(listDataDict).returning(table.c.id)
    Result = conn.execute(query)
    result=Result.fetchall()
    if commit: conn.commit()
    return result

#2.1 Using raw SQL
def insertSQL(table: str, listDataDict: list[dict],conn: db.engine.base.Connection=None, commit:bool=False):
    for aDict in listDataDict:
        fields='(' + ','.join(str(key) for key in aDict.keys()) + ')'
        values=fields.replace("(", "( :"). replace(",",", :") 
        command="INSERT INTO "+ table + fields + "VALUES "+ values 
        conn.execute(db.text(command), **aDict)
        if commit: conn.commit()
        
    
    
#3. List all elements in a table
def listAll(table: db.Table | str, conn: db.engine.base.Connection=None):
    mytable=db.Table('Student', metadataObj, autoload_with=conn.engine) if isinstance(table, str) else table
    query = db.select([mytable])
    result = conn.execute(query)
    return result

#3.1 Execute a command from SQL 
def execSQL(sql: str, conn: db.engine.base.Connection=None, commit: bool=False) ->list:   
    result=conn.execute(db.text(sql))  
    if commit: conn.commit()
    else: return result.fetchall()
    
            
engine=db.create_engine('sqlite:///test.db', echo=True)
conn=engine.connect()

metadataObj = db.MetaData() #extracting the metadata


#4. Construct the DB
'''
tableLst=getTables(tablesFilePath='static/tables/prov.yml')
dbTableLst=[]
for tableInfo in tableLst:
    table=getTable(tableInfo, metadataObj)
    print(repr(table))
    dbTableLst.append(table)
'''
sql="INSERT INTO Student (name, age, tutor) VALUES('Peret',14,1)"
execSQL(sql,conn,True)

sql="CREATE VIEW VWKK AS SELECT * FROM Student a join Teacher b on a.tutor=b.id"
execSQL(sql,conn,True)

result=execSQL('SELECT * FROM VWKK',conn)
#tuples=result.fetchall()
for t in result:
    print(str(t))
    
    
'''
query = db.insert(dbTableLst[0]).values(name='Matthew', subject="Quantum Mechanics").returning(dbTableLst[0].c.id)
Result = conn.execute(query)
print(Result.fetchone().id)   
query = db.insert(dbTableLst[0]).values(name='Motos', subject="Ampliació de matemàtiques").returning(dbTableLst[0].c.id)
Result = conn.execute(query)
print(Result.fetchone().id)   

query = db.insert(dbTableLst[1]).values(name='Matthew', age=18, tutor=1).returning(dbTableLst[1].c.id)
Result = conn.execute(query)
print(Result.fetchone().id)   
query = db.insert(dbTableLst[1]).values(name='Pepet', age=18, tutor=2).returning(dbTableLst[1].c.id)
Result = conn.execute(query)
print(Result.fetchone().id)   
conn.commit()

output = conn.execute(dbTableLst[0].select()).fetchall()
print(output)
output = conn.execute(dbTableLst[1].select()).fetchall()
print(output)

Student= db.Table('Student', metadataObj, autoload_with=engine) #Table object

#Teacher= db.Table('Teacher', metadata, autoload_with=engine) #Table object
#print(repr(metadata.tables['Student','Teacher']))

    
'''    


Esta es la configuración de las tablas en formato yml (static/tables/prov.yml)

 - name: Teacher
   columns:
      - [id, int, primary_key: true, autoincrement: true]
      - [name, str(50), nullable : false]
      - ['subject', str(50), default: Maths]


 - name: Student
   columns:
      - [id, int, primary_key: true, autoincrement: true]
      - [name, str(50), nullable : false]
      - [age, int, default: 15]
      - [tutor, ForeignKey(Teacher.id), nullable: false]









sábado, 2 de noviembre de 2024

Fast HTML (VI). Dejando el tree view en un collapsible panel

1. menu.py


#1. Imports
from fastapi.staticfiles import StaticFiles
from fasthtml import common as fh
import yaml


#2. Defining Menu class
class Menu():
	# Constructor 
	def __init__(self, id, description, action=None, tooltip=None, icon=None):
		self.id = id
		self.description = description
		self.action = action
		self.tooltip=tooltip #if tooltip is not None else description 
		self.icon=icon
		self.parentId=self.parentId=None if self.id is None or self.id<10 else int(str(id)[0:-1])
		self.childrenIds=[]

	# Alternative constructor from a dioctionary using @classmethod
	@classmethod
	def fromDict(cls, d:dict):
		return cls(d['id'], d['description'], d.get('action'), d.get('tooltip'),d.get('icon'))
		
	# Setter for parentId
	def setParendId(self):
		self.parentId=self.parentId=None if self.id is None or self.id<10 else int(str(id)[0:-1])
	
	# Setter for icon 
	def setIcon(self,dictIcons:dict):
		if self.icon is None:
			if len(self.childrenIds)>0:
				self.icon=dictIcons.get('defa')
			else:
				act=self.action[:4]
				self.icon=dictIcons.get(act)
		

#3. Load Resources from conf folder
#3.1  Load icons from conf/menu_icons.yml as a dict. The icons are font-awesome
def getMenuIcons(menuIconsFilePath: str='static/conf/menus.yml')->dict:
	with open(menuIconsFilePath) as f: 
		iconDict=yaml.safe_load(f)	
		return iconDict

#3.2 Load menus from conf/menus.yml as a list of dict
def getMenus(menuFilePath: str='static/conf/menus.yml')->dict:
	with open(menuFilePath) as f: 
		mnuDict=yaml.safe_load(f)	
		mnuLst=[Menu.fromDict(m) for m in mnuDict['menus']]
		return mnuLst
	
#4. Convert the list of menus into a dict whose key is the id
#   and get the children menus and icons
def getMenuDict(menus:list[Menu], dictIcons:dict={}):
	menuDict={}
	for i, m in enumerate(menus):
		m.childrenIds=[elem.id for elem in filter(lambda x:x.parentId==m.id,menus)] # Set childrenIds
		m.setIcon(dictIcons)		
		menuDict[m.id] = m
	return menuDict	
	
#5. Defining the drop down menu
#5. Get the code of a sub menu and its nested (children) menus
def getSubmenuCodeFH(id:int, menuDict:dict[int, Menu]):
	menu=menuDict.get(id)
	paramDict={}
	SpanContent=''
	if len(menu.childrenIds)==0:
		paramDict['hx_post']=f"/action/{menu.action}"
		paramDict['hx_target']="#message"
		paramDict['hx_swap']="innerHTML"
	else:
		SpanContent=fh.I(cls='triang fa fa-caret-right')
		#paramDict['href']="#"
		#paramDict['hx-on:click']="alert('hola'); const e=document.getElementById('child"+str(id)+"');const sty=e.style.display; e.style.display= (sty=='block') ? 'none' :'block'; e.style.position='absolute' " 
		#paramDict['hx-on:click']="const e=document.getElementById('child"+str(id)+"');const sty=e.style.display; e.style.display= (sty=='block') ? 'none' :'block'; e.style.position='absolute' " 
		paramDict['hx-on:click']="showHideMenuChildren("+str(id)+")" 
		
	tttClas='tooltiptext' if menu.id<10 else 'tooltiptextchild'	
	return fh.Li(
		fh.A(
				menu.description,
				fh.Span(
					SpanContent,
					cls='expand'
				),
				fh.Span(
					menu.tooltip,
					cls=tttClas
				) if menu.tooltip is not None else '',	
			**paramDict,
			cls='tooltip' 
		),
		fh.Ul(*[getSubmenuCodeFH(k, menuDict) for k in sorted(menu.childrenIds)], cls='child', id='child'+str(id)),
		cls='parent'
	)
	
# Get the code of the main menu
def getGeneralMenuCodeFH(menuDict:dict[int, Menu]):
	return fh.Body(
    	fh.Div(id='navbar', cls='navbar'),
    	fh.Ul(
			*[getSubmenuCodeFH(k, menuDict) for k in filter(lambda x: x<10,list(menuDict.keys())) ], 
			#id='menu'
		),
		fh.Div('qqqqq',id='message', cls='message'),
		cls='menu'
    )


#5. Get subtree
# Get the code of a sub menu and its nested (children) menus
def getSubTreeViewCodeFH(id:int, menuDict:dict[int, Menu]):
	menu=menuDict.get(id)
	tttClas='tooltiptexttree'	
	paramDict={'cls':'tooltip'}
	tooltip=fh.Span(
		menu.tooltip,
		cls=tttClas
	) if menu.tooltip is not None else ''
	icon=[fh.I(cls=menu.icon),fh.Span(' ',cls='right-spacer')]	
	
	if len(menu.childrenIds)==0:
		paramDict['hx_post']=f"/action/{menu.action}"
		paramDict['hx_target']="#message"
		paramDict['hx_swap']="innerHTML"
		return fh.Li(*icon,menu.description,tooltip,**paramDict)
	else:
		return fh.Li(
			fh.Details(
				fh.Summary(*icon,menu.description,tooltip,**paramDict),
				fh.Ul(*[getSubTreeViewCodeFH(k, menuDict) for k in sorted(menu.childrenIds)], cls='child'),
			)	 
		)

# Get the code of the main menu
def getGeneralTreeViewCodeFH(menuDict:dict[int, Menu]):
	mainParamDict={'id':'main'}
	sidebarParamDict={'id':'mySidebar','cls':'sidebar'}
	btOpenParamDict={'cls':'openbtn inline-block','hx-on:click':"openCloseNav()"}
	aCloseParamDict={'cls':'closebtn','hx-on:click':"openCloseNav()"}	
	ulParamDict={'id':'firsttreeul', 'cls':'tree'}
	divAjuntParamDict={'cls':'ajuntament inline-block'}
	return fh.Body(
		fh.Div(
			fh.Button(fh.I(cls='fa-regular fa-circle-xmark'), **aCloseParamDict),   
			fh.Ul(
				*[getSubTreeViewCodeFH(k, menuDict) for k in filter(lambda x: x<10,list(menuDict.keys())) ], 
				**ulParamDict,
			),
			**sidebarParamDict,
		),
		fh.Div(
			fh.Div(fh.Button('☰ Obri', **btOpenParamDict),fh.H1('Ajuntament de Tavernes', **divAjuntParamDict)),	 
			fh.Div('Here goes the content of the main window!!!'),
			**mainParamDict,
   		),	
		fh.Div('Ací van els missatges ....',id='message', cls='message')
    )



dictIcons=getMenuIcons('static/conf/menu_icons.yml')
menus=getMenus('static/conf/menus.yml')
menuDict=getMenuDict(menus,dictIcons=dictIcons)

#6. Defining Headers (Styles, Scripts, etc)
myHeaders = [
	fh.Meta(name='viewport', content='width=device-width, initial-scale=1'),
	#fh.Link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css'),
	fh.Link(rel='stylesheet', href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css'),
	fh.Link(rel='stylesheet', href='../static/css/menu.css'),
 	fh.Link(rel='stylesheet', href='../static/css/treeview.css'),
	fh.Link(rel='stylesheet', href='../static/css/sidebar.css'),
	fh.Script(src='../static/js/menu.js'),
	fh.Script(src='../static/js/sidebar.js')
]

#7. Creating FastAPI App
app = fh.FastHTML(hdrs=myHeaders)

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

#9. Defining the routes
@app.get("/")
def geto1():
	return getGeneralMenuCodeFH(menuDict=menuDict) 

@app.get("/tree")
def geto2():
	return getGeneralTreeViewCodeFH(menuDict=menuDict)

#10. Menu Actions executed in a
@app.post("/action/{actEdu}")
def postAction(actEdu:str):
	return f"Action received: {actEdu}"


2.  css

2.1 static/css/menu.css


body {font-family: Arial, Helvetica, sans-serif;}
.navbar {overflow: hidden;background-color: black;position:fixed;top: 0;height:46px; width: 100%; z-index:-1}
#menu {padding-left:10px; }
.menu {padding-left:10px; }

.parent {display: block;position: relative;float: left;line-height: 30px;background-color: black;border-right:#CCC 1px solid;}	
.parent a{margin: 10px;color: #FFFFFF;text-decoration: none;}
.parent:hover > ul {display:block;position:absolute;}
/*.parent li:hover {background-color: #F0F0F0;}*/
.menu .parent li:hover {background-color: Gainsboro;}

/*
.child {display: none;padding-top: 10px;box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);} FALLA 
.menu .child {display: none;padding-top: 10px;box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);width:300px;} SI
.menu .child {display: none;padding-top: 10px;box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);width:auto;} FALLA
*/
.menu .child {display: none;padding-top: 10px;box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);width:16em;z-index:2}

.menu .child li {background-color: #f9f9f9;line-height: 30px;border-right:#CCC 1px solid; width:100%;}
.menu .child li a{color: #000000;}
.menu .child .child{padding-top: 0px;}

/*ul{list-style: none;margin: 0;padding: 0px; min-width:10em;}*/
.menu ul{list-style: none;margin: 0;padding: 0px; min-width:10em;}
/*ul ul ul{left: 100%;top: 0;margin-left:1px;}*/
.menu ul ul ul{left: 100%;top: 0;margin-left:1px;}

/*li:hover {background-color: red;}*/
.menu li:hover {background-color: red;}

.menu .expand{float:right;margin-right:5px;}
.menu .triang{margin-top:6px;}
.message {position: fixed; bottom:50px;color: green;background-color: white;border-right:#CCC 1px solid;}	

/*.tooltip {}*/
.menu .tooltip .tooltiptext {	
	visibility: hidden;	width: 150px; font-size:85%;
	color: black; background-color:gold;
	text-align: center;	border-radius: 6px;	padding: 5px 0;
	position: absolute;	z-index: 1;	top: -5px;	left: 105%;
}
.menu .tooltip:hover .tooltiptext {visibility: visible;}


.menu .tooltip .tooltiptextchild {	
	visibility: hidden;	width: 150px; font-size:85%;
	color: black; background-color:gold;
	text-align: center;	border-radius: 6px;	padding: 5px 0;
	position: absolute;	z-index: 1;	top: -5px;	left: 105%;
}
.menu .tooltip:hover .tooltiptextchild {visibility: visible;}	


2.2 static/css/treeview.css


.tree {
    --spacing: 1.5rem;
    --radius: 10px;
    line-height:var(--spacing); /*EDU*/
    /*cursor:default;*/
    cursor: pointer;
  }
  
  .tree li {
    display: block;
    position: relative;
    padding-left: calc(2 * var(--spacing) - var(--radius) - 2px);
  }

  
  
  .tree ul {
    margin-left: calc(var(--radius) - var(--spacing));
    padding-left: 0;
  }

  #firsttreeul {
    margin-left: 0px;
    padding-left: 10px;
    
  }
  
  .tree ul li {
    border-left: 2px solid #ddd;
  }
  
  .tree ul li:last-child {
    border-color: transparent;
  }
  
  .tree ul li::before {
    content: '';
    display: block;
    position: absolute;
    top: calc(var(--spacing) / -2);
    left: -2px;
    width: calc(var(--spacing) + 2px);
    height: calc(var(--spacing) + 1px);
    border: solid #ddd;
    border-width: 0 0 2px 2px;
  }
  
  .tree summary {
    display: block;
    /*cursor: pointer;*/
    cursor: default;
  }
  
  .tree summary::marker,
  .tree summary::-webkit-details-marker {
    display: none;
  }
  
  .tree summary:focus {
    outline: none;
  }
  
  .tree summary:focus-visible {
    outline: 1px dotted #000;
  }
  
  .tree li::after,
  .tree summary::before {
    content: '';
    display: block;
    position: absolute;
    top: calc(var(--spacing) / 2 - var(--radius));
    left: calc(var(--spacing) - var(--radius) - 1px);
    width: calc(2 * var(--radius));
    height: calc(2 * var(--radius));
    border-radius: 50%;
    background: #ddd;
  }
  
  .tree summary::before {
    z-index: 1;
    background: #696 url('../svg/expand-collapse.svg') 0 0;
  }
  
  .tree details[open] > summary::before {
    background-position: calc(-2 * var(--radius)) 0;
  }

  .right-space {
    padding-right: 1em;
  }

  .tree .tooltip .tooltiptexttree {	
    /*visibility: hidden;*/
    display:none;

    width: 150px; font-size:85%;
    color: black; background-color:gold;
    
    text-align: center;	border-radius: 6px;	padding: 5px 0;
    /*position: absolute;*/	
    /*top: -5px;	left: 105%;*/
    margin-left: 1em;
    padding: 0px 0.5em 0px 0.5em;
    width:fit-content;
    
    z-index: 1;	
  }

  .tree .tooltip:hover  {color: green; font-weight: bold;}	
  /*.tree .tooltip:hover  .tooltiptexttree {visibility: visible;}	*/
  .tree .tooltip:hover  .tooltiptexttree {display: inline-block;}	


2.3 static/css/sidebar.css


.sidebar {
    height: 100%;
    width: 0;
    position: fixed;
    z-index: 1;
    top: 0;
    left: 0;
    background-color: #f0f0f0;
    overflow-x: hidden;
    transition: 0.5s;
    padding-top: 15px;
  }
  
  .sidebar .closebtn {
    position: absolute;
    top: 0;
    right: 25px;
    background-color: #04AA6D;
    color:white;
    font-size: 25px;
    margin-left: 50px;
    padding-left: 5px;
    padding-right: 5px;
    border: none;
    border-radius: 8px
  }
  .sidebar .closebtn:hover {
    background-color: #026e47;
    font-weight: bold;
  }  

.openbtn {
  font-size: 20px;
  cursor: pointer;
  background-color: #04AA6D;
  color: white;
  padding: 10px 15px;
  border: none;
  border-radius: 12px
}

.openbtn:hover {
  background-color:#026e47;
  
}

#main {
  transition: margin-left .5s;
  padding-left: 16px;
  
}

.inline-block {
    display: inline-block;
}

.ajuntament {
    padding-left: 1em;
}

/* On smaller screens, where height is less than 450px, change the style of the sidenav (less padding and a smaller font size) */
@media screen and (max-height: 450px) {
  .sidebar {padding-top: 15px;}
  .sidebar a {font-size: 18px;}
}


3. js

3.1 static/js/menu.js


showHideMenuChildren = function(id) {
    let childId='child'+id.toString()
    let e=document.getElementById(childId);
    let sty=e.style.display;
    
    // Hide the children of other menus
    let other = document.querySelectorAll("[id^='child']");
    for (let i = 0; i < other.length; i++) {
        //if (!other[i].id.startsWith(childId) && !childId.startsWith(other[i].id)) {
        //if (!other[i].id.startsWith(childId) ) {
            if( childId.startsWith(other[i].id)) {
                //alert(other[i].id + '-' + other[i].style.display + '-'+other[i].style.position)
                other[i].style.display= (sty=='block') ? null : 'block';
                other[i].style.position=(sty=='block') ? null : 'absolute'
                
            } else {
                other[i].style.display = null;
                other[i].style.position = null;
            }
            console.log(other[i].id + '-' + other[i].style.display + '-'+other[i].style.position)    
        //}
    }
    console.log("-----------------------------------------------------------");
    //Change the state of the elements  
    //e.style.display= (sty=='block') ? null : 'block';
    //e.style.position=(sty=='block') ? null : 'absolute'
}


3.2 static/js/sidebar.js


{
    let isOpen = false ;
    const panelWidth = "400px";
    
    function openCloseNav() {
      let myWidth = isOpen ? "0" : panelWidth;
      document.getElementById("mySidebar").style.width = myWidth;
      document.getElementById("main").style.marginLeft = myWidth;
      isOpen=!isOpen
    }
}



4. datos de configuracion yml


4.1 static/conf/menus.yml


menus:
  - { id: 1    , description: 'Secretaria'                    , tooltip: 'Secretaria guai!' , icon: 'fa-solid fa-skull-crossbones'}
  - { id: 11   , description: "Llibre d'actes de plenari bonico"         , action: null               }
  - { id: 111  , description: "1. Extreure els documents"     , action: 'acti-1-extr-docs'  }
  - { id: 112  , description: "2. Signar documents Alcaldia"  , action: 'acti-1-sign-alc'   }
  - { id: 113  , description: "3. Signar documents Secretaria", action: 'acti-1-sign-secr'  }
  - { id: 2    , description: 'Personal'                                }
  - { id: 21   , description: 'Control Presència'             , action: 'acti-2-contr-pres' }
  - { id: 3    , description: 'Gestió Tributària'                    }
  - { id: 31   , description: 'Menu 31'   , action: 'rept-3-1'  }
  - { id: 32   , description: 'Menu 32'   , action: 'view-3-2'  }
  - { id: 321  , description: 'Menu 321'  , action: 'acti-3-21' , icon: 'fa-solid fa-skull-crossbones'}
  - { id: 322  , description: 'Menu 322'   }
  - { id: 323  , description: 'Menu 323'   }
  - { id: 324  , description: 'Menu 324'  , action: 'acti-3-24'}
  - { id: 3221 , description: 'Menu 3221' , action: 'acti-3-221'}
  - { id: 3222 , description: 'Menu 3222' , action: 'acti-3-222'}
  - { id: 3223 , description: 'Menu 3223' , action: 'acti-3-223'}
  - { id: 3224 , description: 'Menu 3224' , action: 'acti-3-224'}
  - { id: 3225 , description: 'Menu 3225' , action: 'acti-3-225'}
  - { id: 3226 , description: 'Menu 3221' , action: 'acti-3-221'}
  - { id: 3227 , description: 'Menu 3222' , action: 'acti-3-222'}
  - { id: 3228 , description: 'Menu 3223' , action: 'acti-3-223'}
  - { id: 3229 , description: 'Menu 3224' , action: 'acti-3-224'}
  - { id: 3231 , description: 'Menu 3221' , action: 'acti-3-221'}
  - { id: 3232 , description: 'Menu 3222' , action: 'acti-3-222'}
  - { id: 3233 , description: 'Menu 3223' , action: 'acti-3-223'}
  - { id: 3234 , description: 'Menu 3224' , action: 'acti-3-224'}
  - { id: 3235 , description: 'Menu 3225' , action: 'acti-3-225'}
  - { id: 3236 , description: 'Menu 3221' , action: 'acti-3-221'}
  - { id: 3237 , description: 'Menu 3222' , action: 'acti-3-222'}
  - { id: 3238 , description: 'Menu 3223' , action: 'acti-3-223'}
  - { id: 3239 , description: 'Menu 3224' , action: 'acti-3-224'}
  - { id: 4    , description: 'Intervenció' , action: 'acti-4-intervencio'}


4.2 static/conf/menu_icons.yml


defa: 'fa-solid fa-folder'
view: 'fa-solid fa-table-cells'
rept: 'fa-solid fa-rectangle-list'
acti: 'fa-solid fa-gears'


Y sale: