domingo, 29 de diciembre de 2024

Python (XXI) . Creación de un miniframework (III) Definición de las acciones tipo "table"(II)

 1. Definición de las acciones tipo "table"

Veamos el fichero "actions_04" de la carpeta "static/conf" a modo de ejemplo

#########################################
# actions_04.yml
#########################################
# 4.1 Taula municipio
41: 
    type: table
    conn: postgresDemo
    dbTable: POBLACIONS
    help: Taula de municipis 

    
    ox: > 
      Codis: [id, CPRO, CMUN, DC];
      Descripcions: [description];
    
    readOnly: [id]
    hidden: [CODAUTO]  
    
    #events: 
    #  campo1:  
    #    hx_trigger: change   #Podemos añadir mas eventos separado por coma
    #    hx_include: '#campo2'
    #    hx_target: '#campo3' 
    #    hx_edu_target: ["#campo3","#campo4","#grid_interessats"]
    #    module: basicutils/xmalbaopc.py
    #    function: albaExpSelecExpsAndInteressats(expCodiOrigen=expCodiOrigen, expCodiDestinacio=expCodiDestinacio)
     
  
    #other: []
    #session: 
      #save: [campo6, campo7]
      #read: [campo8, campo9]
        

Veamos  los parámetros:

  • La clave del menú "41"
  • type : En este caso es tipo "table"
  • conn: Nombre de la conexión. Las conexiones se guardan en un fichero a parte que indicaremos mas adelante en esta entrada.
  • dbTable: el nombre de la tabla a utilizar
  • readonly: Es la lista de los campos que no se pueden modificar
  • hidden: es una lista de campos de la tabla que se van a ocultar.
  • El resto de campos se indica su definición en la entrada anterior y  estánomentados pues no se utiliza ninguno en este ejemplo, pero no impide que se puedan utilizar y son:  help, params, ox, events (y sus componentes), other session

2. Definición de la conexión:

Para definir a conexión tenemos este fichero "db.yml" que se deberia guardar en "static/conf"

postgresDemo: 
    dbType: 'postgresql+psycopg2'
    dbUser: myUser
    dbPwd: myPassword
    dbHost: localhost
    dbPort: 5432
    dbName: ayto
    dbSchema: TERRIRORI

Cabe destacar el nombre de la conexión y sobre todo el tipo de la conexión. Los demás detalles son importantes sobre todo s se utilian esquemas.


3. Pantallas que se dan:

Hay 3 tabs, para el primer tab el de "taula" solo nos permite buscar un nimero de registro mas o menos grande, y podemos mofivcar los campos directamente en la tabla, Y no podemos hacer nada mas quwe volver

El siguiente tab "Registre" nos permte modificar, dar altas copiando o de un registro en blanco, y borrar.
Hay que observar que la claves que son ajenas se muestran como un "combo" o "select". De momento cuando se tengan un gran número de opciones no se ha pensado en un componente que sea mas versátil, pero se prevé utilizarlo en proximas vesiones. En este caso es el código de provincia


El tercer "tab" es el de búsqueda, donde se permiten seleccionar los campos y establecer el orden de los registros. Aquí se buscan registros cuyo campo descripción termine con "Cull" y seguramene encontrará pocos municipios. Se ordenarán por description e id.


















sábado, 28 de diciembre de 2024

Python (XX) . Creación de un miniframework (II) Definición de las acciones tipo "info", "python" y "bash" (I)

 1. Acciones tipo "info"

Estas acciones son las más simples pues son solo informativas y no hacen nada. 

Veamos un ejemplo que representa la acción del menú "112". Puede tener por nombre "actions_112.yml" y se guardará en la carpeta "static/conf" del proyecto

# 1. Firmar Actes per Alcaldia"
112: 
  type: info
  help: > 
    Alcaldia firmarà després que Secretaria.
    PROCEDIMENT:
    1. Executar Xolido (XolidoSign)
    2. Seleccione els fitxers PDF de la carpeta a firmar (Z:\TEMP\ACTES_PLE_2022\Secretaria)
    3. En firmar-Opciones-Configuración seleccionar:
      3.1 Seleccionar el certificat.
      3.2 Formato de la firma: Perfil -XL (3ªOpció)
      3.3 Selec el tipus de firma:
      3.4 Preferencia de la firma: Marcar totes les opcions 
      3.5 Política de firma: Marcar: 
        3.5.1: Aprobación.
        3.5.2: OID: 2.16.724.1.3.1.1.2.1.9
        3.5.3: URI: https://sede.060.gob.es/politica_de_firma_anexo_1.pdf
        3.5.4: Algoritmo de resumen: SHA-1
        3.5.5: Valor de resumen: G7roucf600+f03r/o0bAOQ6WAs0=
      3.6 Opciones avanzadas: SHA-512 (resto no marcar)
      3.7 Servidores de sello de tiempo: ACCV
      3.8 Información de PDF: 
        3.8.1: Introduir motivo: Alcaldia RD 2568/1986 (ROF)
        3.8.2: Introduir ubicación: Tavernes de Valldigna
      3.9 Indicar una carpeta eixida diferent a la entrada (Z:\TEMP\ACTES_PLE_2022\Alcaldia)
      3.10 Modo de salida : Modo identificado 
    4. Firmar amb Xolido 

      

veamos los parámetros:

  • La clave del menú "112"
  • type : En este caso es tipo "info"
  • help: es el texto informativo de ayuda que te indica lo que hay que hacer

Y sale esta pantalla


2. Acciones tipo "python"

Estas acciones ejecutan un programa en python dentro de este mismo proyecto, por eso nos vale el entorno virtual que tenemos.

Veamos un ejemplo que representa la acción del menú "112". Puede tener por nombre "actions_112.yml" y se guardará en la carpeta "static/conf" del proyecto

211: 
    type: python 
    module: basicutils/xmalbaopc.py 
    function: albaCopyInteressats(expCodiOrigen=expCodiOrigen, expCodiDestinacio=expCodiDestinacio, grid_interessats=grid_interessats)
    help: Per a copiar interessats des de l'expedient origen al de destinació  

    #OJO: el pattern hem de canviar ?->\? si no trenca la http despres en el programa hemde  desfer el canvi \?->?       
    params: [
      [expCodiOrigen,string,10,"Codi expedient Sedipualba Origen:","",[]],
      [expCodiDestinacio,string,10,"Codi expedient Sedipualba Destinació:","",[]],
      [descExpOrigen,string,100,"Descripció expedient Origen:","",[]],
      [descExpDestinacio,string,100,"Descripció expedient Destinació:","",[]],
      [grid_interessats,grid,100,"Interessats:","",[]],
    ]
    ox: > 
      Origen i destinació dels interessats [expCodiOrigen, descExpOrigen;expCodiDestinacio, descExpDestinacio];
      Detall dels interessats [grid_interessats];
   
    events:  
      expCodiOrigen:   
        hx_trigger: change # other evens are possible separated by commas
        hx_include: "#expCodiDestinacio"
        hx_target: "#descExpOrigen"
        hx_edu_target: ["#descExpOrigen","#descExpDestinacio","#grid_interessats"] # More field targets
        module: basicutils/xmalbaopc.py
        function: albaExpSelecExpsAndInteressats(expCodiOrigen=expCodiOrigen, expCodiDestinacio=expCodiDestinacio)
       
      expCodiDestinacio:   
        hx_trigger: change
        hx_include: "#expCodiOrigen"
        hx_target: "#descExpDestinacio"
        hx_edu_target: ["#descExpOrigen","#descExpDestinacio","#grid_interessats"]
        module: basicutils/xmalbaopc.py
        function: albaExpSelecExpsAndInteressats(expCodiOrigen=expCodiOrigen, expCodiDestinacio=expCodiDestinacio)
         
     
    other: [
      #[fase,1],
    ]
    session:  
      save: [expCodiOrigen, expCodiDestinacio]
      #read: [expCodiOrigen, expCodiDestinacio]
    
    

Veamos  los parámetros:

  • La clave del menú "211"
  • type : En este caso es tipo "python"
  • module: el nombre del módulo a ejecutar 
  • function: nombre de la función del módulo con los parámetros ajecutar
  • help: es el texto informativo de ayuda que te indica lo que hay que hacer
  • params: es una lista de campos que apareceran en la pantalla que se pueden editar. Para cada campo tenemos otra lista donde las posiciones indican:
  1. "id" y "name" del campo en html
  2. tipo de campo (str: string, int: entero ..., grid: una tabla)
  3. Longiutd en caracteres (por ejemplo 60)
  4. Etiqueta del campo en el formulario 
  5. Valor por omisión
  6. Conjunto de valores que puede tener en caso de ser un elemto tipo "select" o combo
  • ox: Disposición de los campos según el modelo de pantalla de OpenXava. (Agradezco a Javier Paniza de Gestion400 ). En principio con:
    nombre panel [campo1, campo2;campo3] inidcamos con corchetes que tenemos un panel de nombre "nombre panel" qyue tiene 3 campos, los cos primeros en una misma línea (separado por coma ",") y el tercero en una línea nueva (separado por punto y coma ",")
    nombre tab1 {campo4}, nombre tab2{campo5;campo6}  indicamos que tenemos dos tabs o pestañas, el primero con un campo y el segundo con dos campos, cada uno en una lìnea (separacion por punto y coma ";")
  • events: Definimos los eventos, para ello tenemos que decirle el campo que ha provocdeo el evento, en este caso tenemos 2 campos que lanzan eventos ( expCodiOrigen y  expCodiDestinacio) y hay que pasarle los atributos de htmx (hx-trigger para el evento a lanzar, hx-include para los parámetros a pasar al request a parte del campo que lanza el evento, hx-target que es el elemento que recibe el resultado la request)
    También tenemos que indicarle los elementos que se veran afectados sus valores al recibir la request hx_edu_target.
    También hay que indicar el módulo "module" y la función "function"del módulo junto con sus parámetros que se ejecutará en el request
  • other: Es una lista de campos con valor fijo que normalemente se meteran ocultamente en el form y así poderlos pasar al request. Tendra de estructyra n[nombre_campo, valor], para cada campo
  • session: es para guardar y leer datos datos en la sesión. Normalmente se utiliza cuando una acción recoge valores que se urilizan en una opción. Por ejmplo si qeremos copiar un campo de un registro a otro, se ejecutaría una acción para recoger el campo y guardarlo en la sesión y luego se ejecutaría otra acción que recogería el valor del campo de la sesión y lo copiaría al nuevo registro.


Veamos la pantalla que nos genera

En este caso al darle valor al campo de "codi expedient Sedipualba Origen" rellena el campo de descripción de expediente de origen y llena la abla con los interesados del primer expedeiente, Cuando se de el expediente de estino se llenara el campo de descripción y en la tabla solo apareceran los interesados del expediene origen que no estan en el expediente de destino y con el botón aceotar de traspasan los interesados al expdiente de destino

3. Acciones tipo "bash". Se desaconseja su uso

En este caso todo es igual que las acciones tipo python excepto que  podemos llamamar a módulos de otro proyecto y por tanto tenenemos que cargar su entorno virtual.

Esta opción "batch" la desaconsejo, pues complica las ejecución, La única ventaja que tiene es que podemos separar el proyecto en módulos independientes, per hay que cargar el entorno virtual de cada proyecto de llamada

Veamos el fichero parte del fichero  "actions_01.yml" de la carpeta "static/script":

# 3. Generar l'index en PDF i part del llibre ENI XML
113  : 
  type: bash
  program: llibreries
  folder: ../02.llibreries
  venv: venv02
  module: eni/xmexpeni.py
  function: expedientENI

  help: >
    Han d'estar firmades les actes per part d'Alcaldia i Secretaria.
    Es generaran els documents ENI i s'ompli la taula d'índexs en el DOCX

  #OJO: el pattern hem de canviar ?->\? si no trenca la http
  #     despres en el programa hem de  desfer el canvi \?->?       
  params: [
    [anyPle,int,4,"Any:","$$YEAR-1$$",[]],
    [expCodi,string,10,"Codi expedient Sedipualba:","1814787N",[]],
    [prefPle,string,10,"Prefix ple:","INDEX_ACTES_PLE",[]],
    [docsFolderPath, folder, 60, "Carpeta de Documents:", '/mnt/NASDADES_DOCS/TEMP/ACTES_PLE_2022/Definitiu/', [] ],
    [windowsDocsFolderPath, folder, 60, "Carpeta de Documents Windows:", 'Z:\\TEMP\\ACTES_PLE_2022\\Definitiu\\', [] ],
    [pattern, string, 60, "Pattern de les actes:", '^Ple.*\.pdf$', [] ],
    [sufixFirmaSecretari, str, 60, "Sufix firma Secretari:", '_firmado_por_CESAR_HERRERO_POMBO_-_DNI_35299409K.pdf', [] ],
  ]
  ox: > 
      Paràmetres Generals [anyPle, expCodi];
      Localització Documents [docsFolderPath;windowsDocsFolderPath];
      Altres [ prefPle; pattern; sufixFirmaSecretari]
    
  other: [
    [fase,1],
    [templatesPath,templates/ENI/],
    [organo,L01462384],
    [tDocu,TD02],
    [plantillaXML,ENIExpTemplate.xml],
    [docxIndex,indexTemplate.docx],
  ]
    


  • "type" es ahora "bash" en vez de "python".
  • "type" es ahora "bash" en vez de "python".
  • "program" es la shell a ejecutar añadiéndole la terminación ".sh" y estará en la carpeta "static/scripts". En este caso utilizamos el shell "llibreries.sh" que se mostrará más adelante.
  • "venv": nombre de la carpeta donde está el entorno virtual a ejecutar
  • "folder": La carpeta general del proyecto
  • "module": Es la carpeta donde está el módulo python
  • "function": Es la función a ejecutar
Los otros campos coinciden con el de la acción tipo "bash", por tanto se puede consultar en el apartado anterior estos parámetros: help, params, ox, events (y sus componentes), other y session

En ese caso la pantalla resultante no difiere para nada de la de una tipo "python"

Veamos la complejidad que aporta el shell script "llibreries.sh" de la carpeta "static/scrips"

#!/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}







Python (XIX) . Creación de un miniframework (I) Definición de los menús

1. Configuración de los menús

 Para definir los menús utilizaremos un fichero yaml, donde cada menú es un objeto o diccionario cuya clave es el número de menu.

Si la clave tiene una sola cifra se entiende que es un menú principal (que no tiene padre), per ejemplo 1,2,3...

Si acabamos con los números podemos utilizar letras.

Es conveniente situarlos en orden, prinmero el menú y despúes sus hijos.

Supongamos que tenemos 3 menús principales (1,2,3) , y el número 1 tiene 2 hijos (11 y 12) y que el hijo (11) tiene 3 hijos más (111,112, y 113). Tendremos este orden

1,11,111,112,113,12,2,

Si al menú 2 le damos dos hijos (21, 22) y al 3 le damos 3 hijos (31,32,33) quedaría:

1,11,111,112,113,12,2,21,22,3,31,32,33 

Las propedades que podemos dar a cada menú son:

description: La descripción que aprarecerá

tooltip: mensaje que aparece al situar el rató sobre el menú,

icon: icono de font awesome

En principio, obligatoriament se necesita la clave que indica la posición del menú por ejemplo "111" y la "description". Los demás parámetros son opcionales, pero recomendables.

Veamos un ejemplo del fichero menus.yaml. Este fichero se guardará en la carpeta "static/conf" del proyecto

############################################################:
# menus.yml
############################################################:
1    : {description: 'Secretaria', tooltip: 'Secretaria guai!' , icon: 'fa-solid fa-skull-crossbones'}
11   : { description: "Llibre d'actes de plens"   }
111  : { description: "1. LOCAL: Firmar Actes per l'Alcaldia" }
112  : { description: "2. LOCAL: Firmar Actes per la Secretaria" }
113  : { description: "3. Generar l'index en PDF i part del llibre ENI XML" }
114  : { description: "4. LOCAL: firmar l'índex en PDF per Secretaria"    }
115  : { description: "5. Afegir l'índex al PDF per Secretaria"  }
116  : { description: "6. LOCAL: Firmar l'index del llibre ENI XML per Secretaria"  }
2    : { description: 'Sedipualba'  }
21   : { description: 'A. Expedients'}
211  : { description: 'A1. Copiar interessats entre expedients'}
212  : { description: 'A2. Copiar les resolucions d''un any a un expedient'}
3    : { description: 'Personal'                    }
31   : { description: 'Control Presència'}
32   : { description: 'Menu 32'     }
321  : { description: 'Menu 321' , icon: 'fa-solid fa-skull-crossbones'}
322  : { description: 'Menu 322'   }
323  : { description: 'Menu 323'   }
4    : { description: 'Informàtica' ,  tooltip: 'Informàtica guai del Paraguai!' , icon: 'fa-solid fa-skull-crossbones'}
41   : { description: 'Taula Municipio'   }

Esto nos dará una imagen como esta

2. La clase menu.py


En la clase menú, a parte de tener la clave ("id"), "description", "tooltip" e "icon", tenemos la propieada "action", que es la acción del menú y se definirá en fichero a parte.

A partir del fichero "menu.yml" podemos obtener quien es el padre de nuestro menú (basta con quitarle la última letra de nuestro menú) que es la propiedad "parentId" y la lista de hijos "childrenIds" que corresponden a todos aquellos menús que tienen una letra más de longitud en su "id" y que sus claves comientan con la clave del padre. Por ejemplo "3" es padre de "31" y los hijos de "31" són "311" y "312"

De esta clase hemos destacado sus atributos que hemos visto antes. Cabe destacar el constuctor "__init__" ,los "setters" y las funciones encargadas de leer las propiedades de los menús que están guardadas en:

  • menus_???.yml: Nomalmente los emnús de una aplicación se pueden ver bien en un solo fichero "menus.yml", pero podemos partirlo en varios ficheros como "menus_01.yml", "menus_011.yml". La condicion es que comience por "menus "y a continuación  un guión bajo "_" y varios dígitos y terminar en ".yml"
  • menu_icons.yml: Para cada tipo de acción asigna un icono por omisión.
  • actions_???.yml: Aquí se definen las acciones. Si que es conveniente separarlo en varios ficheros pues las acciones tienen un montón de parámetros, por tanto con el fin de evitar errores se opta por tener un fichero por cada acción y como mucho 3 acciones, pero no es conveniente.
Veamos el fichero menu_icons.yml que para cada tipo de acción (tabién se guardará en la carpeta "static/conf" del proyecto

############################################################:
# menu_icons.yml
############################################################:

default: 'fa-solid fa-folder'
table: 'fa-solid fa-table-cells'
python: 'fa-brands fa-python'
report: 'fa-solid fa-rectangle-list'
action: 'fa-solid fa-gears'
bash: 'fa-solid fa-bolt'
none: 'fa-solid fa-hand-middle-finger'

Los tipos de acciones son:
default: por omisión
table: para hace un CRUD de una tabla
python: un programa python
report: Un listado
action: Una accion general (???)
bash: un porgrama bash
none: Nada (????)






viernes, 27 de diciembre de 2024

Python (XVIII) sqlalchemy: CRUD. Problema con el "raw" SQL. Metadatos de la BD

1. Crear la conexión

Veamos como se crean los elementos para conectar con la base de datos con SQLAlchemy:

Aquí hemos complicado un poco las cosas pues para cadas sesión tenemos una conexión. Dejamos un máximo de 100 conexiones.

import json
from typing import Dict
from fastapi import Request
from sqlalchemy import String, Integer, Boolean, Date, DateTime, Float, Table, create_engine, MetaData, select, inspect, text, asc, desc, insert, delete
from sqlalchemy.orm import sessionmaker
from sqlalchemy.engine import Engine
import psycopg2

#DB vars
sessions={}
maxSessions=100
#connection = engine =sessionLocal = metadata = schema = None


def getDBConnection(dbType:str, dbUser:str, dbPwd:str, dbHost:str, dbPort:int|str, dbName:str, dbSchema:str, sessionId:str):
	''' Function to connect to the database and save the parameters 

	Args:
		dbType (str): Type of database (e.g., "postgresql", "mysql", "sqlite")
		dbUser (str): Database username
		dbPwd (str): Database password
		dbHost (str): Database host
		dbPort (int|str): Database port
		dbName (str): Database name
		dbSchema (str): Database schema
		
	Returns:	
		connection (Engine): Connection to the database
		engine (Engine): SQLAlchemy engine
		SessionLocal (Session): SQLAlchemy session	 
        metadata (MetaData): SQLAlchemy metadata       
	'''
	#global connection, engine, sessionLocal, metadata, schema
	global sessions
	if sessionId not in sessions:
		myDict={}
		
		# Create the connection string
		connection_string = f"{dbType}://{dbUser}:{dbPwd}@{dbHost}:{str(dbPort)}/{dbName}"

		# Create the SQLAlchemy engine
		myDict['engine'] = create_engine(connection_string)

		# Database configuration
		myDict['sessionLocal'] = sessionmaker(autocommit=False, autoflush=False, bind=myDict['engine'])
		#metadata = MetaData(bind=engine)
		# Reflect the database schema
		myDict['metadata'] = MetaData()
		myDict['metadata'].reflect(bind=myDict['engine'])

		#Connection
		myDict['connection']=myDict['engine'].connect()

		# Set the schema
		#connection.execute(text(f"SET search_path TO {dbSchema}"))
		myDict['schema']=dbSchema
		sessions[sessionId]=myDict
		#Not allowed more than maxSessions
		keys=list(sessions.keys())
		if len(keys)>maxSessions: 
			sessions[keys[0]]['connection'].close()	
			del sessions[keys[0]]

2. Consultas con "Raw" SQL con SQLAlchemy

Parece ser que si se permite utilizar consultas SQL "a pelo" per se tienen estas restricciones:

  1. Los nombres de esquemas, tablas y campos deben ser estar en minúsculas.
  2. Las cláusulas LIKE y cadenas de caracteres a comparar deben ir en comillas simples.
  3. Hay que pasar la funcion "text" a la sentencia SQL
  4. Si queremos usar nombres de esquemastablas y campos en MAYÚSCULA, se deben entrecomillar entre comillas dobles
Veamos ejemplos de errores frecuentes:

A. Esquema: esq  Tabla: tab Campo: camp 

SELECT * FROM esq.tab WHERE camp LIKE "P%"
El error está en utilizar comillas dobles en el literal del LIKE "P%". La solución es:
SELECT * FROM esq.tab WHERE camp LIKE 'P%'

B. Esquema: ESQ  Tabla: tab Campo: camp 

SELECT * FROM esq.tab WHERE camp LIKE 'P%'
El error está en NO utilizar comillas dobles en el nombre del esquema que tiene letras mayúsculas. La solución es:
SELECT * FROM "ESQ".tab WHERE camp LIKE 'P%'

C. Esquema: ESQ  Tabla: TAB Campo: camp 

SELECT * FROM "ESQ".TAB WHERE camp LIKE 'P%'
El error está en NO utilizar comillas dobles en el nombre de la tabla que tiene letras mayúsculas. La solución es:
SELECT * FROM "ESQ"."TAB" WHERE camp LIKE 'P%'

D. Esquema: ESQ  Tabla: TAB Campo: CAMP 

SELECT * FROM "ESQ"."TAB" WHERE CAMP LIKE 'P%'
El error está en NO utilizar comillas dobles en el nombre del campo que tiene letras mayúsculas. La solución es:
SELECT * FROM "ESQ"."TAB" WHERE "CAMP" LIKE 'P%'

Veamos ahora como ejecutar estas consultas SQL en SQLAlchemy:

Supongamos que ya tenemos la variable session creada, y vamos a realizar una búsqueda, a la que le pasamos la cláusula WHERE de la sentencia SQL. Para construir la sentencia completa hay que unir 'SELECT * FROM "esquema"."tabla" WHERE ' a las condiciones que recibimos en un string dentro del diccionario "values" y su clave es "cerca_expressio" que  puede tener este valor por ejemplo ' "DESCRIPCION" LIKE 'P%' "

Observar que se utiliza el objeto "engine" para realizar la consulta

def cercar(sessionId:str, values:dict):
    """
    Endpoint to fetch rows for the ag-Grid 
    - Supports pagination, sorting, and filtering.
    """
	tableName=sessions[sessionId]['dbTable']
	schema=sessions[sessionId]['schema']
	fullTableName=getFullTableName(schema, tableName)
	table=Table(tableName, sessions[sessionId]['metadata'], autoload_with=sessions[sessionId]['engine'], schema=sessions[sessionId]['schema'])
	columns=[col.name for col in table.columns]
	sqlWhere=values.get('cerca_expressio','')
	sqlSentence="SELECT * FROM " + fullTableName + " WHERE " +sqlWhere
	try:
		with sessions[sessionId]['engine' ].connect() as connection:
			result = connection.execute(text(sqlSentence))
			rows=result.fetchall()
			data=[]
			for row in rows: data.append(tuple2Dict(row, table, columns))
			return json.dumps({"message": "Query executed successfully", 'id_btn_clicked':'cercar','data': data})	
	except Exception as e:
		return json.dumps({"message": "Record query failed: "+ ', '.join(e.args),'id_btn_clicked':'Error'})

def getFullTableName(schema:str, tableName:str):
        '''Une con un punto esquema y tabla entrecomillado con comillas dobles'''
	return '"'+schema+'"."'+tableName+'"'


3. Modificaciones

En este caso utilizamos la session y no el engine. Le pasamos un diccionario de valores a modificar, pero tenemos que descartar aquellos que no forman parte de la tabla. Para ello hemos creado una función que solo toma los valores para modificar en el registro.

def modificar(sessionId:str, values:dict):
	tableName=sessions[sessionId]['dbTable']
	row=None
	try: 
		table=Table(tableName, sessions[sessionId]['metadata'], autoload_with=sessions[sessionId]['engine'], schema=sessions[sessionId]['schema'])
		session=sessions[sessionId]['sessionLocal']()
		fieldDict=getTableFieldValues(sessionId, tableName,values)

		# Update the record
		result = session.query(table).filter(table.columns.id == values.get('id',-1)).update(fieldDict)
		print("result=",result)
		session.commit()
  
		row = session.query(table).filter_by(id=values.get("id",-1)).first()
		rowDict=tuple2Dict(row, table, None)
	except Exception as e:
		session.rollback()
		return json.dumps({"message": "Record update failed: "+ ', '.join(e.args),'id_btn_clicked':'Error'})
	finally:
		session.close()
	return json.dumps({"message": "Record updated successfully", 'id_btn_clicked':values.get("id_btn_clicked",''),'row': rowDict})

'''Filtramos los campos de la tabla'''
def getTableFieldValues(sessionId: int | str, tableName: str, values:dict):
	columns, fkColDict, colNames, colQuotes=getTableInfo(tableName, sessionId)	
	fieldDict={ key:values[key] for key in colNames if key in values}
	return fieldDict


4. Altas


Aquí utilizamos "engine" y le pasamos un diccionario de los campos y sus valores al igual que en las modificaciones

def alta(sessionId:str, values:dict):
	tableName=sessions[sessionId]['dbTable']
	session=sessions[sessionId]['sessionLocal']()
	row=None
	try:
		table=Table(tableName, sessions[sessionId]['metadata'], autoload_with=sessions[sessionId]['engine'], schema=sessions[sessionId]['schema'])
		fieldDict=getTableFieldValues(sessionId,tableName, values)

		# New record
		# Create a new record using the dictionary
		stmt = insert(table).values(fieldDict)

		# Execute the statement
		with sessions[sessionId]['engine' ].connect() as connection:
			result = connection.execute(stmt)
			newId=result.inserted_primary_key[0]
			print ("newId",newId)
			query = select(table).where(table.c.id == newId)
			row = connection.execute(query).fetchone()
			rowDict=tuple2Dict(row, table, None)
			connection.commit()
	except Exception as e:
		connection.rollback()
		return json.dumps({"message": "Record addition failed: "+ ', '.join(e.args),'id_btn_clicked':'Error'})
	finally:
		connection.close()
	return json.dumps({"message": "Record added successfully", 'id_btn_clicked':values.get("id_btn_clicked",''),'row': rowDict})


5. Bajas


El proceso es parecido a los anteriores,

def borrar(sessionId:str, values:dict):
	tableName=sessions[sessionId]['dbTable']
	session=sessions[sessionId]['sessionLocal']()
	row=None
	try:
		table=Table(tableName, sessions[sessionId]['metadata'], autoload_with=sessions[sessionId]['engine'], schema=sessions[sessionId]['schema'])
		# Delete row with conditions
		stmt = delete(table).where(table.columns.id == values.get('id',-100))

		# Execute the statement
		with sessions[sessionId]['engine' ].connect() as connection:
			result = connection.execute(stmt)
			connection.commit() 
	except Exception as e:
		connection.rollback()
		return json.dumps({"message": "Record deletion failed: "+ ', '.join(e.args),'id_btn_clicked':'Error'})
	finally:
		connection.close()
	return json.dumps({"message": "Record deleted successfully", 'id_btn_clicked':values.get("id_btn_clicked",''),'id': values.get('id',-100)})


6. Obtener información de la tabla


Para ello tenemos estas funciones para obtener los metadatos de una tabla

def getForeignKeysAndReferences(tableName: str, sessionId:str):
	""" Detect foreign keys and referenced tables for a given table. """
	global sessions
	inspector = inspect(sessions[sessionId]['engine'])
	foreignKeys = inspector.get_foreign_keys(table_name= tableName, schema=sessions[sessionId]['schema'])
	fkColDict = {}
	for fk in foreignKeys: fkColDict[fk['constrained_columns']]=fk
	return fkColDict

def getTableInfo(tableName: str, sessionId:str):
	""" Endpoint to dynamically generate a form with foreign key detection and field types. """
	global sessions
	table = Table(tableName, sessions[sessionId]['metadata'], autoload_with=sessions[sessionId]['engine'], schema=sessions[sessionId]['schema'])
	fkColDict = getForeignKeysAndReferences(tableName,sessionId)
	
	# Detect column names and types
	columns = [{"name": col.name, "type": sql2HtmlType(col.type), "value":get_default(col), "width":get_length(col)} for col in table.columns ]
	colNames=[col.name for col in table.columns ]
	colQuotes= {col.name : sqlQuote(col.type) for col in table.columns }
	return columns, fkColDict, colNames, colQuotes




lunes, 16 de diciembre de 2024

Onshape (IV): Exportar la piezas en formato step

Para exportar las pieas en formato STEP hay que pinchar la pieza con el botón derecho y darle a exportar. 


Y nos aparece esta pantalla donde elegimos el formato STEP que es uno de los que mas se utilizan para el mecanizado CNC.