`iris-persistence` : une couche de persistance Python-first pour InterSystems IRIS
Avec Embedded Python et la Native API, il devient de plus en plus naturel d'ecrire une partie de la logique applicative IRIS en Python. Mais une question revient vite : comment manipuler des objets persistants IRIS depuis Python sans perdre le lien avec le modele objet natif, les tables sql de définition de classes, les index, le stockage et les projections SQL ?

iris-persistence explore cette question. Le projet fournit une couche de persistance objet en Python pour InterSystems IRIS, inspiree de %Persistent. L'idee est simple : declarer des modeles Python types, puis les synchroniser avec des classes IRIS, ou au contraire se connecter a des classes IRIS existantes sans les modifier.
Un modele Python, une classe IRIS
Voici un exemple minimal :
from typing import Annotated
from iris_persistence import Field, Model
class Product(Model, persistent=True):
name: str = Field(required=True, max_length=200, unique=True)
price: Annotated[float, Field(default=0.0)]
in_stock: bool = True
class Meta:
classname = "Demo.Product"
mode = "replace"
Product.sync_schema()
product = Product(name="Widget", price=12.5, in_stock=True)
product.save()
same = Product.get(product.pk)
rows = Product.where(name="Widget").order_by("name").all()
Le modele Python declare les champs, leurs types et une partie des metadonnees IRIS. Lors de sync_schema(), iris-persistence ecrit dans les tables SQL de définition de classe d'IRIS, notamment les definitions de classe, de proprietes, d'index et de stockage. IRIS compile ensuite la classe et conserve son role de source de verite pour la projection SQL et le runtime objet.
Dans cet exemple, unique=True permet de declarer l'unicite directement sur le champ Python. L'objectif reste de travailler avec des objets IRIS, pas de generer du SQL DDL a la main.
Trois modes selon le niveau de controle souhaite
Le point important du projet est la notion de mode de synchronisation. Tous les usages n'ont pas le meme rapport au schema.
| Mode | Idee | Cas d'usage |
|---|---|---|
extend |
Python ajoute ou met a jour ce qu'il declare, sans supprimer le reste | Demarrer prudemment sur une classe existante |
replace |
Python reconstruit la classe IRIS depuis le modele | Nouveau schema entierement gere cote Python |
observe |
Python ne modifie jamais le schema | Lire et manipuler des classes IRIS existantes |
Ce decoupage est utile pour les projets brownfield. On peut commencer en observe pour s'attacher a des classes existantes, passer en extend pour ajouter quelques elements controles depuis Python, ou utiliser replace pour des classes nouvelles dont le schema est entierement gere par le code Python.
Pas seulement des champs simples
Le projet supporte les declarations classiques avec Field(...), mais aussi typing.Annotated. Il sait mapper les types Python courants vers des types IRIS :
str,int,float,boolbytesdictetlistdatetime.date,datetime.time,datetime.datetime- references vers d'autres modeles
Model
Il est aussi possible de forcer le type IRIS sous-jacent quand le mapping automatique ne suffit pas :
class Event(Model, persistent=True):
payload: bytes = Field(iris_type="%Stream.GlobalBinary")
created_at: str = Field(iris_type="%Library.TimeStamp")
Objets lies et %SerialObject
iris-persistence gere aussi les relations entre modeles persistants et objets serialises. Par exemple :
from iris_persistence import Field, Model
class Customer(Model, persistent=True):
Name: str = Field(required=True, max_length=120)
class Meta:
classname = "Demo.Customer"
mode = "replace"
class Address(Model, serial=True):
Street: str = Field(required=True, max_length=120)
City: str = Field(required=True, max_length=80)
class Meta:
classname = "Demo.Address"
mode = "replace"
class Order(Model, persistent=True):
OrderNumber: str = Field(required=True, max_length=40, unique=True)
Customer: Customer | None = None
ShipTo: Address | None = None
class Meta:
classname = "Demo.Order"
mode = "replace"
Lors de la sauvegarde, les objets references sont materialises dans IRIS. Un %Persistent peut referencer un autre %Persistent, et un %Persistent peut embarquer un %SerialObject.
Le depot contient aussi des exemples avec des collections list et array de modeles serialises.
D'une classe .cls IRIS vers un modele Python
Un cas d'usage important est la generation de modeles Python depuis des classes IRIS deja presentes dans une namespace. Imaginons une classe ObjectScript compilee dans IRIS :
Class Demo.Article Extends %Persistent
{
Property Title As %String(MAXLEN = 200) [ Required ];
Property Body As %String(MAXLEN = 4000);
Property PublishedAt As %TimeStamp;
}
Une fois cette classe disponible dans la namespace active, on peut demander a iris-persistence de lire les tables sql de définition de classe d'IRIS et de generer la facade Python :
from iris_persistence import scaffold_from_iris
generated_files = scaffold_from_iris(
"Demo.Article",
"./generated_models",
mode="observe",
extract_meta=True,
)
Le fichier genere ressemble alors a ceci :
import datetime
from typing import Annotated
from iris_persistence import Field, Model
class Article(Model, persistent=True):
Body: Annotated[str, Field(max_length=4000)]
PublishedAt: datetime.datetime | None = None
Title: Annotated[str, Field(required=True, max_length=200)]
class Meta:
classname = "Demo.Article"
mode = "observe"
Le mode observe est important ici : le modele Python permet de manipuler les donnees, mais il ne modifie jamais la classe IRIS. C'est une approche pratique pour exposer progressivement un modele %Persistent existant a du code Python type.
Si plusieurs classes sont liees entre elles, include_related=True permet aussi de generer les modeles voisins quand ils sont references par les proprietes de la classe principale.
Stockage et metadonnees avancees
Pour les cas plus avances, le projet expose des dataclasses de metadonnees :
from iris_persistence import (
ClassMetadata,
Field,
Model,
StorageData,
StorageDefinition,
)
class ShowcaseRecord(Model, persistent=True):
Title: str = Field(required=True, max_length=350)
class Meta:
classname = "Demo.ShowcaseRecord"
mode = "replace"
parameters = {"DEFAULTGLOBAL": "^Demo.ShowcaseRecordD"}
metadata = ClassMetadata(
description="advanced schema example",
final=True,
sql_table_name="Demo_ShowcaseRecord",
procedure_block=True,
)
storage = StorageDefinition(
data_location="^Demo.ShowcaseRecordD",
default_data="ShowcaseRecordDefaultData",
type="%Storage.Persistent",
data=(
StorageData(
name="ShowcaseRecordDefaultData",
structure="listnode",
values={
"1": "%%CLASSNAME",
"2": "Title",
},
),
),
)
Ce niveau de detail est important : l'objectif n'est pas seulement d'avoir un CRUD Python, mais de rester proche de la representation IRIS reelle.
Runtime : Embedded Python ou Native API
Le projet utilise iris-embedded-python-wrapper comme facade runtime. Deux scenarios principaux sont prevus.
Dans Embedded Python, execute directement dans IRIS :
import iris_persistence
iris_persistence.configure()
Depuis un processus Python externe, via la Native API :
import iris
import iris_persistence
conn = iris.connect(host, port, namespace, user, password)
iris_persistence.configure(conn)
Le meme code metier peut donc etre teste et execute dans plusieurs contextes.
Ce que le projet n'est pas
iris-persistence n'est pas un ORM SQL generique. Les requetes simples utilisent bien une projection SQL pour retrouver les ID, mais le modele mental reste celui d'IRIS : classes, objets, dictionnaire, compilation et stockage.
Ce n'est pas non plus une abstraction qui cache completement IRIS. Au contraire, l'API expose volontairement des notions comme classname, %Persistent, %SerialObject, StorageDefinition et les parametres de classe.
Conclusion
Ce projet explore une approche Python-first de la persistance sur InterSystems IRIS, sans abandonner les mecanismes natifs qui font la force d'IRIS. Il peut etre utile pour prototyper rapidement de nouveaux modeles en Python, exposer des classes IRIS existantes a du code Python type, ou tester des comportements metier sans toujours demarrer une instance IRIS.
Les retours de la communaute seraient particulierement utiles sur trois points :
- les scenarios brownfield reels autour de classes
%Persistentexistantes ; - le niveau de metadonnees IRIS a exposer dans l'API Python ;
- l'equilibre entre une API Python simple et la fidelite au modele objet IRIS.