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:
| Datei | Beantwortet | Sprache |
|---|---|---|
config/config.json | Welche Typen müssen existieren, und welche Regel erzeugt was? | JSON |
__init__.py | Wie wird aus einer Modell-Entität die Daten, die eine Vorlage braucht? | Python |
templates/*.jinja2 | Wie 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:
scope | Läuft | Der Builder bekommt |
|---|---|---|
entity (Vorgabe) | einmal je passender Entität | die Id dieser Entität |
viewpoint | einmal je Ansicht | die Id der Ansicht |
model | einmal für das ganze Modell | die 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.
- Eine Definition braucht
context_builder. Ohne ihn weistTemplateConfigdas 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__.pymü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:
| Hilfe | Liefert |
|---|---|
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.
create_packist Pflicht. Ohne sie wirftload_packeinenPackProtocolError. Eine leere__init__.pygenü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 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.