Skip to content

2. Les données

This content is not available in your language yet.

L’œuvre

models/title.py déclare ce que la bibliothèque possède intellectuellement.

"""``library_title`` -- a work the library holds, in one or more copies."""
from __future__ import annotations
from typing import TYPE_CHECKING
from kewos.orm.fields import CharField, IntegerField, SelectionField
from kewos.search import SearchableDescriptor, is_searchable, register_searchable
if TYPE_CHECKING:
from kewos.orm.registry import Registry
GENRES = [
("fiction", "Fiction"),
("science", "Science"),
("history", "History"),
("children", "Children"),
("reference", "Reference"),
]
def register_title(registry: Registry) -> None:
"""Declare ``library_title`` and make it searchable and taggable."""
registry.register_model(
"library_title",
module="library",
taggable=True,
fields=[
CharField(name="name", nullable=False, max_length=200),
CharField(name="author", nullable=False, max_length=120),
CharField(name="isbn", nullable=True, max_length=17, unique=True),
IntegerField(name="published_year", nullable=True),
SelectionField(name="genre", choices=GENRES, default="fiction"),
# A long text is a CharField with NO max_length.
CharField(name="summary", nullable=True),
],
)
if not is_searchable("library_title"):
register_searchable(
SearchableDescriptor(model="library_title", title="name", subtitle="author")
)

Trois choses valent d’être remarquées.

unique=True sur l’ISBN pose une contrainte dans le schéma, pas une vérification dans un formulaire : deux processus qui insèrent le même ISBN en même temps ne peuvent pas passer tous les deux.

taggable=True branche le modèle sur le système d’étiquettes du noyau. Aucune table à créer, aucun champ à déclarer.

register_searchable le fait apparaître dans la recherche globale, avec un titre et un sous-titre. Une recherche métier ne s’écrit jamais à la main dans un module.

Les types de champ

TypePour
CharFieldUn texte. Avec max_length pour une ligne, sans pour un texte long.
IntegerField, BigIntegerFieldDes entiers.
DecimalFieldTout montant et toute quantité. precision et scale obligatoires.
BooleanFieldOui ou non.
DateField, DateTimeFieldUne date, un instant. Les instants sont toujours en UTC.
SelectionFieldUne liste fermée de valeurs.
JsonField, RichTextFieldUne structure, un contenu riche.
FileField, BinaryFieldUne pièce jointe, un contenu binaire.
Many2one, One2many, Many2manyLes relations.
ComputedFieldUne valeur dérivée d’un calcul.
RelatedFieldUn raccourci vers un champ d’une relation.
AggregateFieldUne somme ou un compte sur les enfants d’une relation.

L’exemplaire, et le champ que tout le monde oublie

def _copy_name(instance: Any) -> str:
"""``<barcode> - <shelf>``, recomputed on every write."""
shelf = instance.shelf or "?"
return f"{instance.barcode} - {shelf}"
def register_copy(registry: Registry) -> None:
"""Declare ``library_copy``."""
registry.register_model(
"library_copy",
module="library",
fields=[
Many2one(name="title_id", target_model="library_title", nullable=False),
CharField(name="barcode", nullable=False, max_length=32, unique=True),
CharField(name="shelf", nullable=True, max_length=16),
SelectionField(name="condition", choices=CONDITIONS, default="good"),
DateField(name="acquired_on", nullable=True),
ComputedField(
name="name",
base_field=CharField(name="name", nullable=True, max_length=64),
compute=_copy_name,
depends=("barcode", "shelf"),
store=True,
),
],
)

Un ComputedField avec store=True a une vraie colonne : la valeur est recalculée à chaque écriture par le noyau, et elle est donc triable et cherchable en SQL. N’y affectez jamais de valeur à la main : le recalcul l’écrase au moment du flush.

Avec store=False, il n’y a pas de colonne : la valeur est recalculée à chaque lecture, et elle est invisible pour une requête SQL.

La relation

Many2one(name="title_id", target_model="library_title", nullable=False)

Rassembler les déclarations

models/__init__.py les appelle dans l’ordre des dépendances : un exemplaire pointe vers une œuvre, un emprunt vers un exemplaire et un adhérent.

def register_models(registry: Registry) -> None:
"""Declare every model of the module, dependencies first."""
register_title(registry)
register_copy(registry)
register_member(registry)
register_loan(registry)
register_loan_workflow(registry)

Des fichiers aux tables

Fenêtre de terminal
kewos reconcile --confirm --modules-root ./modules
main [active]: reconciled (0 migration(s), 1 column(s))
done: 1 tenant(s), 0 failed

Les quatre tables existent :

library_copy library_loan library_member library_title

Et les colonnes sont plus nombreuses que ce que nous avons écrit :

acquired_on barcode condition created_at created_by_id deleted_at
deleted_by_id id kws_x_extra name shelf title_id updated_at
updated_by_id version

Ce que le noyau ajoute tout seul

ChampRôle
idLa clé primaire.
created_at, created_by_idQui a créé, et quand.
updated_at, updated_by_idQui a modifié en dernier, et quand.
versionUn compteur d’écritures, qui sert à détecter deux modifications concurrentes.
deleted_at, deleted_by_idLa suppression douce : l’enregistrement disparaît des listes sans quitter la base.
kws_x_extraLes champs ajoutés à chaud par le Studio.

On ne les déclare pas, on ne les redéfinit pas, et on peut compter dessus dans n’importe quel modèle.

L’API, sans l’avoir écrite

Fenêtre de terminal
curl -X POST http://127.0.0.1:8080/api/v1/library_title \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"name":"Dune","author":"Frank Herbert","isbn":"9780441013593",
"published_year":1965,"genre":"fiction"}'
{"id":1,"name":"Dune","author":"Frank Herbert","isbn":"9780441013593",
"published_year":1965,"genre":"fiction","summary":null,
"created_at":"2026-09-03T18:24:11.402881+00:00","created_by_id":1,
"updated_at":"2026-09-03T18:24:11.402881+00:00","updated_by_id":1,
"version":1,"deleted_at":null,"deleted_by_id":null}

Et ensuite

Les écrans.