Zum Hauptinhalt springen

Ein Template-Paket schreiben

Paketkonfiguration liest zwei fertige Pakete, um die Hebel zu erklären. Diese Seite geht die andere Richtung: die Anatomie eines Pakets, das Sie selbst schreiben – die drei Dateien, wofür jede zuständig ist, und die Fallen, die still statt laut scheitern. Die Einführung baut dann eines von Anfang bis Ende.

Ein Template-Paket macht aus einem Modell Dateien. Es ist kein Programm, das Sie ausführen; es ist ein Paket, das die Generierungs-Engine lädt und aufruft. Nichts darin ist auf ein bestimmtes Modell zugeschnitten – dasselbe Paket läuft über jedes Modell, das die Typen hat, nach denen es fragt.

Die drei Dateien​

Ein minimales Paket ist ein Ordner mit genau drei Dingen darin:

library_cards/
config/config.json the vocabulary and the rules
__init__.py the context builders (Python)
templates/book_card.md.jinja2 the template

Jede beantwortet eine andere Frage, und sie übergeben in eine Richtung:

Die drei Dateien eines Pakets und was jede beantwortetDie Konfiguration entscheidet, auf welche Modellelemente eine Regel zutrifft, und nennt Builder und Vorlage. Der Builder liest das Modell und gibt Kontext zurück. Die Vorlage macht aus diesem Kontext den Text einer Datei. Jede übergibt an die nächste, in eine Richtung.Welche Elemente, welche Regel?config/config.jsondie nötigen Typen,die Definitionen unddie Namen der beidenDateien danebenWas gilt für dieses Element?__init__.pycreate_pack plus einContext Builder jeDefinition – die einzigeStelle, die das Modell liestWie sieht die Datei aus?templates/*.jinja2formatiert den Kontextund sonst nichts – sieläuft nie selbst durchdas Modellnennt den Buildergibt Kontext zurück
DateiBeantwortetSprache
config/config.jsonWelche Typen müssen existieren, und welche Regel erzeugt was?JSON
__init__.pyWie wird aus einer Modell-Entität die Daten, die eine Vorlage braucht?Python
templates/*.jinja2Wie sieht die erzeugte Datei aus?Jinja (ITB-Trennzeichen)

Die Engine liest die Konfiguration, findet jede Entität, auf die eine Definition passt, ruft den Context Builder dieser Definition, um aus der Entität ein einfaches Datenobjekt zu machen, und rendert die Vorlage damit. Modell → Kontext → Datei.

1 · Die Konfiguration​

config/config.json hat zwei Aufgaben: das Vokabular zu deklarieren, das das Paket in Ihrem Modell braucht, und die Definitionen – die Regeln, die aus dem Modell Dateien machen.

{
"version": 1,
"metadata": { "name": "library_cards", "version": "1.0.0", "display_name": "Library Cards" },
"requirements": {
"entity_types": [ { "name": "Book", "key": "lib_book", "match_by": "key" } ],
"attribute_types": [ { "name": "Title", "key": "lib_title", "match_by": "key" },
{ "name": "Detail", "key": "lib_detail", "match_by": "key" } ],
"entity_custom_properties": [ { "name": "Shelf", "key": "lib_shelf", "data_type": "string" } ]
},
"definitions": [
{
"name": "book_card",
"type_key": "lib_book",
"template": "book_card.md.jinja2",
"context_builder": "build_book_card_context",
"name_pattern": "{name}.card.md"
}
]
}

Requirements sind ein Vertrag mit dem Modell. Bei der Installation gleicht ITB sie gegen die Typen Ihres Modells ab – ein Book-Typ mit dem Schlüssel lib_book, zwei Attributtypen, eine benutzerdefinierte Eigenschaft. match_by: "key" heisst, dass die Definition auf den Schlüssel passt und nicht auf den Anzeigenamen; „Book" in der Oberfläche in „Title" umzubenennen macht also nichts kaputt.

Eine Definition ist eine Regel. type_key ist, worauf sie passt; template und context_builder sind die beiden Hälften, die die Datei erzeugen; name_pattern benennt sie, wobei {name} aus der Entität gefüllt wird.

Drei Felder einer Definition entscheiden, wie oft und worüber sie läuft:

scopeLäuftDer Builder bekommt
entity (Vorgabe)einmal je passender Entitätdie Id dieser Entität
viewpointeinmal je Ansichtdie Id der Ansicht
modeleinmal für das ganze Modelldie Kennung __model__

Eine Buchkarte ist entity-scoped: eine Karte je Buch. Ein Katalog über alle Bücher wäre model-scoped: eine Datei, und der Builder läuft durch das ganze Modell.

Stille Fehler in der Konfiguration
  • Eine Definition braucht context_builder. Ohne ihn weist TemplateConfig das ganze Paket mit missing required field(s) zurück – dieser Fall scheitert also laut. Die übrigen nicht:
  • Ein Capability-Paket braucht distribution, ein Template-Paket nicht. (Template-Pakete werden als einfache Dateien ausgeliefert; nur Capability-Pakete sind verschlüsselt.)
  • Ein ins Leere zeigender context_builder-Name – einer, den das Python nicht exportiert – erzeugt keine Datei und keinen Fehler, den Sie bemerken. Der Name in der Konfiguration und die Funktion in __init__.py müssen genau übereinstimmen.

2 · Der Context Builder​

__init__.py ist Python, und es ist Pflicht – es gibt kein Paket „nur aus Konfiguration". Es exportiert eine Fabrik, create_pack, und einen Context Builder je Definition.

import os
from pydantic import BaseModel
from template_engine_api import TemplatePack, TransformHelpers

CONFIG_PATH = os.path.join(os.path.dirname(__file__), "config", "config.json")
TEMPLATES_DIR = os.path.join(os.path.dirname(__file__), "templates")


class BookCardContext(BaseModel):
title: str
details: list[dict]
shelf: str


class LibraryCardBuilders(TransformHelpers):
def build_book_card_context(self, entity_id: str) -> BookCardContext:
entity = self._find(entity_id)
title = ""
details = []
for attr in self._attrs(entity_id):
role = self.imh.type_key(attr.id) if attr.type else None
if role == "lib_title":
title = attr.name
elif role == "lib_detail":
details.append({"name": attr.name, "value_type": attr.datatype or "string"})
return BookCardContext(
title=title or entity.name,
details=details,
shelf=self._cp(entity_id, "lib_shelf", ""),
)


def create_pack(data_model, config=None) -> TemplatePack:
builders = LibraryCardBuilders(data_model, config=config)
return TemplatePack(
context_builders={"build_book_card_context": builders.build_book_card_context},
template_config=CONFIG_PATH,
template_loader_path=TEMPLATES_DIR,
)

Die ganze Aufgabe eines Builders ist, das Modell zu lesen und ein einfaches Datenobjekt zurückzugeben. Er erbt von TransformHelpers, was ihm die Lesehilfen gibt:

HilfeLiefert
self._find(entity_id)die Entität
self._attrs(entity_id)ihre Attribute
self._cp(entity_id, key, default)den Wert einer benutzerdefinierten Eigenschaft
self.imh.type_key(obj_id)den Schlüssel des Typs einer Entität, eines Attributs oder einer Beziehung

type_key ist die, auf die es ankommt: damit fragt der Builder „welche Rolle spielt dieses Attribut" und bekommt lib_title oder lib_detail zurück – den Schlüssel, nicht den Anzeigenamen, also stabil gegenüber Umbenennungen. Ein Attribut ohne Typ liefert None und wird übersprungen statt geraten: in einem erzeugten Vertrag ist ein Feld, über dessen Verbindlichkeit niemand entschieden hat, schlimmer als ein fehlendes.

Zwei Fallen im Python
  • create_pack ist Pflicht. Ohne sie wirft load_pack einen PackProtocolError. Eine leere __init__.py genügt nicht, so plausibel sie aussieht.
  • Schreiben Sie kein from __future__ import annotations. Es macht aus jeder Annotation eine Zeichenkette, und Pydantic kann die Kontextmodelle dann nicht auflösen, wenn das Paket dynamisch importiert wird – „…Context is not fully defined". Das hat schon echte Zeit gekostet; lassen Sie es weg.

3 · Die Vorlage​

Der Rückgabewert des Builders kommt in der Vorlage als ctx an. Die Vorlage macht daraus die Datei.

# {@ ctx.title @}

{= if ctx.details =}
| Detail | Type |
| --- | --- |
{= for d in ctx.details =}
| {@ d.name @} | {@ d.value_type @} |
{= endfor =}
{= else =}
_No details recorded yet._
{= endif =}

{= if ctx.shelf =}
Shelf: **{@ ctx.shelf @}**
{= endif =}

Die Trennzeichen sind nicht die üblichen. Ein Ausdruck ist {@ … @} und ein Block {= … =}, nicht {{ … }} und {% … %}. Das ist Absicht: es lässt {{ … }} unangetastet, sodass eine Vorlage dbt- oder Jinja-Code in die erzeugte Datei schreiben kann, ohne dass die Engine ihn auffrisst – die Data-Vault-Pakete verlassen sich genau darauf.

Die Falle, die wie ein Kontextfehler aussieht

Die Standardtrennzeichen {{ … }} lösen keinen Fehler aus. Sie werden als wörtlicher Text gerendert und der Ausdruck kommt leer heraus; ein falsches Trennzeichen sieht also so aus, als hätte der Builder den Wert nicht geliefert. Ist ein Wert unerklärlich leer, prüfen Sie das Trennzeichen vor dem Builder.

In eine laufende Instanz bringen​

Ein selbst geschriebenes Template-Paket wird nicht über den Marketplace-Dialog installiert – der listet veröffentlichte Pakete. Eigene laden Sie über die API hoch, je Benutzer einmal:

cd library_cards && zip -r ../library_cards.zip .
curl -X POST "$ITB_API/api/v1/template/packs" \
-H "Authorization: Bearer $TOKEN" \
-F "file=@../library_cards.zip"

Danach führt Generate All es neben jedem anderen installierten Paket aus, und Show Templates meldet, welche seiner Definitionen auf eine bestimmte Entität passen. Die Einführung macht das von Anfang bis Ende, ausgehend von einem leeren Modell.

Ein Paket testen, bevor Sie es ausliefern​

Ein Paket ist Code, und der billigste Weg zu wissen, dass es funktioniert, ist, es gegen ein Stub-Modell zu rendern – den Builder aufrufen, sein Ergebnis durch eine Jinja-Umgebung mit denselben Trennzeichen schicken, die die Engine benutzt, und die Ausgabe prüfen. So sind die Demo-Pakete getestet, und es fängt beide Arten stiller Fehler von oben: ein falsches Trennzeichen lässt den Wert leer, und ein Builder, der ein typloses Attribut überspringt, beweist das durch dessen Fehlen in der gerenderten Datei.