Skip to content

Writing Gramplets.fr

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

🌐 English · Deutsch · Español · Nederlands · 简体中文

Écrire des Gramplets

Comment écrire votre propre Gramplet et le publier dans la boutique de Gramplets. Seul le propriétaire d'un arbre ou un administrateur du site peut créer ou modifier des Gramplets (voir Qui peut utiliser les Gramplets).

Pour commencer

Le code d'un Gramplet s'exécute dans un bac à sable où un ensemble de fonctions et d'objets est déjà disponible ; aucun import n'est nécessaire pour accéder à l'arbre.

Le répertoire gramplet_examples/ du dépôt est une visite numérotée de l'API, du plus simple au plus avancé. La même référence est disponible dans l'application, via le bouton d'aide (i) « Writing a Gramplet » de l'éditeur de Gramplets.

Le plus petit Gramplet utile

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

columns("Person")
for person in matches:
    row(person)

Récupérer des enregistrements

people(where=None, what=None, order=None, limit=50) récupère les Personnes correspondant à une requête, sous forme d'enregistrements complets, en un seul appel. Avec what=["gramps_id", "primary_name"], il ne récupère que ces champs.

Il existe families() / events() / places() / repositories() / sources() / citations() / media() / notes() / tags() pour chacun des autres types d'enregistrement, avec la même signature.

Écrire la requête

where= s'écrit en GOQL, la même syntaxe que le champ de recherche de n'importe quelle liste. Il peut traverser les relations (birth.date.sortval), même quand le champ sur lequel vous filtrez n'est pas l'un de ceux que vous affichez.

Pas besoin d'await, même s'il s'agit en coulisse d'un vrai appel réseau. Il est inséré automatiquement.

Afficher les résultats

  • row(*values) ajoute une ligne au tableau. Passer un objet Person/Event/Place/... entier, plutôt que l'un de ses champs, l'affiche comme un lien cliquable avec son nom ou titre complet et son ID Gramps. Un clic ouvre une fenêtre pour voir cet enregistrement en Liste, Carte, Graphe ou Chronologie.
  • name_of(person) est le nom d'une personne en texte, prénom puis noms de famille (« Root Ancestor »), comme les étiquette row(), pour un titre ou une phrase. Il accepte aussi l'un de ses noms, par exemple name_of(person.alternate_names[0]).
  • row(), html() et print() peuvent chacun être appelés autant de fois que vous voulez, dans n'importe quel ordre. Tout apparaît dans l'ordre où vous l'avez ajouté.

Travailler avec les enregistrements

Chaque enregistrement renvoyé par people(), families() et les autres est un « DataDict » Gramps : un simple dict des champs de l'enregistrement, avec l'accès par point en plus. person.primary_name.first_name est la même valeur que person["primary_name"]["first_name"]. Utilisez-le quand vous voulez calculer quelque chose à partir d'un champ, ou en afficher un que row() n'affiche pas.

Afficher un enregistrement

Afficher un enregistrement n'échoue jamais. print(person) ou f"{person}" montre un enregistrement entier comme l'étiquette row() (« Root Ancestor [I0001] »), une date comme une date, et tout le reste (un résultat de filter(), person.primary_name) sous forme de ses champs simples.

Compter sans télécharger l'arbre

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>() est un décompte bon marché de tout l'arbre. Le total arrive dans un en-tête de réponse, donc aucun enregistrement n'est téléchargé. db.get_number_of(object_type, where=...) fait la même chose avec un filtre.

Traverser les relations

Dans une requête

Une condition where= peut traverser une relation pour filtrer dessus : l'événement de naissance d'une personne, le père d'une famille, le lieu d'un événement.

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

Pour l'affichage

people(), families() et les autres renvoient toujours l'enregistrement lui-même, sans moyen de demander directement un champ d'un enregistrement lié. Le récupérer pour l'afficher passe par une recherche séparée avec les méthodes de db, comme db.get_event_from_handle(...), en suivant les mêmes handles que le propre modèle de données de Gramps.

Collections

any(c.given_name == 'Steve' for c in children) correspond à une famille si l'un des enfants remplit une condition, et len([c for c in children if c.gender == Person.MALE]) > 1 ne compte que les fils. Voir GOQL pour plus de détails, y compris la limite actuelle : un saut dans une collection, sans chaîner au-delà de ce qu'elle contient.

Réagir à l'enregistrement sélectionné

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

get_selected() renvoie l'enregistrement ouvert dans le panneau de détail de la vue. Il vaut None quand rien n'est sélectionné, ou lors d'une exécution depuis l'aperçu de l'éditeur, qui n'a pas de vue. Il fait une requête réseau à son premier appel dans une exécution, puis garde le résultat pour le reste de celle-ci.

isinstance(record, Person) (etc.) vous dit quel type d'enregistrement vous avez reçu, ce qui compte pour un Gramplet rattaché à « Toutes » les vues.

Se mettre à jour quand la sélection change

Pour que le Gramplet se mette à jour en direct pendant que vous parcourez la liste, activez « Re-run automatically when the selected record changes » dans son éditeur. Si c'est désactivé (par défaut), get_selected() fonctionne quand même, mais ne reflète que ce qui était sélectionné lors de la dernière exécution du Gramplet.

Réagir au filtre actif

Un Gramplet peut superposer sa propre requête au filtre appliqué à la vue, qu'il soit saisi dans le champ de recherche ou choisi dans Filtres (un filtre enregistré ou une règle personnalisée). Filtrer la liste sur une branche de l'arbre restreint alors la sortie du Gramplet de la même façon :

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

Cela demande deux réglages dans le manifeste :

  • views: [...], car get_filter() ne renvoie qu'un where_expr correspondant au propre type d'enregistrement de la vue ;
  • listensToFilter: true, pour que le Gramplet se réexécute quand le filtre change.

Diagrammes

pygal, matplotlib et plotly sont fournis, donc un simple import (ou, pour plotly, from plotly.subplots import make_subplots) fonctionne hors ligne, sans %pip install.

print(fig) sur une Figure plotly, une chaîne SVG de pygal ou une figure matplotlib est reconnu et dessiné comme un diagramme, sans to_html() ni intégration manuelle.

Pour les données d'un diagramme, passez what=[...] quand vous n'avez besoin que d'un ou deux champs. people() seul télécharge tous les champs de tous les enregistrements, ce qui est du gaspillage pour un diagramme qui ne montre, par exemple, que l'âge au décès.

Disposition côte à côte

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

st.columns(spec) dispose des zones côte à côte : un entier pour autant de colonnes de même largeur, ou une liste de poids (st.columns([2, 1])) pour des largeurs proportionnelles. Il renvoie une zone par colonne.

Tout ce qui est écrit dans with col: (row(), html(), print() ou un autre appel st.*, y compris un st.columns() imbriqué) va dans cette colonne. col.write(x) fait la même chose sans bloc with.

Un Gramplet dans sa propre fenêtre

Gramplets → Nouveau Gramplet… ouvre l'éditeur de Gramplets prêt pour cette sorte. Dans un Gramplet existant, réglez Sorte sur Gramplet — opens in a window from the Gramplets menu.

À la place des réglages de Gramplet de vue, il y a un champ Catégorie, qui le regroupe avec d'autres dans le menu Gramplets (par exemple chart). Laissé vide, il va sous Other.

Demander une saisie

Le code s'exécute dès que la fenêtre s'ouvre. Pour demander d'abord une saisie, dessinez-la avec les widgets st.* et faites le vrai travail derrière un st.button. Ils suivent les noms et arguments de Streamlit, et fonctionnent de la même façon dans les Gramplets de vue :

  • st.button, st.text_input, st.number_input, st.checkbox, st.selectbox, st.radio
  • st.columns, st.progress
  • les encadrés de message st.info / st.success / st.warning / st.error
  • st.session_state

Afficher la progression

Pour un Gramplet qui dure longtemps, affichez une barre avec st.progress(value, text), comme dans Streamlit. value va de 0 à 100 (ou de 0.0 à 1.0). .progress(...) sur la barre renvoyée la fait avancer sur place et .empty() la retire, assez bon marché pour être appelé une fois par ligne.

people(..., limit=None) (et les autres fonctions de liste) renvoient toutes les correspondances au lieu des 50 premières :

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

Exécutions longues

Un Gramplet peut s'exécuter aussi longtemps qu'il le faut, et une page ouverte depuis des heures peut toujours en exécuter. Chaque requête d'un Gramplet utilise votre connexion actuelle, renouvelée en arrière-plan comme dans le reste de l'application. Ce n'est que si la connexion elle-même a pris fin qu'une exécution s'arrête, avec Your session has expired. Sign in again, then rerun this Gramplet.

Installer un paquet tiers

%pip install unidecode
from unidecode import unidecode

%pip install est une syntaxe à la Jupyter, réécrite en un vrai appel await micropip.install([...]) avant l'exécution de votre code. Il doit être sur sa propre ligne, en haut, comme dans un notebook ; à l'intérieur d'un if, par exemple, il ne fonctionne pas.

Cela ne fonctionne que pour les paquets en Python pur, sans code compilé ni extension C. La plupart des petits utilitaires en Python pur de PyPI s'installent sans problème ainsi.

Modifier des données depuis le code

Un Gramplet (pas un Gramplet de vue, qui ne modifie jamais de données) peut modifier et supprimer des enregistrements une fois Autoriser les modifications de données activé dans l'éditeur de Gramplets. L'API est celle qu'utilise Gram.py Script dans Gramps de bureau : modifiez un enregistrement comme n'importe quel objet Python.

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

Exemples dans la boutique

  • Mark Unsourced People Private trouve d'abord les personnes à moindre coût avec filter(), puis ne récupère et ne modifie que celles qui en ont besoin, dans un groupe begin_changes() nommé.
  • Tag People ajoute à une liste (person.tag_list.append(tag.handle) ; les enregistrements stockent les étiquettes sous forme de handles, pas de noms) et utilise un st.selectbox() dans lequel il faut choisir avant que quoi que ce soit change.

Modifier, supprimer et regrouper

  • record.field = value modifie un champ, y compris imbriqué (person.primary_name.first_name = "Chris"). De même pour les modifications de listes (.append(), .pop(), ...) et toute méthode set_*(), par exemple person.set_privacy(True).
  • delete(record) supprime un enregistrement. Il ne retire pas les références à celui-ci dans les autres enregistrements.
  • begin_changes("message") / end_changes() regroupent les modifications faites entre les deux sous ce message. Ils sont facultatifs : sans eux, toute l'exécution forme un seul groupe, nommé d'après le Gramplet.

Quels enregistrements peuvent être modifiés

Les enregistrements issus de people() et des autres fonctions au pluriel, de get_raw_object(), get_object(), get_selected() et db.* peuvent être modifiés. Les résultats de filter() ne le peuvent pas, car ils ne contiennent qu'une partie des champs d'un enregistrement.

Demander deux fois le même enregistrement dans une exécution renvoie le même objet, avec vos modifications dedans.

Essais à blanc pendant le développement

Dans l'éditeur de Gramplets, chaque exécution est un essai à blanc. Chaque enregistrement modifié est d'abord vérifié par rapport à son schéma Gramps, la même vérification que fait le serveur ; une valeur invalide arrête l'exécution et nomme le champ.

Un essai à blanc peut différer de la vraie exécution sur un point : les recherches (filter(), count(), where=) s'exécutent sur le serveur, elles voient donc encore l'arbre tel qu'il était avant les modifications de l'exécution.

Quand les modifications sont enregistrées

Dans une vraie exécution, Exécuter enregistre chaque groupe au moment où le code le ferme, à end_changes() ou à la fin de l'exécution. Voir Modifier des données pour ce que voit l'utilisateur, y compris Annuler.

Les widgets de la sortie (st.button("Apply"), etc.) réexécutent le code dans le mode d'où provient le résultat actuel. Un Gramplet peut ainsi montrer dans un essai à blanc ce qu'il changerait, et l'appliquer après Exécuter.

Publier dans la boutique

Organisation des dossiers

Un dossier par Gramplet dans gramplet-store/, nommé d'après son id. Le nom du dossier et le propre champ id du manifeste doivent correspondre.

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

manifest.json

Champ Requis Signification
id oui Doit correspondre au nom du dossier.
name oui Affiché comme titre de l'entrée dans la boutique.
description oui Affichée sous le nom dans la boutique.
version oui Semver simple, par ex. "1.0.0" — l'incrémenter à chaque changement de code.py ou du manifeste, pour que les copies installées puissent détecter qu'une mise à jour est disponible.
author oui Affiché dans la boutique.
category oui Une courte étiquette ("example", "detail", "utility", "chart", …) utilisée pour grouper/filtrer les entrées.
views non À quelles vues de type d'objet ("person", "family", …) ce Gramplet peut être ajouté. Omettre pour « tous les types ».
listensToSelection non Si ce Gramplet doit se réexécuter quand la fiche sélectionnée change.
listensToFilter non Si ce Gramplet doit se réexécuter quand le filtre actif change (champ de recherche ou sélecteur Filtres).
kind non "view" (par défaut) pour un Gramplet de vue ; "window" pour un Gramplet lancé depuis le menu Gramplets. views/listensTo* ne s'appliquent qu'aux Gramplets de vue.
allowChanges non true pour l'installer avec Autoriser les modifications de données activé (la boutique affiche un badge Modifie vos données). Autorisé seulement avec "kind": "window".

Régénérer le catalogue

Après avoir ajouté ou modifié une entrée, régénérez catalog.json, le seul fichier que télécharge l'application :

npm --prefix app run build:gramplet-catalog

Cela vérifie chaque entrée (champs obligatoires présents, id correspondant à son dossier, version ressemblant à du semver, code.py non vide, kind valide) et échoue clairement au premier problème plutôt que de publier un catalogue cassé.

Clone this wiki locally