Skip to content

Gramplets.es

Doug Blank edited this page Oct 5, 2026 · 3 revisions

🌐 English · Deutsch · Français · 简体中文

Gramplets y la tienda de complementos

Los Gramplets son el sistema de complementos de Gramps Connect: pequeños programas de Python, asociados a una vista (Persona, Familia o cualquier otro tipo de objeto, o «Todas»), que consultan el árbol y muestran un resultado: una tabla, un diagrama, texto simple o HTML. Lo que los hace poco habituales es dónde se ejecutan: cada uno es Python real, que se ejecuta directamente en la pestaña del navegador con Pyodide (CPython compilado a WebAssembly), sobre wheels compiladas localmente del propio modelo de datos gramps.gen.lib de Gramps. No se instala nada en su equipo y nada se ejecuta en un servidor; véase Arquitectura.

Eso también significa que todos los Gramplets son editables. Abra uno desde el editor de Gramplets de la aplicación y estará viendo (y podrá cambiar) su código fuente real, no una caja negra detrás de un panel de configuración. Cambie el aspecto de un diagrama, ajuste lo que busca una consulta o escriba uno nuevo desde cero, todo sin salir de la aplicación.

Explorar e instalar

La tienda de Gramplets es un catálogo navegable dentro de la aplicación. Instale, actualice o quite cualquier entrada con un clic. Se publica de forma independiente de las versiones de Gramps Connect: el catálogo es contenido estático (gramplet-store/ en el repositorio) que la aplicación obtiene en tiempo de ejecución, no algo incluido en su compilación, así que pueden aparecer Gramplets nuevos en la tienda sin que nadie tenga que actualizar el propio Gramps Connect.

Entre los ejemplos que ya hay en el catálogo están:

Gramplet Qué hace
Age-at-Death Histogram Histograma interactivo de plotly de la edad al fallecer en todo el árbol (o en la lista filtrada actual)
All Relationships Cálculos de relaciones en todo el árbol
Attributes / Backlinks / Children / Citations / Coordinates / Events / Gallery / Notes Paneles de detalles al estilo de los Gramplets de escritorio para el registro seleccionado
Born In Búsqueda por lugar de nacimiento
Crossing Relationships Encuentra relaciones que cruzan generaciones de formas poco habituales
GOQL Query Escriba una cláusula where de GOQL en bruto y elija expresiones de campo como columnas
Hello Table El ejemplo inicial mínimo
Interactive Search Un Gramplet de búsqueda en vivo, mientras escribe
People Explorer Un Gramplet más completo para explorar personas
Phonebook Surname Sort Ordenación de apellidos que ignora los acentos (p. ej. «Müller» se ordena junto a «Muller»)
Relationship / Relationships Cómo están emparentadas dos personas seleccionadas
Selected Record Reacciona al registro que esté abierto en ese momento
To-Do / Todo Tracker Lleva el control de las tareas de investigación pendientes en todo el árbol
Tree Statistics Un tablero con recuentos de registros y porcentajes derivados

En cada vista de lista (Personas, Familias, Eventos, …), los Gramplets que ha añadido a esa vista aparecen como pestañas en un panel plegable Gramplets bajo la tabla. El panel empieza plegado, y un Gramplet solo se ejecuta mientras el panel está abierto: al expandirlo se ejecuta la pestaña seleccionada. La primera ejecución de una sesión también carga el entorno de Python del navegador, así que puede tardar unos segundos; las siguientes son rápidas. Mantener el panel plegado no cuesta nada, ya que no se carga Python hasta que lo abre.

Escribir un Gramplet

El código de un Gramplet se ejecuta en un entorno aislado con un conjunto de funciones y objetos ya disponibles: no hace falta importar nada para llegar al árbol. El directorio gramplet_examples/ del repositorio es un recorrido numerado por la API, del más sencillo al más avanzado; la misma referencia está disponible dentro de la propia aplicación mediante el botón de ayuda (i) «Writing a Gramplet» del editor de Gramplets.

El Gramplet útil más pequeño

matches = people(
    "gender == Person.MALE and birth.date.sortval >= Date('Jan 1, 1900')",
    limit=25,
)

columns("Person")
for person in matches:
    row(person)
  • people(where=None, order=None, limit=50) obtiene las Personas que coinciden con una consulta, como registros completos, en una sola llamada; y hay un families()/events()/places()/repositories()/sources()/ citations()/media()/notes()/tags() para cada uno de los demás tipos de objeto, con la misma firma, solo que con otra tabla.
  • where= se escribe en GOQL: exactamente la misma sintaxis que el cuadro de búsqueda de cualquier vista de lista, incluida la posibilidad de recorrer relaciones (birth.date.sortval) aunque el campo por el que filtra no sea uno de los que muestra.
  • No hace falta await aunque por debajo sea una llamada de red real: se inserta automáticamente.
  • row(*values) añade una fila a la tabla. Pasar un objeto Persona/Evento/Lugar/... completo (en lugar de un campo elegido a mano) lo muestra como un enlace en el que se puede hacer clic, que ya muestra su nombre/título completo y su ID de Gramps; al hacer clic se abre una ventana emergente para ver ese registro en Lista, Mapa, Gráfico o Cronograma.

Cada registro que devuelven people()/families()/etc. es un «DataDict» de Gramps: un dict normal con los campos del objeto, pero con acceso por punto encima: person.primary_name.first_name es el mismo valor que person["primary_name"]["first_name"]. Recurra a esto siempre que quiera calcular algo a partir de un campo o mostrar uno que la propia representación de row() no muestra. row(), html() y print() se pueden llamar tantas veces como quiera, en cualquier orden: todo aparece en el orden en que lo añadió.

Contar sin coste y sin descargar el árbol

total_people = db.get_number_of_people()
women = db.get_number_of("person", where="gender == Person.FEMALE")
no_birth_date = db.get_number_of("person", where="birth.date.sortval is None")

db.get_number_of_<type>() es un recuento barato de todo el árbol: el total llega en una cabecera de la respuesta y no se descarga realmente ningún registro. db.get_number_of(object_type, where=...) es el equivalente condicional, para cuando el propio recuento necesita un filtro.

Recorrer relaciones en una consulta, y volver para mostrarlas

Una condición where= puede recorrer una relación para filtrar por ella (el evento de nacimiento de una persona, el padre de una familia, el lugar de un evento), aunque people()/families()/etc. siempre devuelven un objeto completo y aislado, sin forma de pedir directamente un campo relacionado:

born_and_died_same_place = people(
    "birth.place.title == death.place.title and birth.place.title is not None",
    order=[{"column": "surname", "direction": "asc"}],
    limit=50,
)

Obtener un campo relacionado para mostrarlo (en lugar de solo filtrar por él) es una búsqueda aparte, a mano, mediante los propios métodos de db (db.get_event_from_handle(...), etc.), siguiendo la misma indirección por handles que usa internamente el modelo de datos de Gramps.

Las colecciones también funcionan: any(c.given_name == 'Steve' for c in children) coincide con una familia si algún hijo cumple una condición, y len([c for c in children if c.gender == Person.MALE]) > 1 cuenta solo los hijos varones. Consulte GOQL para ver todo lo relativo a las colecciones, incluido su límite actual (un salto dentro de una colección, sin seguir encadenando más allá de lo que contiene).

Reaccionar al registro seleccionado

record = get_selected()
if record is not None:
    if isinstance(record, Person):
        ...

get_selected() devuelve el registro que esté abierto en ese momento en el panel de detalles de la vista: None cuando no hay nada seleccionado, o cuando se ejecuta desde la vista previa del editor independiente (que no tiene ningún contexto de vista). Hace una petición de red en la primera llamada de una ejecución y la memoriza para el resto. isinstance(record, Person) (etc.) le indica qué tipo de registro ha obtenido, lo cual importa en un Gramplet asociado a «Todas» las vistas. Para que se actualice en vivo mientras hace clic por la lista, active «Re-run automatically when the selected record changes» en el propio editor del Gramplet; si lo deja desactivado (por defecto), get_selected() sigue funcionando, pero solo refleja lo que estuviera seleccionado la última vez que el Gramplet se ejecutó por algún otro motivo.

Reaccionar al filtro activo

Un Gramplet puede añadir su propia consulta sobre el filtro que esté aplicado en ese momento a la vista (escrito en el propio cuadro de búsqueda de la vista o aplicado mediante el selector «Filtros»: un filtro guardado o una regla personalizada), de modo que filtrar la lista hasta una rama del árbol restringe la salida del Gramplet de la misma manera:

rows = filter(
    "person",
    where=and_filters(get_filter(), "birth.date.sortval is not None and death.date.sortval is not None"),
)

Esto necesita views: [...] en el manifiesto (get_filter() solo devuelve un where_expr que corresponda al tipo de objeto de la propia vista) y listensToFilter: true para que el Gramplet realmente se vuelva a mostrar cuando cambie el filtro.

Diagramas

pygal, matplotlib y plotly vienen incluidos: un simple import (o, para plotly, from plotly.subplots import make_subplots) funciona sin conexión, sin necesidad de %pip install. print(fig) sobre una Figure de plotly, una cadena SVG de pygal o una figura de matplotlib se reconoce automáticamente y se muestra como diagrama, sin necesidad de to_html()/incrustación manual.

Para los datos de un diagrama, es preferible filter(object_type, what=[...], where=..., limit=...) a people()/families()/etc. cuando solo necesita uno o dos campos: people() implica una petición de red del objeto completo por registro, lo cual es un desperdicio para un diagrama que solo representa, por ejemplo, la edad al fallecer.

Disposición lado a lado

col1, col2 = st.columns([2, 1])
with col1:
    row("Wider column")
with col2:
    row("Narrower column")

st.columns(spec) dispone spec regiones lado a lado (un entero para ese número de columnas de igual ancho, o una lista de pesos (st.columns([2, 1])) para anchos proporcionales) y devuelve una región por columna. Todo lo que se escriba dentro de with col: (row()/html()/print(), u otra llamada st.*, incluido un st.columns() anidado) va a esa columna en lugar de al nivel superior; col.write(x) funciona igual sin un bloque with.

Instalar un paquete de terceros

%pip install unidecode
from unidecode import unidecode

%pip install es sintaxis «mágica» de Jupyter, que se reescribe como una llamada real await micropip.install([...]) antes de que se ejecute su código: debe ir en su propia línea, al principio, exactamente como en un cuaderno (escribirlo dentro de un if, por ejemplo, no funcionará). Solo funciona con paquetes puramente de Python, sin código compilado ni extensiones en C; pygal/matplotlib/plotly no lo necesitan porque vienen incluidos, pero la mayoría de las pequeñas utilidades puramente de Python de PyPI se instalan bien así.

Publicar en la tienda

Una carpeta por Gramplet en gramplet-store/, con el nombre de su id (el nombre de la carpeta y el campo id del manifiesto deben coincidir):

gramplet-store/
  <slug>/
    manifest.json   # obligatorio
    code.py         # obligatorio -- el código fuente Python del Gramplet
    icon.png        # opcional (png/jpg/jpeg/svg/webp)

Campos de manifest.json:

Campo Obligatorio Significado
id sí Debe coincidir con el nombre de la carpeta.
name sí Se muestra como título de la entrada en la tienda.
description sí Se muestra bajo el nombre en la tienda.
version sí Semver simple, p. ej. "1.0.0"; increméntela cada vez que cambie code.py o el manifiesto, para que las copias instaladas puedan detectar que hay una actualización disponible.
author sí Se muestra en la tienda.
category sí Una etiqueta corta ("example", "detail", "utility", "chart", ...) que se usa para agrupar/filtrar entradas.
views no A qué vistas de tipo de objeto ("person", "family", ...) se puede añadir este Gramplet. Omítalo para «todos los tipos».
listensToSelection no Si este Gramplet debe volver a ejecutarse cuando cambie el registro seleccionado.
listensToFilter no Si este Gramplet debe volver a ejecutarse cuando cambie el filtro activo (cuadro de búsqueda o selector Filtros).

Después de añadir o editar una entrada, regenere catalog.json, el único archivo que la aplicación obtiene realmente:

npm --prefix app run build:gramplet-catalog

Esto valida cada entrada (campos obligatorios presentes, id igual a su carpeta, version con aspecto de semver, code.py no vacío) y falla de forma ruidosa en el primer problema en lugar de publicar un catálogo roto.

Lo que los Gramplets todavía no pueden hacer

Hoy los Gramplets son de solo lectura sobre el árbol: pueden consultar (filter()/get_object()) pero no editar ni crear objetos. Tampoco hay todavía un equivalente para otros tipos de complementos de Gramps (herramientas, informes). Consulte Hoja de ruta y limitaciones conocidas para ver en qué punto están.

Clone this wiki locally