Skip to content

Writing Gramplets.es

Doug Blank edited this page Oct 19, 2026 · 1 revision

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

Escribir Gramplets

Cómo escribir su propio Gramplet y publicarlo en la tienda de Gramplets. Solo el propietario de un árbol o un administrador del sitio puede crear o editar Gramplets (vea Quién puede usar los Gramplets).

Primeros pasos

El código de un Gramplet se ejecuta en un entorno aislado con un conjunto de funciones y objetos ya disponibles, así que no hacen falta imports para llegar al árbol.

El directorio gramplet_examples/ del repositorio es un recorrido numerado por la API, de lo más sencillo a lo más avanzado. La misma referencia está disponible en la aplicación, desde 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)

Obtener registros

people(where=None, what=None, order=None, limit=50) obtiene las Personas que cumplen una consulta, como registros completos, en una sola llamada. Con what=["gramps_id", "primary_name"] obtiene solo esos campos.

Hay families() / events() / places() / repositories() / sources() / citations() / media() / notes() / tags() para cada uno de los demás tipos de registro, con la misma firma.

Escribir la consulta

where= se escribe en GOQL, la misma sintaxis que el cuadro de búsqueda de cualquier lista. Puede cruzar relaciones (birth.date.sortval), aunque el campo por el que filtra no sea uno que muestre.

No hace falta await, aunque por debajo es una llamada de red real. Se inserta automáticamente.

Mostrar resultados

  • row(*values) añade una fila a la tabla. Pasar un objeto Person/Event/Place/... completo, en lugar de uno de sus campos, lo muestra como un enlace con su nombre o título completo y su ID de Gramps. Al hacer clic se abre una ventana para ver ese registro en Lista, Mapa, Grafo o Cronología.
  • name_of(person) es el nombre de una persona como texto, nombre de pila y luego apellidos («Root Ancestor»), tal como los etiqueta row(), para un encabezado o una frase. También acepta uno de sus nombres, por ejemplo name_of(person.alternate_names[0]).
  • row(), html() y print() pueden llamarse tantas veces como quiera, en cualquier orden. Todo aparece en el orden en que lo añadió.

Trabajar con registros

Cada registro devuelto por people(), families() y los demás es un «DataDict» de Gramps: un dict simple con los campos del registro, con acceso por punto encima. person.primary_name.first_name es el mismo valor que person["primary_name"]["first_name"]. Úselo cuando quiera calcular algo a partir de un campo, o mostrar uno que row() no muestra.

Imprimir un registro

Imprimir un registro nunca falla. print(person) o f"{person}" muestra un registro completo como lo etiqueta row() («Root Ancestor [I0001]»), una fecha como fecha, y cualquier otra cosa (un resultado de filter(), person.primary_name) como sus campos simples.

Contar 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, así que no se descarga ningún registro. db.get_number_of(object_type, where=...) es lo mismo con un filtro.

Cruzar relaciones

En una consulta

Una condición where= puede cruzar una relación para filtrar por ella: el evento de nacimiento de una persona, el padre de una familia, el lugar de un evento.

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,
)

Para mostrar

people(), families() y los demás siempre devuelven el propio registro, sin forma de pedir directamente un campo de un registro relacionado. Obtenerlo para mostrarlo es una consulta aparte con los métodos de db, como db.get_event_from_handle(...), siguiendo los mismos handles que usa el propio modelo de datos de Gramps.

Colecciones

any(c.given_name == 'Steve' for c in children) encaja 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. Vea GOQL para más detalles, incluido el límite actual: un salto dentro de una colección, sin encadenar 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 abierto en el panel de detalle de la vista. Es None cuando no hay nada seleccionado, o al ejecutar desde la vista previa del editor, que no tiene vista. Hace una petición de red en su primera llamada de una ejecución y recuerda el resultado durante el resto.

isinstance(record, Person) (etc.) le dice qué tipo de registro ha recibido, lo que importa para un Gramplet asociado a «Todas» las vistas.

Actualizar cuando cambia la selección

Para que el Gramplet se actualice en vivo mientras recorre la lista, active «Re-run automatically when the selected record changes» en su editor. Con eso desactivado (lo predeterminado), get_selected() sigue funcionando, pero solo refleja lo que estaba seleccionado la última vez que se ejecutó el Gramplet.

Reaccionar al filtro activo

Un Gramplet puede superponer su propia consulta al filtro aplicado a la vista, ya sea escrito en el cuadro de búsqueda o elegido en Filtros (un filtro guardado o una regla personalizada). Filtrar la lista a una rama del árbol restringe entonces 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 dos ajustes en el manifiesto:

  • views: [...], ya que get_filter() solo devuelve un where_expr que coincide con el propio tipo de registro de la vista;
  • listensToFilter: true, para que el Gramplet se vuelva a ejecutar cuando cambie el filtro.

Diagramas

pygal, matplotlib y plotly vienen incluidos, así que un simple import (o, para plotly, from plotly.subplots import make_subplots) funciona sin conexión, sin %pip install.

print(fig) sobre una Figure de plotly, una cadena SVG de pygal o una figura de matplotlib se reconoce y se dibuja como diagrama, sin to_html() ni incrustación manual.

Para los datos de un diagrama, pase what=[...] cuando solo necesite uno o dos campos. people() por sí solo descarga todos los campos de todos los registros, lo que es un desperdicio para un diagrama que solo representa, por ejemplo, la edad al morir.

Disposición en columnas

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

st.columns(spec) dispone regiones una al lado de otra: un entero para ese número de columnas de igual anchura, o una lista de pesos (st.columns([2, 1])) para anchuras proporcionales. 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. col.write(x) hace lo mismo sin bloque with.

Un Gramplet en su propia ventana

Gramplets → Nuevo Gramplet… abre el editor de Gramplets preparado para este tipo. En un Gramplet existente, ponga Clase en Gramplet — opens in a window from the Gramplets menu.

En lugar de los ajustes de Gramplet de vista hay un campo Categoría, que lo agrupa con otros en el menú Gramplets (por ejemplo chart). Si se deja vacío, va a Other.

Pedir datos

El código se ejecuta en cuanto se abre la ventana. Para pedir datos primero, dibújelos con los widgets st.* y haga el trabajo real tras un st.button. Siguen los nombres y argumentos de Streamlit, y funcionan igual en los Gramplets de vista:

  • st.button, st.text_input, st.number_input, st.checkbox, st.selectbox, st.radio
  • st.columns, st.progress
  • los cuadros de mensaje st.info / st.success / st.warning / st.error
  • st.session_state

Mostrar el progreso

Para un Gramplet de larga duración, muestre una barra con st.progress(value, text), como en Streamlit. value va de 0 a 100 (o de 0.0 a 1.0). .progress(...) de la barra devuelta la mueve en su sitio y .empty() la quita, lo bastante barato como para llamarlo una vez por fila.

people(..., limit=None) (y las demás funciones de lista) devuelven todas las coincidencias en lugar de las 50 primeras:

surname = st.text_input("Surname", "Smith")
if st.button("Run"):
    everyone = people(f"primary_name.surname_list[0].surname == '{surname}'", limit=None)
    bar = st.progress(0, text="Checking people...")
    for i, person in enumerate(everyone):
        bar.progress((i + 1) / len(everyone))
        ...
    bar.empty()

Ejecuciones largas

Un Gramplet puede ejecutarse todo el tiempo que necesite, y una página abierta durante horas puede seguir ejecutándolos. Cada petición que hace un Gramplet usa su sesión actual, renovada en segundo plano como en el resto de la aplicación. Solo si la propia sesión ha terminado se detiene una ejecución, con Your session has expired. Sign in again, then rerun this Gramplet.

Instalar un paquete de terceros

%pip install unidecode
from unidecode import unidecode

%pip install es sintaxis al estilo de Jupyter, que se reescribe como una llamada real await micropip.install([...]) antes de ejecutar su código. Debe ir en su propia línea, al principio, como en un notebook; dentro de un if, por ejemplo, no funciona.

Solo funciona con paquetes puramente de Python, sin código compilado ni extensiones en C. La mayoría de las pequeñas utilidades en Python puro de PyPI se instalan bien así.

Cambiar datos desde el código

Un Gramplet (no un Gramplet de vista, que nunca cambia datos) puede cambiar y eliminar registros una vez activado Permitir cambios en los datos en el editor de Gramplets. La API es la misma que usa Gram.py Script en Gramps de escritorio: cambie un registro como cambiaría cualquier objeto de Python.

for person in people("len(citation_list) == 0", limit=None):
    person.private = True

Ejemplos en la tienda

  • Mark Unsourced People Private encuentra primero a las personas de forma barata con filter(), y luego obtiene y cambia solo las que lo necesitan, dentro de un grupo begin_changes() con nombre.
  • Tag People añade a una lista (person.tag_list.append(tag.handle); los registros guardan las etiquetas como handles, no como nombres) y usa un st.selectbox() en el que hay que elegir antes de que cambie nada.

Cambiar, eliminar y agrupar

  • record.field = value cambia un campo, también los anidados (person.primary_name.first_name = "Chris"). Lo mismo hacen las modificaciones de listas (.append(), .pop(), ...) y cualquier método set_*(), por ejemplo person.set_privacy(True).
  • delete(record) elimina un registro. No quita las referencias a él de otros registros.
  • begin_changes("message") / end_changes() agrupan los cambios que hay entre ellos bajo ese mensaje. Son opcionales: sin ellos toda la ejecución es un grupo, con el nombre del Gramplet.

Qué registros se pueden cambiar

Se pueden cambiar los registros de people() y las demás funciones en plural, get_raw_object(), get_object(), get_selected() y db.*. Los resultados de filter() no, ya que solo contienen algunos campos de un registro.

Pedir el mismo registro dos veces en una ejecución devuelve el mismo objeto, con sus cambios dentro.

Pruebas mientras desarrolla

En el editor de Gramplets, cada ejecución es una prueba. Cada registro cambiado se comprueba primero contra su esquema de Gramps, la misma comprobación que hace el servidor; un valor no válido detiene la ejecución e indica el campo.

Una prueba puede diferir de la ejecución real en una cosa: las búsquedas (filter(), count(), where=) se ejecutan en el servidor, así que siguen viendo el árbol como estaba antes de los cambios de la ejecución.

Cuándo se guardan los cambios

En una ejecución real, Ejecutar guarda cada grupo cuando el código lo cierra, en end_changes() o al terminar la ejecución. Vea Cambiar datos para lo que ve el usuario, incluido Deshacer.

Los widgets de la salida (st.button("Apply"), etc.) vuelven a ejecutar el código en el mismo modo del que procede el resultado actual. Así, un Gramplet puede mostrar en una prueba lo que cambiaría y aplicarlo tras Ejecutar.

Publicar en la tienda

Estructura de carpetas

Una carpeta por Gramplet en gramplet-store/, con el nombre de su id. El nombre de la carpeta y el propio campo id del manifiesto deben coincidir.

gramplet-store/
  <slug>/
    manifest.json   # required
    code.py         # required -- the Gramplet's Python source
    icon.png        # optional (png/jpg/jpeg/svg/webp)

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).
kind no "view" (el valor predeterminado) para un Gramplet de vista; "window" para un Gramplet que se ejecuta desde el menú Gramplets. views/listensTo* solo se aplican a los Gramplets de vista.
allowChanges no true para instalarlo con Permitir cambios en los datos activado (la tienda muestra una insignia Cambia sus datos). Solo se permite con "kind": "window".

Regenerar el catálogo

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

npm --prefix app run build:gramplet-catalog

Esto comprueba cada entrada (campos obligatorios presentes, id coincide con su carpeta, version parece semver, code.py no vacío, kind válido) y falla de forma clara ante el primer problema en lugar de publicar un catálogo roto.

Clone this wiki locally