Limpiar filtro
Artículo
Alberto Fuentes · 22 abr, 2022
Durante una actualización a una versión principal (major) es aconsejable recompilar las clases y rutinas de todos tus namespaces (ver Tareas tras la instalación de una versión major).
do $system.OBJ.CompileAllNamespaces("u")
do ##Class(%Routine).CompileAllNamespaces()
Para automatizar esta tarea de administración y mantener un registro de cualquier error, os muestro un ejemplo de una clase para importar y compilar en el namespace USER, que puedes usar después de cada actualización: admin.utils.cls
Class
Class admin.utils.cls
Class admin.utils
{
ClassMethod upgrade(verbose As %Boolean = 0) As %Status
{
set ns=$Namespace
// Stopping all productions
do ..stopAllProductions(verbose)
zn "%sys"
kill ^[ns]upgradeLog
// UpgradeAll
set ^[ns]upgradeLog("UpgradeAll")=$zdt($now(),3,,6)
set start=$zh
do $system.OBJ.UpgradeAll(,.errorLogUpgrade)
do ##class(%SYSTEM.OBJ).UpgradeAll()
set ^[ns]upgradeLog("UpgradeAll","duration")=$zh-start
merge ^[ns]upgradeLog("UpgradeAll","errors")=errorLogUpgrade
// Compile Classes in All Namespaces
set ^[ns]upgradeLog("classes")=$zdt($now(),3,,6)
set start=$zh
do $system.OBJ.CompileAllNamespaces("cbk",.errorLogClasses
set ^[ns]upgradeLog("classes","duration")=$zh-start
merge ^[ns]upgradeLog("classes","errors")=errorLogClasses
// Compile Routines in All Namespaces
set ^[ns]upgradeLog("routines")=$zdt($now(),3,,6)
set start=$zh
do ##Class(%Library.Routine).CompileAllNamespaces(,,.count,.errorLogRoutines)
set ^[ns]upgradeLog("routines","duration")=$zh-start
merge ^[ns]upgradeLog("routines","errors")=errorLogRoutines
merge ^[ns]upgradeLog("routines","count")=coun
// Starting all productions
do ..startAllProductions(verbose)
zn ns
return $$$OK
}
ClassMethod startAllProductions(verbose As %Boolean = 0) As %Status
{
set sc=$$$OK
set ns=$Namespace
set sc=##class(%SYS.Namespace).ListAll(.namespacesList)
set namespace=$ORDER(namespacesList(""))
while (namespace'="") {
if (##class(%EnsembleMgr).IsEnsembleNamespace(namespace))&&(namespace'["^^") {
write:verbose namespace
zn namespace
kill prodname,status
set sc=##class(Ens.Director).GetProductionStatus(.prodname,.status)
if status=2 {
write:verbose " starting production "_prodname,!
set sc=##class(Ens.Director).StartProduction()
}
write:verbose !
zn ns
}
set namespace=$ORDER(namespacesList(namespace))
}
return sc
}
ClassMethod stopAllProductions(verbose As %Boolean = 0) As %Status
{
set sc=$$$OK
set ns=$Namespace
set sc=##class(%SYS.Namespace).ListAll(.namespacesList)
set namespace=$ORDER(namespacesList(""))
while (namespace'="") {
if (##class(%EnsembleMgr).IsEnsembleNamespace(namespace))&&(namespace'["^^") {
write:verbose namespace
zn namespace
kill prodname,status
set sc=##class(Ens.Director).GetProductionStatus(.prodname,.status)
if status=1 {
write:verbose " stopping production "_prodname,!
set sc=##class(Ens.Director).StopProduction(0,1)
}
write:verbose !
zn ns
}
set namespace=$ORDER(namespacesList(namespace))
}
return sc
}
ClassMethod restartAllProductions(verbose As %Boolean = 0) As %Statu
{
set sc=$$$OK
set ns=$Namespace
set sc=##class(%SYS.Namespace).ListAll(.namespacesList)
set namespace=$ORDER(namespacesList(""))
while (namespace'="") {
if (##class(%EnsembleMgr).IsEnsembleNamespace(namespace))&&(namespace'["^^") {
write:verbose namespace
zn namespace
kill prodname,status
set sc=##class(Ens.Director).GetProductionStatus(.prodname,.status)
if status=1 {
write:verbose " restarting production "_prodname,!
set sc=##class(Ens.Director).RestartProduction(0,1)
}
write:verbose !
zn ns
}
set namespace=$ORDER(namespacesList(namespace))
}
return sc
}
ClassMethod cleanAllProductions(verbose As %Boolean = 0) As %Status
{
set sc=$$$OK
set ns=$Namespace
set sc=##class(%SYS.Namespace).ListAll(.namespacesList)
set namespace=$ORDER(namespacesList(""))
while (namespace'="") {
if (##class(%EnsembleMgr).IsEnsembleNamespace(namespace))&&(namespace'["^^") {
write:verbose namespace
zn namespace
kill prodname,status
set sc=##class(Ens.Director).GetProductionStatus(.prodname,.status)
if status=3 {
write:verbose " cleaning production "_prodname,!
set sc=##class(Ens.Director).CleanProduction(1)
}
write:verbose !
zn ns
}
set namespace=$ORDER(namespacesList(namespace))
}
return sc
}
}
Después de la actualización, simplemente ejecuta el método admin.utils.upgrade desde una sesión de terminal de IRIS:
USER>do ##class(admin.utils).upgrade()
Y puedes ver los resultados desde el portal de administración a través del explorador System > Globals > upgradeLog Este artículo está etiquetado como "Mejores prácticas" ("Best practices").
Los artículos con la etiqueta "Mejores prácticas" incluyen recomendaciones sobre cómo desarrollar, probar, implementar y administrar mejor las soluciones de InterSystems.
Artículo
Muhammad Waseem · 6 abr, 2022
¡Hola Comunidad!
Esta publicación es una introducción a mi aplicación iris-globals-graphDB en Open Exchange.
En este artículo, mostraré cómo guardar y recuperar Graph Data en InterSystems Globals con la ayuda del framework Python Flask Web y la librería PYVIS Interactive network visualizations.
Recomendación
Leer la documentación relacionada: Using Globals
Introducción al SDK nativo
PYVIS Librería de visualización interactiva de redes
Paso 1: Establecer conexión con IRIS Globals mediante el SDK nativo de Python
#create and establish connection
if not self.iris_connection:
self.iris_connection = irisnative.createConnection("localhost", 1972, "USER", "superuser", "SYS")
# Create an iris object
self.iris_native = irisnative.createIris(self.iris_connection)
return self.iris_native
Paso 2: Guardar datos en globals mediante la función iris_native.set( )
#import nodes data from csv file
isdefined = self.iris_native.isDefined("^g1nodes")
if isdefined == 0:
with open("/opt/irisapp/misc/g1nodes.csv", newline='') as csvfile:
reader = csv.DictReader(csvfile)
for row in reader:
self.iris_native.set(row["name"], "^g1nodes", row["id"])
#import edges data from csv file
isdefined = self.iris_native.isDefined("^g1edges")
if isdefined == 0:
with open("/opt/irisapp/misc/g1edges.csv", newline='') as csvfile:
reader = csv.DictReader(csvfile)
counter = 0
for row in reader:
counter = counter + 1
#Save data to globals
self.iris_native.set(row["source"]+'-'+row["target"], "^g1edges", counter)
Paso 3: Transferir datos de nodos y bordes a PYVIS desde globals mediante la función iris_native.get()
#Get nodes data for basic graph
def get_g1nodes(self):
iris = self.get_iris_native()
leverl1_subscript_iter = iris.iterator("^g1nodes")
result = []
# Iterate over all nodes forwards
for level1_subscript, level1_value in leverl1_subscript_iter:
#Get data from globals
val = iris.get("^g1nodes",level1_subscript)
element = {"id": level1_subscript, "label": val, "shape":"circle"}
result.append(element)
return result
#Get edges data for basic graph
def get_g1edges(self):
iris = self.get_iris_native()
leverl1_subscript_iter = iris.iterator("^g1edges")
result = []
# Iterate over all nodes forwards
for level1_subscript, level1_value in leverl1_subscript_iter:
#Get data from globals
val = iris.get("^g1edges",level1_subscript)
element = {"from": int(val.rpartition('-')[0]), "to": int(val.rpartition('-')[2])}
result.append(element)
return result
Paso 4: Usar PYVIS Javascript para generar datos gráficos
<script type="text/javascript">
// initialize global variables.
var edges;
var nodes;
var network;
var container;
var options, data;
// This method is responsible for drawing the graph, returns the drawn network
function drawGraph() {
var container = document.getElementById('mynetwork');
let node = JSON.parse('{{ nodes | tojson }}');
let edge = JSON.parse('{{ edges | tojson }}');
// parsing and collecting nodes and edges from the python
nodes = new vis.DataSet(node);
edges = new vis.DataSet(edge);
// adding nodes and edges to the graph
data = {nodes: nodes, edges: edges};
var options = {
"configure": {
"enabled": true,
"filter": [
"physics","nodes"
]
},
"nodes": {
"color": {
"border": "rgba(233,180,56,1)",
"background": "rgba(252,175,41,1)",
"highlight": {
"border": "rgba(38,137,233,1)",
"background": "rgba(40,138,255,1)"
},
"hover": {
"border": "rgba(42,127,233,1)",
"background": "rgba(42,126,255,1)"
}
},
"font": {
"color": "rgba(255,255,255,1)"
}
},
"edges": {
"color": {
"inherit": true
},
"smooth": {
"enabled": false,
"type": "continuous"
}
},
"interaction": {
"dragNodes": true,
"hideEdgesOnDrag": false,
"hideNodesOnDrag": false,
"navigationButtons": true,
"hover": true
},
"physics": {
"barnesHut": {
"avoidOverlap": 0,
"centralGravity": 0.3,
"damping": 0.09,
"gravitationalConstant": -80000,
"springConstant": 0.001,
"springLength": 250
},
"enabled": true,
"stabilization": {
"enabled": true,
"fit": true,
"iterations": 1000,
"onlyDynamicEdges": false,
"updateInterval": 50
}
}
}
// if this network requires displaying the configure window,
// put it in its div
options.configure["container"] = document.getElementById("config");
network = new vis.Network(container, data, options);
return network;
}
drawGraph();
</script>
Paso 5: Llamar a los códigos anteriores desde el archivo principal app.py
#Mian route. (index)
@app.route("/")
def index():
#Establish connection and import data to globals
irisglobal = IRISGLOBAL()
irisglobal.import_g1_nodes_edges()
irisglobal.import_g2_nodes_edges()
#getting nodes data from globals
nodes = irisglobal.get_g1nodes()
#getting edges data from globals
edges = irisglobal.get_g1edges()
#To display graph with configuration
pyvis = True
return render_template('index.html', nodes = nodes,edges=edges,pyvis=pyvis)
Espero que os resulte útil.
Artículo
Jose-Tomas Salvador · 1 abr, 2025
La interfaz de usuario de Interoperabilidad ahora incluye experiencias modernizadas para las aplicaciones DTL Editor y Production Configuration, las cuales están disponibles para su activación en todos los productos de interoperabilidad. Podéis alternar entre las vistas moderna y tradicional. Todas las demás pantallas de interoperabilidad permanecen en la interfaz de usuario estándar. Tenéis que tener en cuenta que los cambios se limitan a estas dos aplicaciones, y a continuación se identifica la funcionalidad que está disponible actualmente.
Para probar las nuevas pantallas antes de la actualización, podéis descargar la versión 2025.1 desde nuestra página web del kit de la comunidad aquí: https://evaluation.intersystems.com/Eval/.
Configuración de Producción - Introducción a las Tareas de Configuración
Configuración de Producción: Soporte en esta versión de la Configuración de Producción:
Creación/Edición/Copia/Eliminación de Hosts
Detener/Iniciar Hosts
Edición de Configuración de Producción
Detener/Iniciar Producciones
Integración con Control de Versiones: El soporte para la integración con control de versiones para la funcionalidad de configuración mencionada está disponible.
Vista de Panel Dividido: Los usuarios pueden abrir directamente el Editor de Reglas y el Editor DTL desde la pantalla de Configuración de Producción para ver y editar reglas y transformaciones incluidas en la producción en una vista de panel dividido.
Filtrado Mejorado: Un cuadro de búsqueda en la parte superior permite buscar y filtrar en todos los componentes de negocio, incluyendo múltiples categorías, DTLs y subtransformaciones. Usad la barra lateral izquierda para buscar independientemente del panel principal y ver los resultados de búsqueda a través de hosts y categorías.
Edición Masiva de Categorías de Hosts: Podéis añadir una nueva categoría o editar una existente para una producción añadiendo hosts desde la configuración de producción.
Routers Expandibles: Los routers pueden ser expandidos para ver todas las reglas, transformaciones y conexiones en línea.
Conexiones de Hosts Reformuladas: Las conexiones directas e indirectas ahora se muestran cuando se selecciona un host de negocio, permitiendo ver el camino completo que un mensaje puede tomar. Colocad el cursor sobre cualquier host de salida o entrada para diferenciar mejor las conexiones. El interruptor Mostrar solo Hosts Conectados filtrará solo los hosts seleccionados y sus conexiones.
DTL Editor - Introducción a las Herramientas DTL
Integración con Control de Versiones: El soporte para la integración con control de versiones está disponible.
Integración con VS Code: Los usuarios pueden ver esta versión del Editor DTL en su IDE de VS Code.
Soporte de Python Embebido: El soporte de Python embebido se extiende a esta versión del Editor DTL.
Pruebas de DTL: La utilidad de prueba DTL está disponible en esta versión del Editor DTL.
Cambio de Diseño del Panel: El editor DTL soporta un diseño de lado a lado y de arriba a abajo. Haced clic en el botón de diseño en la cinta superior para experimentar con esto.
Deshacer/Volver a Hacer: Los usuarios pueden deshacer y rehacer todas las acciones con los botones de deshacer/rehacer que aún no se han guardado en el código.
Generar Parámetro de Segmentos Vacíos: El parámetro GENERATEEMPTYSEGMENTS está disponible para generar segmentos vacíos para los campos faltantes.
Visualización de Subtransformaciones: Los usuarios pueden ver subtransformaciones haciendo clic en el ícono del ojo para abrir el DTL de la subtransformación en una nueva pestaña.
Desplazamiento:
Desplazamiento Independiente: Las secciones izquierda y derecha (origen y destino) del DTL pueden desplazarse de manera independiente colocando el cursor sobre una de las secciones y utilizando la rueda de desplazamiento o el trackpad para mover los segmentos verticalmente.
Desplazamiento Conjunto: Ambas secciones, origen y destino, pueden desplazarse juntas colocando el cursor en el medio del diagrama.
Autocompletado de Campos: El autocompletado está disponible para los campos: 'origen', 'destino' y 'condición', así como para la Clase de Origen, Tipo de Documento de Origen, Clase de Destino y Tipo de Documento de Destino.
Numeración Ordinal: El editor visual permite activar y desactivar la visualización de los números ordinales y la expresión del camino completo para cada segmento.
Referencias Fáciles: Cuando un campo en el Editor de Acción está enfocado, al hacer doble clic en un segmento en el Editor Gráfico se inserta la referencia del segmento correspondiente en la posición actual del cursor en el Editor de Acción.
Sincronización: Al hacer clic en un elemento en el editor visual, se resalta la fila correspondiente en el editor de acción.
📣 LLAMADA A LA ACCIÓN 📣
Si tenéis comentarios, por favor proporcionadlos a través de los siguientes canales:
✨ Nuevas Características en toda la Interoperabilidad: Introducid una idea en el portal de ideas o participad en otras ideas en el Portal de Ideas de InterSystems. Para nuevas ideas, añadid la etiqueta "Interoperabilidad" en vuestro post o votad positivamente por las características ya propuestas en la lista.
💻 Comentarios Generales sobre la Experiencia del Usuario en toda la Interoperabilidad: Comentad vuestros comentarios o participad en otros comentarios a continuación.
🗒 Sugerencias/Comentarios sobre Aplicaciones Modernizadas (como se describe arriba): Comentad vuestros comentarios o participad en otros comentarios a continuación.
¡Considerad completar la oportunidad de Global Master's para interactuar con el equipo en una sesión de retroalimentación guiada privada y ganar puntos! ¡Inscribíos en estas sesiones a través de Global Masters >> aquí!
Si queréis proporcionar comentarios adicionales de manera privada, por favor enviad vuestros pensamientos o preguntas a: ux@intersystems.com.
Artículo
Ricardo Paiva · 29 abr, 2025
Cuando trabajáis con InterSystems IRIS, los desarrolladores y arquitectos de bases de datos a menudo se enfrentan a una decisión crítica: si usar SQL Dinámico o SQL Embebido para consultar y actualizar datos. Ambos métodos tienen sus propias fortalezas y casos de uso, pero comprender sus implicaciones en el rendimiento es esencial para tomar la decisión correcta. El tiempo de respuesta, una métrica clave en la evaluación del rendimiento de las aplicaciones, puede variar significativamente dependiendo del enfoque de SQL que utilicéis. El SQL Dinámico ofrece flexibilidad, ya que las consultas pueden construirse y ejecutarse en tiempo de ejecución, lo que lo hace ideal para escenarios con necesidades de consulta impredecibles o altamente variables. Por el contrario, el SQL Embebido enfatiza la estabilidad y eficiencia al integrar el código SQL directamente en la lógica de la aplicación, ofreciendo tiempos de respuesta optimizados para patrones de consulta predefinidos.
En este artículo, exploraré los tiempos de respuesta al usar estos dos tipos de SQL y cómo dependen de las diferentes estructuras de clases y del uso de parámetros. Para ello, voy a utilizar las siguientes clases del diagrama:
Para probar los tiempos, creé la siguiente cantidad de objetos para cada clase:
Patient - 50M
Visit - 150M
Doctor - 500K
Address - 50M
De esta manera, esperaba que vierais tiempos razonables para la ejecución de las consultas y que pudierais observar las diferencias entre ejecutar SQL Embebido y SQL Dinámico. El único índice que añadí fue el índice automático de uno a muchos para la relación Doctor-Visit.
Así que vamos a ver las consultas que voy a ejecutar y, después, la duración de su ejecución:
select count(*) from Hospital.Address
select count(*) from Hospital.Address where State = :param
select count(*) from Hospital.Patient p left join Hospital.Address a on p.address = a.id
select count(*) from Hospital.Patient p left join Hospital.Address a on p.address = a.id where a.State = :param
select count(a.Address->State) from Hospital.Patient a
select count(*) from Hospital.Patient p where p.Address->State = :param
select count(p.Visit->VisitDate) from Hospital.Patient p
select count(*) from Hospital.Patient p where p.Visit->VisitDate > :param
select count(v.Patient->Name) from Hospital.Visit v
select count(*) from Hospital.Visit v where v.Patient->Name %startswith :param
select count(v.Patient->Address->State) from Hospital.Visit v
select count(*) from Hospital.Visit v where v.Patient->Address->State = :param
select count(v.Doctor->Name) from Hospital.Visit v
select count(*) from Hospital.Visit v where v.Doctor->Name %startswith :param
select count(*) into :p from Hospital.Visit v where v.Doctor->Name %startswith :param and v.Patient->Name %startswith :param
select count(*) into :p from Hospital.Visit v where v.Doctor->Name %startswith :param and v.Patient->Name %startswith :param and v.Patient->Address->State = :param1
Obviamente, lo anterior es la sintaxis para SQL Embebido (porque hay parámetros nombrados). Para SQL Dinámico, las consultas son casi iguales, pero en lugar de parámetros nombrados, tenéis parámetros no nombrados 😉. Por ejemplo, para la última consulta, tengo la siguiente consulta:
select count(*) from Hospital.Visit v where v.Doctor->Name %startswith ? and v.Patient->Name %startswith ? and v.Patient->Address->State = ?
Ahora, vamos a echar un vistazo a los resultados:
No of query
Embedded SQL (sec)
Dynamic SQL (sec)
1
49
12
2
3
3
3
32
26
4
47
46
5
48
46
6
47
46
7
1767
1806
8
1768
1841
9
31
26
10
83
81
11
41
45
12
73
71
13
23
26
14
1
1
15
2
2
16
3
3
Podéis ver un gran valor atípico, que es la primera consulta. El SQL Embebido tardó mucho más tiempo en ejecutarse que el SQL Dinámico. Ejecutando las mismas consultas varias veces obtuve más o menos el mismo resultado. Así que es lo que es.
En general, podéis ver que la relación padre-hijos es mucho más lenta desde el lado de la propiedad de los hijos, aunque todos los datos de Patient y Visit estén almacenados en el global de Patient. El índice salvó la situación para la relación de uno a muchos, y el tiempo de ejecución fue considerablemente más corto. En conjunto, el tiempo de respuesta es mayormente similar y difiere en menos del 10%; a veces, incluso es el mismo. Por supuesto, utilicé consultas simples que no tardaron demasiado en prepararse, así que esta etapa casi podría ser ignorada.
Artículo
Ricardo Paiva · 3 mayo, 2022
Todo el código fuente del artículo está disponible en: https://github.com/antonum/ha-iris-k8s
En el [artículo anterior](https://es.community.intersystems.com/post/implementaci%C3%B3n-de-iris-con-alta-disponibilidad-en-kubernetes-sin-mirroring), comentamos cómo configurar IRIS en el clúster k8s con alta disponibilidad, basado en el almacenamiento distribuido, en vez del *mirroring* tradicional. Como ejemplo, ese artículo utilizaba el clúster Azure AKS. En esta ocasión, seguiremos explorando las configuraciones de alta disponibilidad en los k8s. Esta vez, basado en Amazon EKS (servicio administrado para ejecutar Kubernetes en AWS) e incluiría una opción para hacer copias de seguridad y restauraciones de la base de datos, basado en Kubernetes Snapshot.
## Instalación
Vamos a lo más importante. Primero: necesitas una cuenta de AWS, y las herramientas [AWS CLI,](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html) [kubectl](https://kubernetes.io/docs/tasks/tools/) y [eksctl](https://docs.aws.amazon.com/eks/latest/userguide/eksctl.html) instaladas. Para crear el nuevo clúster, ejecuta el siguiente comando:
eksctl create cluster \
--name my-cluster \
--node-type m5.2xlarge \
--nodes 3 \
--node-volume-size 500 \
--region us-east-1
Este comando tarda unos 15 minutos, implementa el clúster EKS y lo convierte en un clúster predeterminado para tu herramienta kubectl. Puedes verificar la implementación ejecutando:
kubectl get nodes
NAME STATUS ROLES AGE VERSION
ip-192-168-19-7.ca-central-1.compute.internal Ready <none> 18d v1.18.9-eks-d1db3c
ip-192-168-37-96.ca-central-1.compute.internal Ready <none> 18d v1.18.9-eks-d1db3c
ip-192-168-76-18.ca-central-1.compute.internal Ready <none> 18d v1.18.9-eks-d1db3c
El siguiente paso es instalar el motor de almacenamiento distribuido Longhorn.
kubectl create namespace longhorn-system
kubectl apply -f https://raw.githubusercontent.com/longhorn/longhorn/v1.1.0/deploy/iscsi/longhorn-iscsi-installation.yaml --namespace longhorn-system
kubectl apply -f https://raw.githubusercontent.com/longhorn/longhorn/master/deploy/longhorn.yaml --namespace longhorn-system
Y, por último, el propio IRIS:
kubectl apply -f https://github.com/antonum/ha-iris-k8s/raw/main/tldr.yaml
En este momento, tendrás un clúster EKS completamente funcional con el almacenamiento distribuido de Longhorn y la implementación de IRIS instaladas. Puedes volver al artículo anterior e intentar hacer todo tipo de daño en el clúster y la implementación de IRIS, solo para ver cómo el sistema se repara a sí mismo. Echa un vistazo a la sección ["Simulación del error"](https://es.community.intersystems.com/post/implementaci%C3%B3n-de-iris-con-alta-disponibilidad-en-kubernetes-sin-mirroring).
## Bonus #1 IRIS en ARM
IRIS EKS y Longhorn son compatibles con la arquitectura ARM, por lo que podemos implementar la misma configuración utilizando instancias de AWS Graviton2, basadas en la arquitectura ARM.
Todo lo que necesitas es cambiar el tipo de instancia para los nodos EKS a la familia "m6g" y la imagen IRIS a la basada en ARM.
eksctl create cluster \
--name my-cluster-arm \
--node-type **m6g.2xlarge** \
--nodes 3 \
--node-volume-size 500 \
--region us-east-1
tldr.yaml
containers:
#- image: store/intersystems/iris-community:2020.4.0.524.0
- image: store/intersystems/irishealth-community-arm64:2020.4.0.524.0
name: iris
O simplemente utiliza:
kubectl apply -f https://github.com/antonum/ha-iris-k8s/raw/main/tldr-iris4h-arm.yaml
¡Eso es todo! Ya tienes el clúster IRIS Kubernetes, ejecutándose en la plataforma ARM.
## Bonus #2 Copia de seguridad y restauración
Una parte de la arquitectura para nivel de producción que se suele pasar por alto es la capacidad de crear copias de seguridad de la base de datos y que las restaure y/o clone rápidamente cuando sea necesario.
En Kubernetes, la manera más habitual de hacerlo es utilizando Persistent Volumen Snapshots (Snapshots de volumen persistente).
En primer lugar, debes instalar todos los componentes de k8s requeridos:
#Install CSI Snapshotter and CRDs
kubectl apply -n kube-system -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/client/config/crd/snapshot.storage.k8s.io_volumesnapshotcontents.yaml
kubectl apply -n kube-system -f https://github.com/kubernetes-csi/external-snapshotter/raw/master/client/config/crd/snapshot.storage.k8s.io_volumesnapshotclasses.yaml
kubectl apply -n kube-system -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/client/config/crd/snapshot.storage.k8s.io_volumesnapshots.yaml
kubectl apply -n kube-system -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/deploy/kubernetes/snapshot-controller/setup-snapshot-controller.yaml
kubectl apply -n kube-system -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/deploy/kubernetes/snapshot-controller/rbac-snapshot-controller.yaml
A continuación, configura las credenciales del bucket S3 para Longhorn (consulta [estas instrucciones](https://longhorn.io/docs/1.1.0/snapshots-and-backups/backup-and-restore/set-backup-target/)):
#Longhorn backup target s3 bucket and credentials longhorn would use to access that bucket
#See https://longhorn.io/docs/1.1.0/snapshots-and-backups/backup-and-restore/set-backup-target/ for manual setup instructions
longhorn_s3_bucket=longhorn-backup-123xx #bucket name should be globally unique, unless you want to reuse existing backups and credentials
longhorn_s3_region=us-east-1
longhorn_aws_key=AKIAVHCUNTEXAMPLE
longhorn_aws_secret=g2q2+5DVXk5p3AHIB5m/Tk6U6dXrEXAMPLE
El siguiente comando tomará las variables de entorno del paso anterior y las utilizará para configurar la copia de seguridad de Longhorn
#configure Longhorn backup target and credentials
cat <<EOF | kubectl apply -f -
apiVersion: longhorn.io/v1beta1
kind: Setting
metadata:
name: backup-target
namespace: longhorn-system
value: "s3://$longhorn_s3_bucket@$longhorn_s3_region/" # backup target here
---
apiVersion: v1
kind: Secret
metadata:
name: "aws-secret"
namespace: "longhorn-system"
labels:
data:
# echo -n '<secret>' | base64
AWS_ACCESS_KEY_ID: $(echo -n $longhorn_aws_key | base64)
AWS_SECRET_ACCESS_KEY: $(echo -n $longhorn_aws_secret | base64)
---
apiVersion: longhorn.io/v1beta1
kind: Setting
metadata:
name: backup-target-credential-secret
namespace: longhorn-system
value: "aws-secret" # backup secret name here
EOF
Puede parecer mucho, pero esencialmente le indica a Longhorn que utilice un bucket S3 específico con las credenciales precisas para almacenar el contenido de las copias de seguridad.
¡Eso es todo! Si ahora vas a la interfaz de usuario de Longhorn, podrás crear copias de seguridad, restaurarlas, etc.

Un recordatorio rápido sobre cómo conectarse a la interfaz de usuario de Longhorn:
kubectl get pods -n longhorn-system
# note the full pod name for 'longhorn-ui-...' pod
kubectl port-forward longhorn-ui-df95bdf85-469sz 9000:8000 -n longhorn-system
Esto reenviaría el tráfico desde la interfaz de usuario de Longhorn a tu http://localhost:9000
## Programación de la copia de seguridad/restauración
Hacer copias de seguridad y restauraciones a través de la interfaz de usuario de Longhorn podría ser un primer paso suficientemente bueno, pero vamos a ir un paso más allá y realizar copias de seguridad y restauraciones programáticamente, utilizando APIs de Snapshots k8s.
En primer lugar, el propio snapshot. iris-volume-snapshot.yaml
apiVersion: snapshot.storage.k8s.io/v1beta1
kind: VolumeSnapshot
metadata:
name: iris-longhorn-snapshot
spec:
volumeSnapshotClassName: longhorn
source:
persistentVolumeClaimName: iris-pvc
Este snapshot de volumen se refiere al volumen de la fuente "iris-pvc" que usamos para nuestra implementación de IRIS. Así que con solo aplicar esto, el proceso para crear una copia de seguridad se iniciaría inmediatamente .
Es una buena idea ejecutar el Freeze/Thaw de IRIS Write Daemon antes o después del snapshot.
#Freeze Write Daemon
echo "Freezing IRIS Write Daemon"
kubectl exec -it -n $namespace $pod_name -- iris session iris -U%SYS "##Class(Backup.General).ExternalFreeze()"
status=$?
if [[ $status -eq 5 ]]; then
echo "IRIS WD IS FROZEN, Performing backup"
kubectl apply -f backup/iris-volume-snapshot.yaml -n $namespace
elif [[ $status -eq 3 ]]; then
echo "IRIS WD FREEZE FAILED"
fi
#Thaw Write Daemon
kubectl exec -it -n $namespace $pod_name -- iris session iris -U%SYS "##Class(Backup.General).ExternalThaw()"
El proceso de restauración es bastante sencillo. Esencialmente se crea un nuevo PVC y se especifican los snapshots como la fuente.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: iris-pvc-restored
spec:
storageClassName: longhorn
dataSource:
name: iris-longhorn-snapshot
kind: VolumeSnapshot
apiGroup: snapshot.storage.k8s.io
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
A continuación, solo hay que crear una nueva implementación, basada en este PVC. Comprueba este [script de prueba en un repositorio de github](https://github.com/antonum/ha-iris-k8s/blob/main/backup/test.sh) que podría realizarse con la siguiente secuencia:
* Crear una nueva implementación de IRIS
* Añadir algunos datos a IRIS
* Freeze Write Daemon, tome un snapshot, thaw Write Daemon
* Crear un clon de implementación de IRIS, basado en el snapshot
* Verificar que todos los datos todavía están allí
En este momento tendrá dos implementaciones idénticas de IRIS, una de las cuales es un clon hecho por medio de una copia de seguridad de la otra.
¡Espero que os resulte útil!
Artículo
Ricardo Paiva · 21 nov, 2022
En la primera parte de este artículo he mostrado cómo empezar un nuevo proyecto en Django, y cómo definir nuevos modelos y añadir modelos ya existentes.
En esta publicación, voy a mostrar un Panel de Administración (disponible con la configuración predeterminada) y cómo puede ser útil.
Nota importante: si intentáis reproducir los pasos de este artículo, no funcionará para vosotros. Porque mientras escribía la publicación he realizado varios ajustes en el proyecto django-iris, e incluso en el driver DB-API de InterSystems, para arreglar algunos problemas ahí también, y creo que el driver aún está en desarrollo y habrá un driver más estable en el futuro. Así que vamos a asumir que este artículo solo explica cómo podría ser si tuviéramos todo terminado.
Vamos a volver a nuestro código y ver lo que tenemos en urls.py, el principal punto de entrada de todas las solicitudes web.
"""main URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/4.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path
urlpatterns = [
path('admin/', admin.site.urls),
]
Se puede ver que ya hay una URL admin definida ahí.
Empezamos el servidor de desarrollo, por comando
python manage.py runserver
Si se va por la URL http://localhost:8000/admin, aparece el formulario de inicio de sesión de Django administration
Para entrar aquí, hace falta un usuario, que podemos crear con este comando
$ python manage.py createsuperuser
Username (leave blank to use 'daimor'): admin
Email address: admin@example.com
Password:
Password (again):
The password is too similar to the username.
This password is too short. It must contain at least 8 characters.
This password is too common.
Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.
Ya podemos usar ese usuario y contraseña. Está bastante vacío ahora, pero ya da acceso a Groups y Users.
Más datos
Previamente ya he instalado el paquete post-and-tags con zpm. Lo podéis instalar también
zpm "install posts-and-tags"
Ahora podemos obtener los modelos para todas las tablas (community.post, community.comment, community.tag) instaladas con este paquete
$ python manage.py inspectdb community.post community.comment community.tag > main/models.py
Esto producirá un archivo un poco largo, así que lo he puesto en este desplegable:
main/models.py
# This is an auto-generated Django model module.
# You'll have to do the following manually to clean this up:
# * Rearrange models' order
# * Make sure each model has one field with primary_key=True
# * Make sure each ForeignKey and OneToOneField has `on_delete` set to the desired behavior
# * Remove `managed = False` lines if you wish to allow Django to create, modify, and delete the table
# Feel free to rename the models, but don't rename db_table values or field names.
from django.db import models
class CommunityPost(models.Model):
acceptedanswerts = models.DateTimeField(db_column='AcceptedAnswerTS', blank=True, null=True) # Field name made lowercase.
author = models.CharField(db_column='Author', max_length=50, blank=True, null=True) # Field name made lowercase.
avgvote = models.IntegerField(db_column='AvgVote', blank=True, null=True) # Field name made lowercase.
commentsamount = models.IntegerField(db_column='CommentsAmount', blank=True, null=True) # Field name made lowercase.
created = models.DateTimeField(db_column='Created', blank=True, null=True) # Field name made lowercase.
deleted = models.BooleanField(db_column='Deleted', blank=True, null=True) # Field name made lowercase.
favscount = models.IntegerField(db_column='FavsCount', blank=True, null=True) # Field name made lowercase.
hascorrectanswer = models.BooleanField(db_column='HasCorrectAnswer', blank=True, null=True) # Field name made lowercase.
hash = models.CharField(db_column='Hash', max_length=50, blank=True, null=True) # Field name made lowercase.
lang = models.CharField(db_column='Lang', max_length=50, blank=True, null=True) # Field name made lowercase.
name = models.CharField(db_column='Name', max_length=250, blank=True, null=True) # Field name made lowercase.
nid = models.IntegerField(db_column='Nid', primary_key=True) # Field name made lowercase.
posttype = models.CharField(db_column='PostType', max_length=50, blank=True, null=True) # Field name made lowercase.
published = models.BooleanField(db_column='Published', blank=True, null=True) # Field name made lowercase.
publisheddate = models.DateTimeField(db_column='PublishedDate', blank=True, null=True) # Field name made lowercase.
subscount = models.IntegerField(db_column='SubsCount', blank=True, null=True) # Field name made lowercase.
tags = models.CharField(db_column='Tags', max_length=350, blank=True, null=True) # Field name made lowercase.
text = models.TextField(db_column='Text', blank=True, null=True) # Field name made lowercase.
translated = models.BooleanField(db_column='Translated', blank=True, null=True) # Field name made lowercase.
type = models.CharField(db_column='Type', max_length=50, blank=True, null=True) # Field name made lowercase.
views = models.IntegerField(db_column='Views', blank=True, null=True) # Field name made lowercase.
votesamount = models.IntegerField(db_column='VotesAmount', blank=True, null=True) # Field name made lowercase.
class Meta:
managed = False
db_table = 'community.post'
class CommunityComment(models.Model):
id1 = models.CharField(db_column='ID1', primary_key=True, max_length=62) # Field name made lowercase.
acceptedanswerts = models.DateTimeField(db_column='AcceptedAnswerTS', blank=True, null=True) # Field name made lowercase.
author = models.CharField(db_column='Author', max_length=50, blank=True, null=True) # Field name made lowercase.
avgvote = models.IntegerField(db_column='AvgVote', blank=True, null=True) # Field name made lowercase.
correct = models.BooleanField(db_column='Correct', blank=True, null=True) # Field name made lowercase.
created = models.DateTimeField(db_column='Created', blank=True, null=True) # Field name made lowercase.
hash = models.CharField(db_column='Hash', max_length=50, blank=True, null=True) # Field name made lowercase.
id = models.IntegerField(db_column='Id') # Field name made lowercase.
post = models.CharField(db_column='Post', max_length=50, blank=True, null=True) # Field name made lowercase.
text = models.TextField(db_column='Text', blank=True, null=True) # Field name made lowercase.
texthash = models.CharField(db_column='TextHash', max_length=50, blank=True, null=True) # Field name made lowercase.
type = models.CharField(db_column='Type', max_length=50) # Field name made lowercase.
votesamount = models.IntegerField(db_column='VotesAmount', blank=True, null=True) # Field name made lowercase.
class Meta:
managed = False
db_table = 'community.comment'
unique_together = (('type', 'id'),)
class CommunityTag(models.Model):
description = models.TextField(db_column='Description', blank=True, null=True) # Field name made lowercase.
name = models.TextField(db_column='Name', primary_key=True) # Field name made lowercase.
class Meta:
managed = False
db_table = 'community.tag'
El dashboard de administración en Django, se puede extender mediante desarrollo. Y es posible añadir algunas tablas más. Para hacerlo, tenemos que añadir un nuevo archivo llamado main/admin.py. He añadido unos pocos comentarios directamente en el código, para explicar algunas líneas.
from django.contrib import admin
# immport our community models for our tables in IRIS
from .models import (CommunityPost, CommunityComment, CommunityTag)
# register class which overrides default behaviour for model CommunityPost
@admin.register(CommunityPost)
class CommunityPostAdmin(admin.ModelAdmin):
# list of properties to show in table view
list_display = ('posttype', 'name', 'publisheddate')
# list of properties to show filter for on the right side of the tablee
list_filter = ('posttype', 'lang', 'published')
# default ordering, means from the latest date of PublishedDate
ordering = ['-publisheddate', ]
@admin.register(CommunityComment)
class CommunityCommentAdmin(admin.ModelAdmin):
# only this two fields show, (post is numeric by id in table post)
list_display = ('post', 'created')
# order by date of creation
ordering = ['-created', ]
@admin.register(CommunityTag)
class CommunityTagAdmin(admin.ModelAdmin):
# not so much to show
list_display = ('name', )
Portal extendido
Vamos a volver a nuestra página de administración en Django, y vemos algunas cosas nuevas ahí
A la derecha se ve el panel de filtros, y lo que es importante es que tiene todos los posibles valores de cada campo.
Desafortunadamente, InterSystems SQL no es compatible con la funcionalidad LIMIT, OFFSET. Y Django no es compatible con TOP. Así que se mostrará aquí la paginación, pero no funciona. Y no hay forma de hacerla funcionar por el momento, y no creo que nunca funcione, lamentablemente.
Se puede incluso analizar el objeto, y Django mostrará el formulario, con los tipos de campo correctos. (Nota: Este conjunto de datos no contiene datos en el campo Text)
Objeto Comments
Problemas esperados con licencias de la Community Edition
Si trabajáis con la Community Edition, podéis encontraros este problema, así es como se ve cuando todas las conexiones están ocupadas, puede pasar muy rápido. Y si véis que la respuesta del servidor es bastante larga, seguramente es el caso, porque IRIS no responde rápido en este caso; por alguna razón lleva mucho tiempo.
Incluso cuando IRIS dice, te queda capacidad
No permite más de 5 conexiones, así que necesitaréis terminar uno o más procesos para que funcione. O reiniciar el servidor de Django
En desarrollo, también se puede limitar el servidor Django al modo no threading, y funcionará en un proceso. Y no debería solicitar más conexiones a IRIS.
python manage.py runserver --nothreading
Artículo
Daniel Aguilar · 26 jun, 2023
¡Hola Comunidad!
¿Habéis tenido que conectar alguna vez IRIS con un sistema SAP?
Tuve que enfrentarme al reto de conectar InterSystems IRIS con SAP, y una vez más pude comprobar el gran acierto que hizo InterSystems añadiendo la posibilidad de ejecutar código nativamente de Python desde IRIS.
Esto me hizo la integración muy fácil gracias a la librería pyrfc.
Con esta librería, fui capaz de realizar llamadas a RFC's de SAP (Remote Function Call) desde una clase de IRIS y recibir datos de la base de datos de SAP.
Pero.., ¿qué es una RFC?
RFC es un protocolo de comunicación usado por SAP. Permite la interacción entre diferentes sistemas SAP o no SAP. También permite a aplicaciones externas comunicarse con el sistema SAP y acceder a las funciones y datos disponibles.
Cuando una RFC es ejecutada, una aplicación externa envía una petición sobre la red al sistema SAP. El sistema SAP recibe la petición, la procesa, y devuelve la respuesta con el resultado o datos a la aplicación externa.
RFC se puede usar para tareas de integración, ejecución de programas y recibir información desde SAP.
En resumen, una RFC de SAP es una llamada remota a una función o servicio disponible en un sistema SAP que permite la comunicación y el intercambio de información entre sistemas, sistemas externos o aplicaciones.
Finalidad del artículo:
Me he decidido a escribir este artículo para describir los pasos que he seguido y el resultado recibido por si alguno se ve en la misma situación poder facilitarle la tarea.
También he publicado una aplicación en el OpenExchange (link) con casi todo lo necesario para arrancar el Docker y conectar con el sistema SAP.
¿Porqué digo casi todo?. Para poder conectar con un servidor SAP, necesitamos descargar el SAP NetWeaver RFC SDK. Por desgracias, está protegido por copyright, y, por este motivo, no puedo compartir los fichero con vosotros. Por lo que si queréis descargar el SDK de la página de SAP debéis tener un usuario de tipo "S".
La aplicación del OpenExchange está lista para descargar, añadir los ficheros del SDK, configurar la conexión con el servidor SAP y ejecutar.
¿Te interesa?, Genial, pues vamos!!
Pasos para descargar SAP NetWeaver RFC SDK:
Una vez tengamos nuestro usuario tipo "S" (Si no tienes un usuario tipo "S" pide a tu administrador del sistema SAP que te facilite uno), debemos entrar en el siguiente link.
Hacemos clic en el link con las instrucciones de descarga:
Después, clic en el link de descarga de los ficheros:
Finalmente, clic en “SAP NW RFC SDK 7.50”
Elegimos Linux X86_64 Bit version (si tienes planeado usar mi aplicación del OpenExchange con Docker) y hacemos click en el botón "descargar":
Añadiendo los ficheros SAP NetWeaver RFC SDK a el projecto:
Cuando tengamos descargados y descomprimidos los ficheros, debemos copiar el contenido de la carpeta "nwrfcsdk" dentro de la carpeta "nwrfcsdk" del proyecto.
Explicación de los archivos auxiliares y el Dockerfile:
El fichero "nwrfcsdk.conf" debe contener la ruta donde los ficheros SDK van a ser copiados. Si no tienes pensado cambiar la ruta no es necesario modificar nada.
El fichero Dockerfile ya incluye el código necesario para descargar e instalar las librerías necesarias para realizar la conexión:
Lo que hace es copiar el fichero "nwrfcsdk.conf", el contenido de la carpeta "nwrfcsdk" y el fichero "sapnwrfc.cfg" (este fichero lo explico luego) dentro del Docker.
También instala las librerías de Python:
pyrfc: se usa para ejecutar las RFC de SAP
configparser: se usa para leer los parámetros de la conexión almacenados en el fichero de configuración sapnwrfc.cfg
Configurando la conexión con el servidor SAP:
Para conectar con el servidor SAP, debes completar la información requerida en el fichero "sapnwrfc.cfg".
Si no tienes la información, pero puedes entrar en tu servidor SAP, sigue los siguientes pasos para encontrarla. Si no tienes acceso al servidor SAP, deberás preguntarle al administrador del sistema SAP la información.
Obtener la información de conexión desde el servidor SAP:
Las capturas de pantalla puede variar en función de si estás usando SAP GUI (usuarios Windows) o SAP GUI for JAVA (usuarios macOS y Linux). En mi caso, es SAP GUI for JAVA.
1 - Entra en SAP GUI y elige tu servidor. Después, haz click en el botón conectar:
2 - Introduce tu usuario y contraseña y aprieta "Enter" o haz clic en el botón aceptar:
3 - Una vez estés dentro del sistema, ve a la barra de menús y selecciona "System" y después la opción "Status" en el desplegable:
En esta ventana, tu puedes obtener la siguiente información:
El valor del campo "Client" para la propiedad "client" del fichero de configuración.
El valor del campo "User" para la propiedad "user" del fichero de configuración (En este caso, mi usuario del sistema SAP y el de RFC es el mismo. Aunque para entornos de producción, es recomendable usar usuarios diferentes).
El valor del campo "Host" para la propiedad "ashost" del fichero de configuración. (Puede ser un nombre de equipo o una dirección IP).
En el caso de que tu usuario para RFC sea el mismo que el del sistema SAP, debes usar la misma contraseña en la propiedad "passwd" del fichero de configuración.
Si tienes problemas con la conexión, comprueba la información con tu administrador de sistemas de SAP que tu usuario tiene permisos para ejecutar RFC's.
Una vez tengamos los pasos listos, estamos listos para empezar!!.
Comprobando la conexión con SAP
Para comprobar la conexión con SAP, puedes ejecutar el metodo TestConecction del la clase RFC.RFCUtil.cls
ClassMethod TestConnection() [ Language = python ]
{
try:
# Import python libraries
from pyrfc import Connection, ABAPApplicationError, ABAPRuntimeError, LogonError, CommunicationError
from configparser import ConfigParser
# Connect to the SAP Server
config = ConfigParser()
config.read('/opt/irisapp/sapnwrfc.cfg')
params_connection = config._sections['connection']
conn = Connection(**params_connection)
# Launch RFC call to STFC_CONNECTION
result = conn.call('STFC_CONNECTION', REQUTEXT=u'Hello SAP!')
# Close the connection
conn.close()
# Print the result
print(result)
except Exception as e:
print(e)
}
Si todo va bien, deberíais ver un mensaje en la consola como este:
(Si recibes un error por favor comprueba la información del fichero "sapnwrfc.cfg".)
Como puedes ver, la manera de ejecutar una RFC es muy fácil:
1 - Conectamos con el servidor SAP
2 - Ejecutamos el metodo conn.call('Nombre de la RFC a ejecutar', Parametros RFC)
3 - Cerramos la conexión
4 - Procesamos el resultado
En este ejemplo, el único parámetro admitido es un String, y lo hemos enviado como un parámetro del tipo REQUTEXT.
Consultar la información de una tabla de SAP
Ahora vamos a llamar a la RFC "RFC_READ_TABLE" que nos permite leer una tabla de SAP.
ClassMethod GetDataTableSFLIGHT() [ Language = python ]
{
try:
# Import python libraries
from pyrfc import Connection, ABAPApplicationError, ABAPRuntimeError, LogonError, CommunicationError
from configparser import ConfigParser
# Connect to the SAP Server
config = ConfigParser()
config.read('/opt/irisapp/sapnwrfc.cfg')
params_connection = config._sections['connection']
conn = Connection(**params_connection)
# Define query parameters
params = {
'QUERY_TABLE': 'SFLIGHT',
'DELIMITER': ',',
'FIELDS': [
{'FIELDNAME': 'CARRID'},
{'FIELDNAME': 'CONNID'},
{'FIELDNAME': 'FLDATE'},
{'FIELDNAME': 'PRICE'}
],
'OPTIONS': [],
}
# Call to función RFC 'RFC_READ_TABLE'
result = conn.call('RFC_READ_TABLE', **params)
# Process results
if 'DATA' in result:
data = result['DATA']
fields = result['FIELDS']
# Imprime los nombres de campo
for field in fields:
print(field['FIELDNAME'], end='\t')
print()
# Imprime los datos
for entry in data:
values = entry['WA'].split(',')
for value in values:
print(value, end='\t')
print()
else:
print('No data found.')
# Close SAP connection
conn.close()
except CommunicationError:
print("Could not connect to server.")
raise
except LogonError:
print("Could not log in. Wrong credentials?")
raise
except (ABAPApplicationError, ABAPRuntimeError):
print("An error occurred.")
raise
except Exception as e:
print(e)
}
Para este ejemplo, hemos pasado como parámetro una variable tipo estructura con la siguiente información:
QUERY_TABLE: Es la tabla que queremos consultar (SFLIGHT tabla demo de vuelos de SAP)
DELIMITER: Nos permite elegir el separador
FIELDS: Son las columnas que queremos consultar, en este caso estoy consultado CARRID (Id de la compañia), CONNID (Id conexión de vuelo), FLDATE (fecha del vuelo), y PRICE (precio del vuelo).
Una vez ejecutado, podemos procesar la respuesta y imprimirla por pantalla o lo que queramos hacer con ella... xD.
Finalmente, cerramos la conexión.
Si todo ha ido bien, deberíais ver algo parecido a esto en la consola:
Consultando datos de una tabla filtrando:
Este va a ser un ejemplo un como mas complejo, vamos a consultar la tabla de clientes para obtener los datos de un único cliente filtrando por su id.
ClassMethod GetDataCustomer(clientSapID As %String) [ Language = python ]
{
try:
from pyrfc import Connection, ABAPApplicationError, ABAPRuntimeError, LogonError, CommunicationError
from configparser import ConfigParser
config = ConfigParser()
config.read('/opt/irisapp/sapnwrfc.cfg')
params_connection = config._sections['connection']
conn = Connection(**params_connection)
# Define the parameters
params = {
'QUERY_TABLE': 'KNA1',
'DELIMITER': ',',
'FIELDS': [{'FIELDNAME': 'KUNNR'}, {'FIELDNAME': 'SORTL'}, {'FIELDNAME': 'TELF1'}],
'OPTIONS': [{'TEXT': "KUNNR = '" + clientSapID + "'"}],
}
# Call the RFC 'RFC_READ_TABLE' to obtain the data
result = conn.call('RFC_READ_TABLE', **params)
# Process the result
if 'DATA' in result:
data = result['DATA']
fields = result['FIELDS']
# Print the fields names
for field in fields:
print(field['FIELDNAME'], end='\t')
print()
# Print the data fields.
for entry in data:
values = entry['WA'].split(',')
for value in values:
print(value, end='\t')
print()
else:
print('No data found.')
# Close the connection
conn.close()
except CommunicationError:
print("Could not connect to server.")
raise
except LogonError:
print("Could not log in. Wrong credentials?")
raise
except (ABAPApplicationError, ABAPRuntimeError):
print("An error occurred.")
raise
except Exception as e:
print(e)
}
Parámetros:
QUERY_TABLE: Es la tabla que queremos consultar (KNA1 es la tabla de clientes en SAP)
DELIMITER: Nos permite elegir el separador
FIELDS: Son las columnas que queremos consultar, en este caso estoy consultando KUNNR (id de cliente SAP), SORTL (nombre corto cliente), y TELF1 (Teléfono del cliente).
OPTIONS: Es el filtro que queremos aplicar a la consulta. En este caso, estamos filtrando por el id de cliente recibido por parámetro en el metodo.
Una vez ejecutado, podemos procesar la respuesta e imprimirla por pantalla o lo que queramos... xD.
Finalmente, cerramos la conexión.
Si todo ha ido bien, deberíamos ver algo similar a esto:
Esto es solo el principio ya que gracias a las llamadas RFC, podemos no solo realizar operaciones de creación, lectura, actualizado, y borrado, también podemos ejecutar cualquier código de SAP (estandard o personalizado) expuesto por una RFC, programas, modulos de funciones, etc... RFC se no solo se usa para leer o escribir datos, también se puede usar para ejecutar programas o funciones.
Espero que algún día si os veis en la situación de integrar InserSystems IRIS con SAP, este artículo pueda seros de ayuda.
Si tenéis cualquier duda, por favor escribidme en los comentarios e intentaré ayudaros.
¡Muchas gracias por leerme!!
Link a la aplicación en OpenExchange: https://openexchange.intersystems.com/package/IrisSapConnector
Link al repositorio: https://github.com/daniel-aguilar-garcia/IrisSapConnector/blob/main/sapnwrfc.cfg
Link a la librería pyrfc: https://sap.github.io/PyRFC/ Gracias por el aporte @Daniel.Aguilar . Muy interesante!
Artículo
Javier Lorenzo Mesa · 16 jul, 2021
Tengo algunos modelos analíticos y numerosos paneles de control, y estoy listo para implementarlos en nuestros usuarios finales y administradores. ¿Cómo configurar DeepSee para que los usuarios no alteren las áreas de los demás y se les restrinja el uso de funciones específicas para los desarrolladores?
Ejecutar un sistema de Business Intelligence requiere, con frecuencia, configurar un modelo de seguridad. Este tutorial mostrará cómo configurar un modelo de seguridad sencillo para DeepSee.
El modelo de seguridad se basa en tres tipos de usuarios. Primero, crearemos un usuario sencillo para DeepSee que tenga acceso pero no pueda editar los paneles de control en DeepSee. El segundo tipo de usuario tendrá acceso a las tablas dinámicas en Analyzer y podrá visualizar, editar y crear paneles de control. Finalmente, los usuarios que son “Administradores” tendrán un control más amplio de la implementación, como el acceso a Architect.
También veremos cómo proteger y controlar la visibilidad de los elementos del modelo, como tablas dinámicas, paneles de control, modelos analíticos, etc. Esperamos que estos consejos para solucionar problemas faciliten la implementación de un modelo de seguridad.
Antes de empezar
En este artículo configuraremos un modelo de seguridad básico. Para ello, es necesario familiarizarse con esta página sobre Cómo configurar la seguridad para DeepSee. Si estás probando o creando una prueba de concepto, no utilices la base de datos o el namespace SAMPLES, ya que tiene una configuración especial. En vez de ello, trabaja en un namespace (por ejemplo, APP) con base(s) de datos dedicada(s) (por ejemplo, APP-DATA).
Para continuar con este tutorial, crea un namespace APP basado en una base de datos de APP-DATA en el Portal de administración de seguridad [SMP] > Configuration > System Configuration > Namespaces. Asigna un nuevo recurso %DB_APP-DATA a la base de datos recién creada. Asegúrate de que la aplicación web predeterminada para el namespace APP (/csp/app) está habilitada en DeepSee. Asumimos que tienes un servidor o una instalación personalizada, desde los que puedes iniciar sesión en Caché con un usuario que tiene los privilegios suficientes para ejecutar las operaciones de esta publicación.
Cómo conceder acceso de solo lectura a los paneles de control
En una implementación habitual, se permite que los usuarios finales utilicen Analytics, pero no pueden editar la implementación como tal. En esta sección definiremos un tipo de usuario que solo puede acceder pero no editar los paneles de control de DeepSee.
Cómo crear un rol DSUser
Según la documentación (mira la fila de la tarea "Viewing the User Portal apart from the Analyzer or the mini Analyzer with no ability to create dashboards" / “Ver el Portal de usuario de forma independiente a Analyzer o al mini Analyzer sin la capacidad de crear paneles” en la tabla), necesitamos permisos de USO para utilizar los recursos de %DeepSee_Portal.
Crea un rol DSUser que incluya los siguientes recursos:
Recurso
Permiso
%DeepSee_Portal
USE
%DB_APP-DATA
RW
Los permisos U y RW para el recurso que se encuentra en la tabla anterior deben configurarse automáticamente cuando asignes los roles. Dependiendo de tu namespace, base de datos y configuración de diagramas, necesitarás un permiso RW para proteger los recursos de las bases de datos. En nuestro ejemplo, %DB_APP-DATA es necesario para acceder a la base de datos predeterminada del namespace APP.
Cómo crear un simpleuser
En Users page > Create new user puedes crear un simpleuser con el rol DSUser asignado, como se muestra en esta captura de pantalla:
Cómo probar simpleuser
Abre una ventana de incógnito o privada en el navegador e inicia sesión como simpleuser. Desde el portal de administración, comprueba que las pestañas Architect y Analyzer en la sección DeepSee aparecen sombreadas en gris. Ve al User Portal / Portal de Usuario y confirma que simpleuser puede visualizar los paneles de control. También confirma que simpleuser no puede ver el botón Save en los paneles de control ni el icono “+” en el User Portal para crear elementos en la carpeta. Ten en cuenta que simpleuser puede visualizar tablas dinámicas en el User Portal, pero cuando intente ver una tabla dinámica debería mostrar que el usuario no está autorizado a ver la página. En esta sección de la parte 5, más adelante, veremos cómo ocultar las tablas dinámicas en el User Portal.
Consejo: utiliza dos ventanas del navegador. Una ventana se puede utilizar para iniciar sesión con un usuario administrador (por ejemplo, _SYSTEM o SuperUser) que puede cambiar la configuración del sistema. En la otra ventana, utiliza un navegador en modo incógnito (Chrome) o privado (Firefox, Edge) e inicia sesión con un usuario de prueba. El modo incógnito/privado asegurará que el caché del navegador de la otra ventana no interfiere con tu trabajo y genere algún comportamiento inesperado.
Consejo: en el Portal de administración utiliza el botón Menu en la esquina superior izquierda para navegar rápidamente a las páginas de Users / Usuarios, Roles / Funciones, Resources / Recursos y Web applications / Aplicaciones web.
Cómo solucionar problemas: permisos públicos
Un problema habitual es encontrar que se permiten algunas funciones a pesar del modelo de seguridad. Como se explica de forma más amplia en la parte 5, una causa habitual de este comportamiento inesperado son los permisos públicos sobre los recursos. Por ejemplo, podrías ver que simpleuser es capaz de crear nuevos paneles de control en el User Portal. Si el recurso %DeepSee\_PortalEdit todavía tiene asignados permisos públicos de USE, cualquier usuario podrá crear tablas dinámicas y paneles de control. Para resolver este problema, elimina los permisos públicos de USE en el recurso %DeepSee\_PortalEdit.
En la parte 2 crearemos un segundo tipo de usuario que pueda editar y crear tablas dinámicas y paneles de control en DeepSee.
Este artículo está etiquetado como "Mejores prácticas" ("Best practices")
(Los artículos con la etiqueta "Mejores prácticas" incluyen recomendaciones sobre cómo desarrollar, probar, implementar y administrar mejor las soluciones de InterSystems).
Artículo
Ricardo Paiva · 11 mar, 2021
Quería escribirlo como comentario al [artículo](https://community.intersystems.com/post/useful-commands-you-day-day-development-docker-containers) de @Evgeny.Shvarov. Pero resultó demasiado largo, así que decidí publicarlo por separado.

Me gustaría añadir una pequeña aclaración sobre cómo utiliza Docker el espacio en disco y como limpiarlo. Yo uso macOS, por lo tanto todo lo que explico aplica principalmente a macOS, pero los comandos de Docker se adaptan a cualquier plataforma.
Cuando Docker se incluye en Linux, por defecto funciona en el mismo sistema de archivos. Pero en Windows y macOS, funciona en una pequeña máquina virtual con su propio Linux dentro. Y el espacio del disco está limitado por mi configuración en Docker. En mi caso, lo configuré para utilizar hasta 112 GB.
.png)
Por lo tanto, cuando trabajes de forma activa con Docker, tu espacio interior dejará de usarse. Puedes comprobar como Docker emplea todo ese espacio con el comando:
$ docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 84 6 66.02GB 55.6GB (84%)
Containers 6 5 4.914GB 0B (0%)
Local Volumes 19 4 1.812GB 342.7MB (18%)
Build Cache 0 0 0B 0B
En macOS con las últimas versiones de Docker, se utiliza el formato en bruto del disco (anteriormente era qcow2). Y junto con el sistema de archivos APFS en macOS, este archivo puede ocupar menos espacio físico que el propio tamaño del archivo. Observa estos dos comandos.
$ ls -lh ~/Library/Containers/com.docker.docker/Data/vms/0/Docker.raw
-rw-r--r--@ 1 daimor staff 104G Jul 13 15:49 /Users/daimor/Library/Containers/com.docker.docker/Data/vms/0/Docker.raw
$ du -h ~/Library/Containers/com.docker.docker/Data/vms/0/Docker.raw
88G /Users/daimor/Library/Containers/com.docker.docker/Data/vms/0/Docker.raw
El comando `ls` muestra el tamaño de mi archivo Docker.raw como 104Gb, mientras que el comando `du` muestra el tamaño real en el disco, que es de 88Gb.
Bien, `docker system df` me mostró que puedo recuperar algo de espacio. Vamos a hacerlo.
$ docker system prune -f
Deleted Containers:
79b3d54ae5a881e37771cfdc1d651db9ce036abc297dc55bdd454eb287f0e329
Deleted Images:
deleted: sha256:298d555976effb112428ed3f6bcc2f4d77ab02b4f287a230d9535001184078f5
deleted: sha256:adb2c64ce6e44d837fce8067c7498574822bff90ed599d1671c126539fe652ac
deleted: sha256:9695172139cec16f1071449daf29bd1c424353044088b92b8acbf33f59952e67
deleted: sha256:24d834b252e25e645b8b5d9194360f5ab1a26ffd2b5c03b6593b9a2c468f59fa
deleted: sha256:1b4e3e73fe0b7d88d5ec718bdc6dc6d17d9fe8ba00988eb72690d76f2da3d1a3
deleted: sha256:9f218f6c7aca9c21760ae43590a2d73b35110e10b6575125ed3ccd12c4495d6e
deleted: sha256:b2fa3335d672a0dc60ea7674c45ee3c85b9fc86584a0e21cc7f1900c368ceec3
deleted: sha256:2ecace396ab65fd393dfb2e330bece974cd952e7a41364352f9c867d9ea4c34e
deleted: sha256:16b894351fe53b95dd43d7437bbbcd5104b8613bc1fa8480826e843d65fc92a3
deleted: sha256:b00d9c05035eac62f3ed99a814cd6feea3e4b68797b6d1203e2f41538c78c2aa
deleted: sha256:5a3d0d9f36b356cb47d3838585da6450c60e2860ef143d1406b48d3a5e72b92b
deleted: sha256:998e719368ff74d13b3a8c096ce81f8f2c4bb28bd1ccd169bfa173b4a78d2e74
deleted: sha256:a74d7ff2ca7d623134f9ce1db40da476134a733935a3f322ba34b99653c6273d
deleted: sha256:4d0dcd2bdad2cf0cb91d13313afff29326771bdac27fcb8780545687dbd39ae4
deleted: sha256:29a8989eed3d4002053f98bf562654910ee5f8836940daaa2f2344a8f29a52a2
deleted: sha256:12d34fbf938d19b193199ea6cce5d690fd0d57ec3f4d1630e1d4b3790379c9ec
deleted: sha256:75aba481bb5ccaa52a3aadf311ae22485fb2a82d69be864fe2f45f2834c5e515
deleted: sha256:326efafee9b92e06876878b21a2931ba771bc0e0b2b359f906ef6cca1d297905
deleted: sha256:913937f4ea932fcb00b6c6b3007970296955aa4f488d6fbaa1a575a5aa4ff5ab
deleted: sha256:f3fc0c75858a36ff9d3f4e8eb7a96f511158bbac92d128760b0d3340d828c5da
deleted: sha256:c002dde1ea6a02ae3e3037442a5c556a925e3e4750a6b2aa923c51fa3d11f5ac
deleted: sha256:e763f6e226613c67aaf5984e4c74b9f6e9e28e0490a4f3286885f498a57d3fa0
deleted: sha256:e7daf0a1574376c8602515dc70e44392d16e1b79013d6e81a9b697794432e660
deleted: sha256:ce33670f78109dcacc73a7c6d70f4b5cd4a13bcfe7878b9df5e4e16b812e5df4
deleted: sha256:95bf79e86f83ed16943f9c34035bf8167a4b89466a05d6793c2957d6d46bab2d
deleted: sha256:056d184391613b33303ccf3818a95a17398e9da813d093a6ee5d87952539380c
Total reclaimed space: 5.537GB
Este comando elimina cualquier contenedor detenido y cualquier imagen no etiquetada que no se esté utilizando por cualquier imagen etiquetada. Y se puede eliminar de forma segura.
Tal vez has notado que solo se recuperaron 5.5 GB, mientras que `docker system df` hablaba de unos 55 GB. Eso es porque df cuenta todas las imágenes no activas, no solo las activas. Si también quieres eliminar todas esas imágenes, puedes utilizar este comando, lo que elimina cualquier imagen que no se utilice en los contenedores que estén ejecutándose en este momento. Por lo tanto, si no tienes ningún contenedor funcionando, eliminará todas las imágenes locales.
docker system prune -a
Acabo de recuperar solo las imágenes activas y los contenedores detenidos. Cuánto espacio utiliza mi docker ahora.
$ docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 83 5 60.48GB 50.1GB (82%)
Containers 5 5 4.914GB 0B (0%)
Local Volumes 19 3 1.812GB 342.7MB (18%)
Build Cache 0 0 0B 0B
Como puedes ver, ya utiliza menos tamaño. `ls` mostrará el mismo resultado. El tamaño del archivo principalmente crece.
$ ls -lh ~/Library/Containers/com.docker.docker/Data/vms/0/Docker.raw
-rw-r--r--@ 1 daimor staff 104G Jul 13 16:07 /Users/daimor/Library/Containers/com.docker.docker/Data/vms/0/Docker.raw
Pero para macOS es más importante cuánto espacio se utiliza en un disco físico.
$ du -h ~/Library/Containers/com.docker.docker/Data/vms/0/Docker.raw
69G /Users/daimor/Library/Containers/com.docker.docker/Data/vms/0/Docker.raw
Y como puedes ver ahora son 69 GB, que son aproximadamente 19 GB menos de los que eran anteriormente.
Así que, para los usuarios de macOS, realmente no importa el tamaño del archivo, con las optimizaciones de APFS en realidad puede ser menor.
Otra forma es reducir las imágenes antiguas con algún filtro por fecha de creación. Al igual que este ejemplo, se eliminarán todas las imágenes creadas hace más de 10 días, pero se mantendrán las imágenes que actualmente utilizan los contenedores.
$ docker image prune --all --filter until=240h
Este artículo ha sido etiquetado como "Mejores prácticas" ("Best practices").
Los artículos con la etiqueta "Mejores prácticas" incluyen recomendaciones sobre cómo desarrollar, probar, implementar y administrar mejor las soluciones de InterSystems.
Artículo
Alberto Fuentes · 19 oct, 2021
¡Hola desarrolladores!
Últimamente he estado probando el módulo [csvgen](https://openexchange.intersystems.com/package/csvgen) y buscaba un fichero CSV para probar. Resulta que encontré un fichero [muy interesante](https://data.world/datasaurusrex/game-of-thones-deaths) en Data.World con estadísticas sobre los episodios de *Game of Thrones* (Juego de Tronos). Estadísticas sobre muertes 😱.
¡Han documentado todos los asesinatos a lo largo de las 8 temporadas y han anotado dónde, quién, qué clan y con qué arma ha matado a otro personaje!
Así que lo he importado para hacer un cuadro de mando con IRIS Analytics.
No te preocupes, Jon. Con este cuadro de mando podremos hacer que averigües algo. Como resultado de esta «investigación» tenemos este [cuadro de mando](http://35.205.133.201:52773/dsw/index.html#/USER/Overview.dashboard):
**Algunos datos**
El «nivel de sangre» crece de temporada en temporada y alcanza su máximo en la Temporada 8.
Hay 68 armas, y la más peligrosa es el fuego de dragón siendo Daenerys Targaryen la heroína más sangrienta con +1000 víctimas.
Si excluimos el fuego de dragón como arma, entonces Cersei Lannister encabeza la lista de asesinos con 199 víctimas a lo largo de las 8 temporadas.
Lo que me ha sorprendido es que la Casa Stark ha matado a más soldados Lannister que viceversa: 34 Vs. 14.
**¿Cómo funciona todo esto?**
Puede montarse más o menos en media hora.
En primer lugar, se puede partir de un repositorio de plantilla como [objectscript-docker-template](https://openexchange.intersystems.com/package/objectscript-docker-template).
El módulo [csvgen](https://openexchange.intersystems.com/package/csvgen) te da la opción de procesar un CSV y generar la clase correspondiente en IRIS para luego importar los datos, y todo en un solo comando. En nuestro caso, lo que nos ha hecho falta es lo siguiente:
```
set fn="/irisdev/app/data/game_of_thrones_deaths_collecti.csv"
set status=##class(community.csvgen).Generate(fn,",",1,,.tResults)
```
Esta [línea](https://github.com/evshvarov/csvtest/blob/a489b7064773c4fe1ac212a1fda638505bf1603d/src/shvarov/csvtest.cls#L6) en el código.
Como resultado, obtenemos [la clase generada](https://github.com/evshvarov/got-analytics/blob/master/src/shvarov/GOT/Deaths.cls), donde como ves csvgen nos ha incluido los tipos de datos también:
```
Class shvarov.GOT.Deaths Extends %Library.Persistent [ Not Abstract, DdlAllowed, Not LegacyInstanceContext, ProcedureBlock ]
{
Property name As %Library.String(MAXLEN = 250) [ SqlColumnNumber = 2 ];
Property allegiance As %Library.String(MAXLEN = 250) [ SqlColumnNumber = 3 ];
Property season As %Library.Integer(MAXVAL = 2147483647, MINVAL = -2147483648) [ SqlColumnNumber = 4 ];
Property episode As %Library.Integer(MAXVAL = 2147483647, MINVAL = -2147483648) [ SqlColumnNumber = 5 ];
Property location As %Library.String(MAXLEN = 250) [ SqlColumnNumber = 6 ];
Property killer As %Library.String(MAXLEN = 250) [ SqlColumnNumber = 7 ];
Property killershouse As %Library.String(MAXLEN = 250) [ SqlColumnNumber = 8, SqlFieldName = killers_house ];
Property method As %Library.String(MAXLEN = 250) [ SqlColumnNumber = 9 ];
Property deathno As %Library.Integer(MAXVAL = 2147483647, MINVAL = -2147483648) [ SqlColumnNumber = 10, SqlFieldName = death_no ];
```
Lo único que hay que cambiar manualmente es una modificación para añadir [una comprobación después del %Save()](https://github.com/evshvarov/got-analytics/blob/0d0e1266c98e4799aae0865231d8fc9ddd0b9672/src/shvarov/GOT/Deaths.cls#L76) en el método Import() generado. Sin esa modificación, nunca sabrías el motivo por el que no has logrado importar datos.
A continuación, se han creado [la clase del cubo](https://github.com/evshvarov/got-analytics/blob/master/src/shvarov/GOT/BI/GOTDeaths.cls) — con el *Architect* — , [pivot queries](https://github.com/evshvarov/got-analytics/tree/master/src/dfi) — con el *Analyzer* — y un [cuadro de mando](https://github.com/evshvarov/got-analytics/blob/master/src/dfi/Overview.dashboard.xml) — con el *IRIS Analytics Portal* —.
Finalmente se visualiza el cuadro de mando con [DSW](https://openexchange.intersystems.com/package/DeepSeeWeb).
Se cocina todo junto en una imagen Docker utilizando este [Dockerfile](https://github.com/evshvarov/got-analytics/blob/master/Dockerfile), donde se utiliza ZPM para instalar:
* [csvgen](https://openexchange.intersystems.com/package/csvgen) - para generar CSV e importar datos.
* [isc-dev](https://openexchange.intersystems.com/package/ISC-DEV) - para exportar fácilmente artefactos de IRIS Analytics.
* [dsw](https://openexchange.intersystems.com/package/DeepSeeWeb) - para visualizar los datos.
Todo ello desarrollado en VSCode utilizando el [plugin de ObjectScript para VSCode](https://marketplace.visualstudio.com/items?itemName=daimor.vscode-objectscript) y una imagen Docker de InterSystems IRIS Community Edition 2020.2
Y además en este caso ha sido desplegado — con cada *push* — al *GCP Kubenertes Engine (GKE)* — utilizando el [Workflow de GitHub Actions](https://github.com/evshvarov/got-analytics/blob/master/.github/workflows/workflow.yaml). En particular, este cuadro de mando se re-despliega con cada *push* que se hace en la rama *master*.
Como resultado, podéis echarle un vistazo al [cuadro de mando interactivo](http://35.205.133.201:52773/dsw/index.html#!/d/Overview.dashboard?ns=IRISAPP) en funcionamiento.
La calidad del código ObjectScript se evalúa continuamente utilizando [ObjectScript Quality](https://openexchange.intersystems.com/package/CachéQuality) vía este [archivo de workflow](https://openexchange.intersystems.com/package/CachéQuality) y puede [ser examinado aquí](https://community.objectscriptquality.com/dashboard?id=intersystems_iris_community%2Fgot-analytics).
¡Cualquier colaboración es bienvenida!
P.D: había 68 personas en la lista de Arya 😎
Artículo
Guillaume Rongier · 28 nov, 2022
# 1. iris-dollar-list
[](https://pypi.org/project/iris-dollar-list/)
[](https://pypi.org/project/iris-dollar-list/)
[](https://github.com/grongierisc/iris-dollar-list/blob/main/LICENSE)
[](https://github.com/grongierisc/iris-dollar-list/actions)
Intérprete de $list para python llamado DollarList.
He hecho este intérprete porque:
* Quería usar $list en python
* Python Embebido no es compatible con $list
* La versión de API nativa no es compatible con $list embebido en $list
Este es un trabajo en desarrollo. Hasta ahora, solo es compatible con $list embebido en $list, int y string.
Trabajo en progreso: float, decimal, double
**Este módulo está disponible en Pypi:**
```sh
pip3 install iris-dollar-list
```
Es compatible con Python Embebido y API nativa.
## 1.1. Índice
- [1. iris-dollar-list](#1-iris-dollar-list)
- [1.1. Índice](#11-table-of-contents)
- [1.2. Uso](#12-usage)
- [1.3. funciones](#13-functions)
- [1.3.1. append](#131-append)
- [1.3.2. from_bytes](#132-from_bytes)
- [1.3.3. from_list](#133-from_list)
- [1.3.4. to_bytes](#134-to_bytes)
- [1.3.5. to_list](#135-to_list)
- [2. $list](#2-list)
- [2.1. ¿Qué es $list?](#21-what-is-list-)
- [2.2. ¿Cómo funciona?](#22-how-it-works-)
- [2.2.1. Header](#221-header)
- [2.2.1.1. Size](#2211-size)
- [2.2.1.2. Type](#2212-type)
- [2.2.2. Body](#222-body)
- [2.2.2.1. Ascii](#2221-ascii)
- [2.2.2.2. Unicode](#2222-unicode)
- [2.2.2.3. Int](#2223-int)
- [2.2.2.4. Negative Int](#2224-negative-int)
- [2.2.2.5. Float](#2225-float)
- [2.2.2.6. Negative Float](#2226-negative-float)
- [2.2.2.7. Double](#2227-double)
- [2.2.2.8. Compact Double](#2228-compact-double)
- [2.3. Development](#23-development)
## 1.2. Uso
Ejemplo:
```objectscript
set ^list = $lb("test",$lb(4))
```
Ejemplo de uso con API nativa:
```python
import iris
from iris_dollar_list import DollarList
conn = iris.connect("localhost", 57161,"IRISAPP", "SuperUser", "SYS")
iris_obj = iris.createIRIS(conn)
gl = iris_obj.get("^list")
my_list = DollarList.from_bytes(gl.encode('ascii'))
print(my_list.to_list())
# ['test', [4]]
```
Ejemplo de uso con Python Embebido:
```python
import iris
from iris_dollar_list import DollarList
gl = iris.gref("^list")
my_list = DollarList.from_bytes(gl[None].encode('ascii'))
print(my_list.to_list())
# ['test', [4]]
```
## 1.3. Funciones
### 1.3.1. append
Añade un elemento a la lista.
Este elemento puede ser:
* un *string*
* un int
* un DollarList
* un DollarItem
```python
my_list = DollarList()
my_list.append("one")
my_list.append(1)
my_list.append(DollarList.from_list(["list",2]))
my_list.append(DollarItem(dollar_type=1, value="item",
raw_value=b"item",
buffer=b'\x06\x01item'))
print(DollarList.from_bytes(my_list.to_bytes()))
# $lb("one",1,$lb("list",2),"item")
```
### 1.3.2. from_bytes
Crea un DollarList desde bytes.
```python
my_list = DollarList.from_bytes(b'\x05\x01one')
print(my_list)
# $lb("one")
```
### 1.3.3. from_list
Crea un DollarList desde una lista.
```python
print(DollarList.from_list(["list",2]))
# $lb("list",2)
```
### 1.3.4. to_bytes
Convierte DollarList en bytes.
```python
my_list = DollarList.from_list(["list",2])
print(my_list.to_bytes())
# b'\x06\x01list\x03\x04\x02'
```
### 1.3.5. to_list
Convierte DollarList en una lista.
```python
my_list = DollarList.from_bytes(b'\x05\x01one')
print(my_list.to_list())
# ['one']
```
# 2. $list
## 2.1. ¿Qué es $list?
$list es formato binario para almacenar datos. Se usa en InterSystems IRIS. Es un formato que es fácil de leer y escribir. También es sencillo de analizar.
Lo genial de $list es que no está limitado por el almacenamiento. También se usa para comunicación en el puerto SuperServer de IRIS.
## 2.2. ¿Cómo funciona?
$list es un formato binario que almacenta una lista de valores. Cada valor se almacena en un bloque. Cada bloque está compuesto de un *header* y un *body*. El *header* se compone de un tamaño y un tipo. El *body* se compone del valor.
### 2.2.1. Header
El header o cabecera se compone de un tamaño y un tipo.
#### 2.2.1.1. Size
El tamaño indica el tamaño del bloque. El tamaño se almacena en `N` bytes.
`N` está determinado por el número de bytes que son cero en los primeros bytes del *header*.
El tamaño se almacena en *little endian*.
#### 2.2.1.2. Type
El tipo es un byte que representa el tipo del valor.
El tipo se almacena justo después del tamaño.
Lista de tipos:
* ascii: 0x01
* unicode: 0x02
* int: 0x04
* negative int: 0x05
* float: 0x06
* negative float: 0x07
* double: 0x08
* compact double: 0x09
### 2.2.2. Body
El cuerpo se compone del valor.
Para analizar el cuerpo, hay que saber el tipo del valor.
#### 2.2.2.1. Ascii
Descodifica el valor como ascii.
Si la descodificación falla, considera el valor como una sub-list.
Si descodificar la sub-lista falla, considera el valor como un binario.
#### 2.2.2.2. Unicode
Descodifica el valor como unicode.
#### 2.2.2.3. Int
Analiza el valor como un número entero en little endian y sin signo.
#### 2.2.2.4. Negative Int
Analiza el valor como un número entero en little endian y con signo.
Artículo
Ricardo Paiva · 26 dic, 2022
Se han publicado en la Comunidad varios artículos muy útiles que muestran cómo usar Grafana con IRIS (o Cache/Ensemble) usando una base de datos intermedia.
Pero yo quería llegar directamente a las estructuras de IRIS. En particular, quería acceder a los datos del Cache History monitor, que es accessible a través de SQL, como se describe aquí:
https://community.intersystems.com/post/apm-using-cach%C3%A9-history-monitor
y no quería nada entre los datos y yo.Ya tenía consultas de clase que devuelven los datos que quiero, así que solo necesitaba embeberlos en una clase REST que devolviera JSON. No he incluido mi clase Grafana.MonitorData porque podría ser cualquier cosa, pero puedo hacerlo si la gente lo quiere.
Solo había dos puntos difíciles. Uno era asegurarse de que el tiempo local y el tiempo UTC cuadraban en cada punto. El otro era que a Grafana no le gustan los valores como .25 sin el cero inicial y da errores de javascript - “t.dataList.map no es a función”. Que es por lo que tengo la línea con $FN(tValue,,4).
He simplificado mi código de producción para aclarar los principios. Podría ponerlo en github, pero de tan simple que es, aquí os lo dejo.
Solución
Class Grafana.SYSHistory Extends %CSP.REST{XData UrlMap{<Routes> <Route Url="/" Method="GET" Call="testAvailability" Cors="true" /> <Route Url="/search" Method="POST" Call="metricFindQuery" Cors="true" /> <Route Url="/query" Method="POST" Call="query" Cors="true" /> </Routes>}ClassMethod testAvailability() As %Status{ write "ok" quit $$$OK}/// This method returns list of available metrics.ClassMethod metricFindQuery() As %Status{ do ##class(Grafana.MonitorData).GetSupportedMetrics(.metrics) w "[" set sub="" set firsttime=1 do { set sub=$o(metrics(sub)) quit:sub="" if firsttime=0 w "," set firsttime=0 w """",sub,"""" } while sub'=""write "]"quit $$$OK}/// Data format for Grafana - http://docs.grafana.org/plugins/developing/datasources/ClassMethod query() As %Status{ set obj = {}.%FromJSON(%request.Content) if obj="" { write "no object found" quit $$$OK } set iter=obj.targets.%GetIterator() set tMetrics=0 while iter.%GetNext(.key,.value) { set tMetrics=tMetrics+1 set tMetrics(tMetrics) = value.target } set from = obj.range.from set to = obj.range.to#define classname 1#define queryname 2set (className,queryName)=""//hard code the class and use 'NamedQuery' items so we don't allow any access to any data via any query...set className="Grafana.MonitorData"set queryName="SysMonHistorySummary"write "["for i=1:1:tMetrics {if i>1 w ","w "{""target"":"""_tMetrics(i)_""",""datapoints"":["do ..ExportJSON(className,queryName,from,to,tMetrics(i))write "]}"}write "]"quit $$$OK}/// The className and QueryName determing the query to be executed./// from and to are local time in %Date (a.k.k. $horolog) format./// The query must return a value for the metric. This code assumes the values are returned/// as Avg_Metric and RunDate, but you could change that
ClassMethod ExportJSON(className As %String, queryName As %String, from, to, pMetric As %String) As %Status{if className="" quit $$$OKif queryName="" quit $$$OKset rs=##class(%ResultSet).%New(className_":"_queryName)if rs="" quit $$$ERROR($$$QueryDoesNotExist,className_":"_queryName)// use this just for param infoset sc=$classmethod(className,queryName_"GetInfo",.colinfo,.paraminfo,.idinfo,.QHandle,0,.extinfo) //The request must contain data with names matching the parameters of the query.//Convert date and time parameters from strings to $hset from=$e(from,1,19)set to=$e(to,1,19)set RunDateUTCFromH=$zdth(from,3)set RunDateFromH=$zdth(RunDateUTCFromH,-3)set RunDateUTCToH=$zdth(to,3)set RunDateToH=$zdth(RunDateUTCToH,-3)set tSc=rs.Execute(RunDateFromH,RunDateToH,"live",pMetric) //param(1),param(2))if $$$ISERR(tSc) quit tScset rowcnt=0while rs.Next() {set rowcnt=rowcnt+1if rowcnt>1 write ","write "["set tRunDate=rs.Data("RunDate")set tUtcRunDate=$zdt(tRunDate,-3)set tValue=rs.Data("Avg_Metric")set tPosixTime=##class(%Library.PosixTime).OdbcToLogical($zdt(tUtcRunDate,3,3))set tUnixTime=##class(%Library.PosixTime).LogicalToUnixTime(tPosixTime)_"000"write $fn(tValue,,4),",",tUnixTimewrite "]"}quit $$$OK}}
Este artículo ha sido etiquetado como "Mejores prácticas" ("Best practices").
Los artículos con la etiqueta "Mejores prácticas" incluyen recomendaciones sobre cómo desarrollar, probar, implementar y administrar mejor las soluciones de InterSystems.
Artículo
Muhammad Waseem · 15 jul, 2022
¡Hola Comunidad!
En este artículo voy a explicar cómo acceder a la información y a las tablas del dashboard (cuadro de mando) del sistema del Portal de Administración mediante el uso de Python Embebido.
Cómo acceder al dashboard del sistema del Portal de Administración
Estos son los pasos para acceder a la información del dashboard del sistema:
Paso 1: cambiar el namespace a %SYS
Paso 2: importar el módulo iris python y crear una instancia de la clase "SYS.Stats.Dashboard"
Paso 3: acceder a las propiedades de la clase instanciada
¡Empezamos!
Paso 1: Cambiar el namespace a %SYS
Para obtener la información del dashboard del sistema, debemos acceder a la clase "SYS.Stats.Dashboard" desde el namespace %SYS.Si no está en el namespace %SYS, entonces tenemos que cambiar el namespace.
Crea la clase Embedded.Utils para obtener o establecer el namespace:
///Esta clase se usará para obtener o establecer el espacio de nombres
Class Embedded.Utils
{
//Obtener espacio de nombres
ClassMethod GetNameSpace() As %Status
{
Return $namespace
}
//Establecer espacio de nombres
ClassMethod SetNameSpace(pNameSpace) As %Status
{
zn pNameSpace
Return $namespace
}
}
Desde el código de Python, escribe el siguiente script para cambiar el namespace:
#Importar biblioteca de iris
import iris
#Establezca Namespoace en %SYS si el espacio de nombres actual no es %SYS
if iris.cls("Embedded.Utils").GetNameSpace() != "%SYS":
iris.cls("Embedded.Utils").SetNameSpace("%SYS")
Paso 2: Importar el módulo iris python y crear una instancia
Ahora podemos acceder a las propiedades de "SYS.Stats.Dashboard" llamando al método de clase 'Sample':
#Importar biblioteca de iris
import iris
#Establecer Namespoace en %SYS si el espacio de nombres actual no es %SYS
if iris.cls("Embedded.Utils").GetNameSpace() != "%SYS":
iris.cls("Embedded.Utils").SetNameSpace("%SYS")
#Se puede crear una instancia de una clase llamando al método de clase 'Muestra', y luego se puede acceder al valor actual de cada propiedad.
ref = iris.cls("SYS.Stats.Dashboard").Sample()
Paso 3: Acceder a las propiedades de la clase instanciada
Esta clase contiene las siguientes propiedades, a las que se puede acceder mediante el siguiente código:
#Importar biblioteca de iris
import iris
#Establezca Namespoace en %SYS si el espacio de nombres actual no es %SYS
if iris.cls("Embedded.Utils").GetNameSpace() != "%SYS":
iris.cls("Embedded.Utils").SetNameSpace("%SYS")
#Se puede crear una instancia de una clase llamando al método de clase 'Muestra', y luego se puede acceder al valor actual de cada propiedad.
ref = iris.cls("SYS.Stats.Dashboard").Sample()
#Mostrar errores de aplicación
print(ref.ApplicationErrors)
#mostrar sesiones de CSP
print(ref.CSPSessions)
Para obtener más detalles, consulta la aplicación iris-python-apps en Open Exchange
Mostar los datos de la tabla Caché en una página web con la ayuda de jquery datatable
Estos son los pasos para mostrar la tabla Security.Users en una página web
Paso 1: Cambiar el namespace a %SYS
Paso 2: Importar el módulo iris python y usar la función iris.sql.exec() para ejecutar una sentencia SQL SELECT para obtener un resultset
Paso 3: Obtener el dataframe llamando a la función resultset dataframe
Paso 4: Obtener datos JSON de encabezado y columnas del dataframe y pasarlos a la web
Paso 5: Mostrar datos en jquery datatable
¡Empezamos!
Paso 1: Cambiar el namespace a %SYS
La misma clase Embedded.Utils anterior se puede usar para obtener o establecer el namespace
#Importar biblioteca de iris
import iris
#Establezca Namespoace en %SYS si el espacio de nombres actual no es %SYS
if iris.cls("Embedded.Utils").GetNameSpace() != "%SYS":
iris.cls("Embedded.Utils").SetNameSpace("%SYS")
Paso 2: Importar el módulo iris python y usar la función iris.sql.exec() para ejecutar una sentencia SQL SELECT para obtener un resultset
Después de importar el módulo iris, usaremos iris.sql.exec() para ejecutar una sentencia SQL SELECT y obtener un resultset
import iris
statement = '''SELECT
ID, AccountNeverExpires, AutheEnabled, ChangePassword, CreateDateTime AS DateCreated, Enabled, ExpirationDate, Flags, Name
FROM Security.Users'''
#Llamar a la clase python incrustada iris.sql.exec para obtener el conjunto de resultados
resultSet = iris.sql.exec(mySql)
Paso 3: Obtener el dataframe llamando a la función resultset dataframe
#Obtenga el marco de datos llamando a la función de marco de datos del conjunto de resultados
dataframe = statement.dataframe()
Paso 4: Obtener datos JSON de encabezado y columnas del dataframe y pasarlos a la web
#Convierta y envíe datos a Json utilizando el método de marco de datos to_json y la función de carga json
my_data=json.loads(dataframe.to_json(orient="split"))["data"]
#Obtener detalles de las columnas
my_cols=[{"title": str(col)} for col in json.loads(df.to_json(orient="split"))["columns"]]
#renderice html pasando las variables my_data y my_cols que se utilizarán para generar la tabla de datos
return render_template('tablesdata.html', my_data = my_data, my_cols = my_cols)
Paso 5: Mostrar datos en jquery datatableDefinir la tabla con id "myTable" y usar javascript para completar los datos de encabezado y columnas pasados
<table id="myTable" class="table table-bordered table-striped">
</table>
<script>
$(document).ready(function() {
// analizar los datos a la variable local pasada desde el archivo app.py
let my_data = JSON.parse('{{ my_data | tojson }}');
let my_cols = JSON.parse('{{ my_cols | tojson }}');
$('#myTable').DataTable( {
"data": my_data,
"columns": my_cols,"} );
} );
Eso es todo. Para obtener más detalles, consulta la aplicación iris-python-apps en Open Exchange.
Echa un vistazo a esta documentación (en inglés): Resumen de Python Embebido.
Este artículo ha sido etiquetado como "Mejores prácticas" ("Best practices").
Los artículos con la etiqueta "Mejores prácticas" incluyen recomendaciones sobre cómo desarrollar, probar, implementar y administrar mejor las soluciones de InterSystems.
Artículo
Ricardo Paiva · 22 sep, 2022
¡Hola Comunidad!
Durante años he trabajado en muchos proyectos diferentes y he podido encontrar muchos datos interesantes.
Pero la mayoría de las veces el conjunto de datos con el que trabajaba era de los clientes. Cuando hace un par de años empecé a participar en los Concursos de Programación de InterSystems, comencé a buscar conjuntos de datos web específicos.
Yo mismo he ido seleccionando algunos datos, pero he pensado: "¿Este conjunto de datos es suficiente para ayudar a otras personas?"
Y discutiendo estas ideas con @José.Pereira, decidimos enfocar este tema usando *una perspectiva diferente*.
Pensamos en ofrecer una variedad de conjuntos de datos que provengan de dos famosas fuentes de datos. De este modo, podemos facilitar que los usuarios encuentren e instalen el conjunto de datos que quieran, de forma rápida y sencilla.
## Socrata
La API de datos abiertos de Socrata permite acceder mediante programación a una gran cantidad de recursos de datos abiertos de gobiernos, organizaciones sin ánimo de lucro y ONGs de todo el mundo.
Para esta versión inicial, utilizamos las API de Socrata para buscar y descargar un conjunto de datos específicos.
Abre la herramienta de la API de tu preferencia, como [Postman](https://www.postman.com/), [Hoppscotch](https://hoppscotch.io/)
```
GET> https://api.us.socrata.com/api/catalog/v1?only=dataset&q=healthcare
```
Este *EndPoint* devolverá todos los conjuntos de datos relacionados con la atención sanitaria, como en la siguiente imagen: 
Ahora, consigue el ID. En este caso el ID es: "n9tp-i3k3"
Ve al terminal
```
IRISAPP>set api = ##class(dc.dataset.importer.service.socrata.SocrataApi).%New()
IRISAPP>do api.InstallDataset({"datasetId": "n9tp-i3k3", "verbose":true})
Compilation started on 01/07/2022 01:01:28 with qualifiers 'cuk'
Compiling class dc.dataset.imported.DsCommunityHealthcareCenters
Compiling table dc_dataset_imported.DsCommunityHealthcareCenters
Compiling routine dc.dataset.imported.DsCommunityHealthcareCenters.1
Compilation finished successfully in 0.108s.
Class name: dc.dataset.imported.DsCommunityHealthcareCenters
Header: Name VARCHAR(250),Description VARCHAR(250),Location VARCHAR(250),Phone_Number VARCHAR(250),geom VARCHAR(250)
Records imported: 26
```
Después del comando anterior, tu conjunto de datos estará listo para utilizarse!

## Kaggle
Kaggle, una filial de Google LLC, es una comunidad en línea de científicos de datos y profesionales del Machine Learning. Kaggle permite que los usuarios encuentren y publiquen conjuntos de datos, exploren y construyan modelos en un entorno de ciencia de datos basados en la web, trabajen con otros científicos de datos e ingenieros de Machine Learning y participen en competiciones para resolver retos de la ciencia de datos.
En junio de 2017 Kaggle anunció que había superado el millón de usuarios registrados, o Kagglers, y desde 2021 cuenta con más de **8 millones de usuarios registrados**. La comunidad abarca **194 países**. Se trata de una comunidad diversa, que va desde los que acaban de empezar hasta muchos de los investigadores más conocidos del mundo.
Esto es lo que yo llamo una gran comunidad, ¡¿cierto?!
Para utilizar los conjuntos de datos de Kaggle, hay que registrarse en [su página web](https://www.kaggle.com/). Después, hay que crear un token de API para utilizar la API de Kaggle.

Ahora, igual que con Socrata, puedes utilizar la API para buscar y descargar el conjunto de datos.
```
GET> https://www.kaggle.com/api/v1/datasets/list?search=appointments
```

Ahora, obtén el valor de la referencia. En este caso, la referencia es: "joniarroba/noshowappointments"
Los parámetros de abajo "_your-username_" y "_your-password_" son los parámetros que proporciona Kaggle cuando creas el token de API.
```
IRISAPP>Set crendtials = ##class(dc.dataset.importer.service.CredentialsService).%New()
IRISAPP>Do crendtials.SaveCredentials("kaggle", "", "")
IRISAPP>Set api = ##class(dc.dataset.importer.service.kaggle.KaggleApi).%New()
IRISAPP>Do api.InstallDataset({"datasetId":"joniarroba/noshowappointments", "credentials":"kaggle", "verbose":true})
Class name: dc.dataset.imported.DsNoshowappointments
Header: PatientId INTEGER,AppointmentID INTEGER,Gender VARCHAR(250),ScheduledDay DATE,AppointmentDay DATE,Age INTEGER,Neighbourhood VARCHAR(250),Scholarship INTEGER,Hipertension INTEGER,Diabetes INTEGER,Alcoholism INTEGER,Handcap INTEGER,SMS_received INTEGER,No-show VARCHAR(250)
Records imported: 259
```
Después del comando anterior, tu conjunto de datos estará listo para utilizarse!

## Interfaz gráfica de usuario
Para facilitar las cosas, ofrecemos una Interfaz gráfica de usuario para instalar el conjunto de datos. Pero esto es algo que nos gustaría discutir en nuestro próximo artículo. Mientras tanto, a continuación puedes ver un adelanto mientras pulimos algunas cosas antes del lanzamiento oficial:


## Video de demostración
¿Cómo es el funcionamiento para descargar un conjunto de datos más grande? +¡¿Más de 400,000 registros no son suficientes?! ¡¿Qué tal 1 MILLÓN DE REGISTROS?! ¡Vamos a verlo!
Voting
Artículo
Ricardo Paiva · 7 jul, 2022
Si tuvieras la oportunidad de cambiar algo en el Visualizador de Mensajes de Interoperabilidad en IRIS, ¿qué harías?
Después de publicar el artículo Panel de Control "IRIS History Monitor", recibí algunos comentarios muy interesantes y varias peticiones. Una de ellas fue un Visualizador de Mensajes mejorado.
Si aún no lo has hecho, echa un vistazo al proyecto: merece la pena que le dediques un rato, y además ganó el 3er premio (Bronce) a Los mejores desarrolladores y aplicaciones de InterSystems Open Exchange en 2019.
Empecé a pensar algunas ideas sobre lo que me gustaría incluir en el "nuevo" Visualizador de Mensajes pero ¿cómo podría mostrar estos recursos de la forma más rápida y sencilla?
Bueno, antes de nada. Por lo general, se empieza configurando una producción de interoperabilidad, y después se exporta y se implementa en el sistema de destino, como se indica en la documentación. Este es un proceso que realmente no me gusta. En realidad, no es que haya algo malo en él. Solo que he idealizado hacer todo mediante el código.
Espero que cada vez que alguien ejecute este tipo de proyectos, empiece de esta manera:
$ docker-compose build
$ docker-compose up -d
¡¡¡Y eso es todo!!!
Con estos sencillos pasos en mente, comencé a buscar en la comunidad de InterSystems y encontré algunos consejos. En una de las publicaciones surgió la pregunta que me estaba haciendo: ¿Cómo crear producciones desde una rutina?
En esa publicación, @Eduard.Lebedyuk respondió, mostrando cómo crear una producción mediante el uso de un código.
"Para crear la clase de producción de forma automática es necesario:
Crear el objeto %Dictionary.ClassDefinition para tu producción de prueba
Crear el objeto Ens.Config.Production
Crear %Dictionary.XDataDefinition
Serializar (2) en (3)
Insertar XData (3) en (1)
Guardar y compilar (1)"
También encontré un comentario de @jennifer.ames:
"Una de las prácticas que recomendamos on frecuencia es crear hacia atrás. Crea las business operations primero, después los business processes, después los business services…"
Así que, ¡hagámoslo!
Solicitudes, Business operations y Business services
La clase diashenrique.messageviewer.util.InstallerProduction.cls es, como su nombre lo indica, la clase que se encarga de instalar nuestra producción. El manifiesto del instalador invoca el ClassMethod **Install** desde esa clase:
/// Helper to install a production to display capabilities of the enhanced viewer
ClassMethod Install() As %Status
{
Set sc = $$$OK
Try {
Set sc = $$$ADDSC(sc,..InstallProduction()) quit:$$$ISERR(sc)
Set sc = $$$ADDSC(sc,..GenerateMessages()) quit:$$$ISERR(sc)
Set sc = $$$ADDSC(sc,..GenerateUsingEnsDirector()) quit:$$$ISERR(sc)
}
Catch (err) {
Set sc = $$$ADDSC(sc,err.AsStatus())
}
Return sc
}
El ClassMethod InstallProduction reúne la estructura principal que permite crear una producción mediante la creación de:
una solicitud
una business operation
un business service
una producción de interoperabilidad
Dado que la idea es crear una producción de interoperabilidad mediante código, vamos al modo de codificación completa para crear todas las clases para la solicitud, la business operation y los business services. Al hacer eso, haremos un amplio uso de algunos paquetes de librerías de InterSystems:
%Dictionary.ClassDefinition
%Dictionary.PropertyDefinition
%Dictionary.XDataDefinition
%Dictionary.MethodDefinition
%Dictionary.ParameterDefinition
El ClassMethod InstallProduction crea dos clases que se extienden desde Ens.Request, por medio de las siguientes líneas:
Set sc = $$$ADDSC(sc,..CreateRequest("diashenrique.messageviewer.Message.SimpleRequest","Message")) quit:$$$ISERR(sc)
Set sc = $$$ADDSC(sc,..CreateRequest("diashenrique.messageviewer.Message.AnotherRequest","Something")) quit:$$$ISERR(sc)
ClassMethod CreateRequest(classname As %String, prop As %String) As %Status [ Private ]
{
New $Namespace
Set $Namespace = ..#NAMESPACE
Set sc = $$$OK
Try {
Set class = ##class(%Dictionary.ClassDefinition).%New(classname)
Set class.GeneratedBy = $ClassName()
Set class.Super = "Ens.Request"
Set class.ProcedureBlock = 1
Set class.Inheritance = "left"
Set sc = $$$ADDSC(sc,class.%Save())
#; create adapter
Set property = ##class(%Dictionary.PropertyDefinition).%New(classname)
Set property.Name = prop
Set property.Type = "%String"
Set sc = $$$ADDSC(sc,property.%Save())
Set sc = $$$ADDSC(sc,$System.OBJ.Compile(classname,"fck-dv"))
}
Catch (err) {
Set sc = $$$ADDSC(sc,err.AsStatus())
}
Return sc
}
Ahora vamos a crear la clase para una business operation que se extiende desde Ens.BusinessOperation:
Set sc = $$$ADDSC(sc,..CreateOperation()) quit:$$$ISERR(sc)
Además de crear la clase, creamos MessageMap y el método Consume:
ClassMethod CreateOperation() As %Status [ Private ]
{
New $Namespace
Set $Namespace = ..#NAMESPACE
Set sc = $$$OK
Try {
Set classname = "diashenrique.messageviewer.Operation.Consumer"
Set class = ##class(%Dictionary.ClassDefinition).%New(classname)
Set class.GeneratedBy = $ClassName()
Set class.Super = "Ens.BusinessOperation"
Set class.ProcedureBlock = 1
Set class.Inheritance = "left"
Set xdata = ##class(%Dictionary.XDataDefinition).%New()
Set xdata.Name = "MessageMap"
Set xdata.XMLNamespace = "http://www.intersystems.com/urlmap"
Do xdata.Data.WriteLine("<MapItems>")
Do xdata.Data.WriteLine("<MapItem MessageType=""diashenrique.messageviewer.Message.SimpleRequest"">")
Do xdata.Data.WriteLine("<Method>Consume</Method>")
Do xdata.Data.WriteLine("</MapItem>")
Do xdata.Data.WriteLine("<MapItem MessageType=""diashenrique.messageviewer.Message.AnotherRequest"">")
Do xdata.Data.WriteLine("<Method>Consume</Method>")
Do xdata.Data.WriteLine("</MapItem>")
Do xdata.Data.WriteLine("</MapItems>")
Do class.XDatas.Insert(xdata)
Set sc = $$$ADDSC(sc,class.%Save())
Set method = ##class(%Dictionary.MethodDefinition).%New(classname)
Set method.Name = "Consume"
Set method.ClassMethod = 0
Set method.ReturnType = "%Status"
Set method.FormalSpec = "input:diashenrique.messageviewer.Message.SimpleRequest,&output:Ens.Response"
Set stream = ##class(%Stream.TmpCharacter).%New()
Do stream.WriteLine(" set sc = $$$OK")
Do stream.WriteLine(" $$$TRACE(input.Message)")
Do stream.WriteLine(" return sc")
Set method.Implementation = stream
Set sc = $$$ADDSC(sc,method.%Save())
Set sc = $$$ADDSC(sc,$System.OBJ.Compile(classname,"fck-dv"))
}
Catch (err) {
Set sc = $$$ADDSC(sc,err.AsStatus())
}
Return sc
}
En el último paso antes de realmente crear la producción de interoperabilidad, vamos a crear la clase responsable del business service:
Set sc = $$$ADDSC(sc,..CreateRESTService()) quit:$$$ISERR(sc)
Esta clase tiene UrlMap y Routes para recibir solicitudes Http.
ClassMethod CreateRESTService() As %Status [ Private ]
{
New $Namespace
Set $Namespace = ..#NAMESPACE
Set sc = $$$OK
Try {
Set classname = "diashenrique.messageviewer.Service.REST"
Set class = ##class(%Dictionary.ClassDefinition).%New(classname)
Set class.GeneratedBy = $ClassName()
Set class.Super = "EnsLib.REST.Service, Ens.BusinessService"
Set class.ProcedureBlock = 1
Set class.Inheritance = "left"
Set xdata = ##class(%Dictionary.XDataDefinition).%New()
Set xdata.Name = "UrlMap"
Set xdata.XMLNamespace = "http://www.intersystems.com/urlmap"
Do xdata.Data.WriteLine("<Routes>")
Do xdata.Data.WriteLine("<Route Url=""/send/message"" Method=""POST"" Call=""SendMessage""/>")
Do xdata.Data.WriteLine("<Route Url=""/send/something"" Method=""POST"" Call=""SendSomething""/>")
Do xdata.Data.WriteLine("</Routes>")
Do class.XDatas.Insert(xdata)
Set sc = $$$ADDSC(sc,class.%Save())
#; create adapter
Set adapter = ##class(%Dictionary.ParameterDefinition).%New(classname)
Set class.GeneratedBy = $ClassName()
Set adapter.Name = "ADAPTER"
Set adapter.SequenceNumber = 1
Set adapter.Default = "EnsLib.HTTP.InboundAdapter"
Set sc = $$$ADDSC(sc,adapter.%Save())
#; add prefix
Set prefix = ##class(%Dictionary.ParameterDefinition).%New(classname)
Set prefix.Name = "EnsServicePrefix"
Set prefix.SequenceNumber = 2
Set prefix.Default = "|demoiris"
Set sc = $$$ADDSC(sc,prefix.%Save())
Set method = ##class(%Dictionary.MethodDefinition).%New(classname)
Set method.Name = "SendMessage"
Set method.ClassMethod = 0
Set method.ReturnType = "%Status"
Set method.FormalSpec = "input:%Library.AbstractStream,&output:%Stream.Object"
Set stream = ##class(%Stream.TmpCharacter).%New()
Do stream.WriteLine(" set sc = $$$OK")
Do stream.WriteLine(" set request = ##class(diashenrique.messageviewer.Message.SimpleRequest).%New()")
Do stream.WriteLine(" set data = {}.%FromJSON(input)")
Do stream.WriteLine(" set request.Message = data.Message")
Do stream.WriteLine(" set sc = $$$ADDSC(sc,..SendRequestSync(""diashenrique.messageviewer.Operation.Consumer"",request,.response))")
Do stream.WriteLine(" return sc")
Set method.Implementation = stream
Set sc = $$$ADDSC(sc,method.%Save())
Set method = ##class(%Dictionary.MethodDefinition).%New(classname)
Set method.Name = "SendSomething"
Set method.ClassMethod = 0
Set method.ReturnType = "%Status"
Set method.FormalSpec = "input:%Library.AbstractStream,&output:%Stream.Object"
Set stream = ##class(%Stream.TmpCharacter).%New()
Do stream.WriteLine(" set sc = $$$OK")
Do stream.WriteLine(" set request = ##class(diashenrique.messageviewer.Message.AnotherRequest).%New()")
Do stream.WriteLine(" set data = {}.%FromJSON(input)")
Do stream.WriteLine(" set request.Something = data.Something")
Do stream.WriteLine(" set sc = $$$ADDSC(sc,..SendRequestSync(""diashenrique.messageviewer.Operation.Consumer"",request,.response))")
Do stream.WriteLine(" return sc")
Set method.Implementation = stream
Set sc = $$$ADDSC(sc,method.%Save())
Set sc = $$$ADDSC(sc,$System.OBJ.Compile(classname,"fck-dv"))
}
Catch (err) {
Set sc = $$$ADDSC(sc,err.AsStatus())
}
Return sc
}
Usando Visual Studio Code
Crear las clases mediante el paquete %Dictionary puede ser difícil, y difícil de leer también, pero es práctico. Para mostrar un procedimiento un poco más sencillo con una mejor comprensión del código, también crearé nuevas clases de solicitud, business service y business operations por medio de Visual Studio Code:
diashenrique.messageviewer.Message.SimpleMessage.cls
diashenrique.messageviewer.Operation.ConsumeMessageClass.cls
diashenrique.messageviewer.Service.SendMessage.cls
Class diashenrique.messageviewer.Message.SimpleMessage Extends Ens.Request [ Inheritance = left, ProcedureBlock ]
{
Property ClassMessage As %String;
}
Class diashenrique.messageviewer.Operation.ConsumeMessageClass Extends Ens.BusinessOperation [ Inheritance = left, ProcedureBlock ]
{
Method Consume(input As diashenrique.messageviewer.Message.SimpleMessage, ByRef output As Ens.Response) As %Status
{
Set sc = $$$OK
$$$TRACE(pRequest.ClassMessage)
Return sc
}
XData MessageMap [ XMLNamespace = "http://www.intersystems.com/urlmap" ]
{
<MapItems>
<MapItem MessageType="diashenrique.messageviewer.Message.SimpleMessage">
<Method>Consume</Method>
</MapItem>
</MapItems>
}
}
Class diashenrique.messageviewer.Service.SendMessage Extends Ens.BusinessService [ ProcedureBlock ]
{
Method OnProcessInput(input As %Library.AbstractStream, ByRef output As %Stream.Object) As %Status
{
Set tSC = $$$OK
// Create the request message
Set request = ##class(diashenrique.messageviewer.Message.SimpleMessage).%New()
// Place a value in the request message property
Set request.ClassMessage = input
// Make a synchronous call to the business process and use the response message as our response
Set tSC = ..SendRequestSync("diashenrique.messageviewer.Operation.ConsumeMessageClass",request,.output)
Quit tSC
}
}
Desde el punto de vista de la legibilidad del código, la diferencia es enorme.
Creando la Producción de Interoperabilidad
Vamos a terminar la creación de nuestra producción de interoperabilidad. Para ello, crearemos una clase de producción, y después la asociaremos con las Business Operation y Business Services.
Set sc = $$$ADDSC(sc,..CreateProduction()) quit:$$$ISERR(sc)
ClassMethod CreateProduction(purge As %Boolean = 0) As %Status [ Private ]
{
New $Namespace
Set $Namespace = ..#NAMESPACE
Set sc = $$$OK
Try {
#; create new production
Set class = ##class(%Dictionary.ClassDefinition).%New(..#PRODUCTION)
Set class.ProcedureBlock = 1
Set class.Super = "Ens.Production"
Set class.GeneratedBy = $ClassName()
Set xdata = ##class(%Dictionary.XDataDefinition).%New()
Set xdata.Name = "ProductionDefinition"
Do xdata.Data.Write("<Production Name="""_..#PRODUCTION_""" LogGeneralTraceEvents=""true""></Production>")
Do class.XDatas.Insert(xdata)
Set sc = $$$ADDSC(sc,class.%Save())
Set sc = $$$ADDSC(sc,$System.OBJ.Compile(..#PRODUCTION,"fck-dv"))
Set production = ##class(Ens.Config.Production).%OpenId(..#PRODUCTION)
Set item = ##class(Ens.Config.Item).%New()
Set item.ClassName = "diashenrique.messageviewer.Service.REST"
Do production.Items.Insert(item)
Set sc = $$$ADDSC(sc,production.%Save())
Set item = ##class(Ens.Config.Item).%New()
Set item.ClassName = "diashenrique.messageviewer.Operation.Consumer"
Do production.Items.Insert(item)
Set sc = $$$ADDSC(sc,production.%Save())
Set item = ##class(Ens.Config.Item).%New()
Set item.ClassName = "diashenrique.messageviewer.Service.SendMessage"
Do production.Items.Insert(item)
Set sc = $$$ADDSC(sc,production.%Save())
Set item = ##class(Ens.Config.Item).%New()
Set item.ClassName = "diashenrique.messageviewer.Operation.ConsumeMessageClass"
Do production.Items.Insert(item)
Set sc = $$$ADDSC(sc,production.%Save())
}
Catch (err) {
Set sc = $$$ADDSC(sc,err.AsStatus())
}
Return sc
}
Utilizamos la clase Ens.Config.Item para asociar la clase de producción con las clases Business Operation y Business Service. Puedes hacer esto tanto si creaste tu clase usando el paquete %Dictionary como con VS Code, Studio o Atelier.
En cualquier caso, ¡lo conseguimos! Creamos una producción de interoperabilidad por medio de código.
Pero recuerda el propósito original de todo este código: crear una producción y mensajes para mostrar las funcionalidades del Visualizador de Mensajes mejorado. Usando los métodos de clase siguientes, ejecutaremos los dos business services y generaremos los mensajes.
Generando mensajes con %Net.HttpRequest:
ClassMethod GenerateMessages() As %Status [ Private ]
{
New $Namespace
Set $Namespace = ..#NAMESPACE
Set sc = $$$OK
Try {
Set action(0) = "/demoiris/send/message"
Set action(1) = "/demoiris/send/something"
For i=1:1:..#LIMIT {
Set content = { }
Set content.Message = "Hi, I'm just a random message named "_$Random(30000)
Set content.Something = "Hi, I'm just a random something named "_$Random(30000)
Set httprequest = ##class(%Net.HttpRequest).%New()
Set httprequest.SSLCheckServerIdentity = 0
Set httprequest.SSLConfiguration = ""
Set httprequest.Https = 0
Set httprequest.Server = "localhost"
Set httprequest.Port = 9980
Set serverUrl = action($Random(2))
Do httprequest.EntityBody.Write(content.%ToJSON())
Set sc = httprequest.Post(serverUrl)
Quit:$$$ISERR(sc)
}
}
Catch (err) {
Set sc = $$$ADDSC(sc,err.AsStatus())
}
Return sc
}
Generando mensajes con EnsDirector:
ClassMethod GenerateUsingEnsDirector() As %Status [ Private ]
{
New $Namespace
Set $Namespace = ..#NAMESPACE
Set sc = $$$OK
Try {
For i=1:1:..#LIMIT {
Set tSC = ##class(Ens.Director).CreateBusinessService("diashenrique.messageviewer.Service.SendMessage",.tService)
Set message = "Message Generated By CreateBusinessService "_$Random(1000)
Set tSC = tService.ProcessInput(message,.output)
Quit:$$$ISERR(sc)
}
}
Catch (err) {
Set sc = $$$ADDSC(sc,err.AsStatus())
}
Return sc
}
}
Eso todo lo que se necesita para el código. Encontrarás el proyecto completo en https://github.com/diashenrique/iris-message-viewer.
Ejecutando el proyecto
Ahora veamos el proyecto en marcha.
Primero, git clone o git pull el repositorio en cualquier directorio local:
git clone https://github.com/diashenrique/iris-message-viewer.git
Después, abre el terminal en este directorio y ejecuta:
docker-compose build
Finalmente, ejecuta el contenedor IRIS con tu proyecto:
docker-compose up -d
Ahora accederemos al Portal de Administración por medio de la página http://localhost:52773/csp/sys/UtilHome.csp. Deberás ver nuestro namespace de interoperabilidad MSGVIEWER, como en esta imagen:
Esta es nuestra pequeña producción, con dos business services y dos business operations:
Tenemos muchos mensajes:
Con todo en marcha en nuestro Visualizador de Mensajes personalizado, vamos a echar un vistazo a algunas de sus funcionalidades.
El Visualizador de Mensajes mejorado
Ten en cuenta que solo se mostrarán los namespaces que estén habilitados para las producciones de interoperabilidad.
http://localhost:52773/csp/msgviewer/messageviewer.csp
El Visualizador de Mensajes mejorado incorpora funcionalidades y la flexibilidad que permite crear diferentes filtros, agrupar las columnas en n niveles, exportar a Excel y muchas cosas más.
Se pueden utilizar diferentes filtros para conseguir los resultados que se necesitan. También se pueden ordenar las columnas de forma múltiple pulsando la tecla Mayúsculas y haciendo clic en el encabezado de las columnas. ¡Incluso se puede exportar la tabla de datos a Excel!
Además, se pueden crear filtros complejos con la opción Generador de filtros.
Se pueden agrupar los datos con cualquier columna disponible, agrupando la información mediante los n niveles que se desee. De forma predeterminada, el grupo se establece a partir del campo Fecha de creación (Date Created).
Y hay una funcionalidad que permite seleccionar columnas. La siguiente página tiene todas las columnas de Ens.MessageHeader, y muestra solo las columnas predeterminadas en la visualización inicial. Pero se pueden elegir las otras columnas mediante el botón "Seleccionador de columnas" (Column Chooser).
Se pueden contraer o expandir todos los grupos con un solo clic.
La información del campo SessionID incluye un enlace a la función Visual Trace.
Se pueden reenviar mensajes si se necesita. Tan solo hay que seleccionar los mensajes que se necesitan y hacer clic para volver a enviarlos. Esta función utiliza el siguiente método de clase:
##class(Ens.MessageHeader).ResendDuplicatedMessage(id)
Por último, como se mencionó, se puede exportar la tabla de datos a Excel:
El resultado en Excel mostrará el mismo formato, contenido y grupo definidos en las páginas del servidor de caché (CSP).
P.D.: Quiero dar un agradecimiento especial a @Renan.Lourenco, que me ayudó mucho en este viaje.