Intégration hôte

Ce guide montre comment intégrer Catnip dans une application Python et exposer proprement des services de l'hôte (cache, logging, exceptions, etc.) aux scripts Catnip.

Philosophie

Catnip est conçu pour être embarqué dans des applications Python. L'hôte peut exposer ses services (Redis, logging, etc.) via le Context pour que les scripts Catnip les utilisent de manière contrôlée.

Expose l'utile, garde le sas fermé.

Mise en Cache Pluggable

Interface de Cache

Le système de cache de Catnip est pluggable : l'hôte peut fournir son propre backend (Redis, diskcache, memcached, etc.).

Protocole de Cache

Le backend doit implémenter le protocole CacheBackend (voir catnip/cachesys/base.py) :

from typing import Protocol, Optional
from catnip.cachesys import CacheBackend
from catnip._rs import CacheEntry, CacheKey

class CacheBackend(Protocol):
    def get(self, key: CacheKey) -> Optional[CacheEntry]: ...
    def set(self, key: CacheKey, value, metadata: dict | None = None) -> None: ...
    def delete(self, key: CacheKey) -> bool: ...
    def clear(self) -> None: ...
    def exists(self, key: CacheKey) -> bool: ...
    def stats(self) -> dict: ...

Implémentation avec Redis

import redis
from catnip import Catnip
from catnip.cachesys import CatnipCache
from catnip._rs import CacheEntry, CacheKey

class RedisCache:
    """Backend de cache utilisant Redis."""

    def __init__(self, client: redis.Redis, prefix: str = "catnip:"):
        self.client = client
        self.prefix = prefix

    def _make_key(self, key: CacheKey) -> str:
        return f"{self.prefix}{key.signature}"

    def get(self, key: CacheKey) -> CacheEntry | None:
        value = self.client.get(self._make_key(key))
        return CacheEntry(value=value, metadata={}) if value else None

    def set(self, key: CacheKey, value, metadata: dict | None = None) -> None:
        redis_key = self._make_key(key)
        self.client.set(redis_key, value)

    def delete(self, key: CacheKey) -> bool:
        return bool(self.client.delete(self._make_key(key)))

    def clear(self) -> None:
        keys = self.client.keys(f"{self.prefix}*")
        if keys:
            self.client.delete(*keys)

    def exists(self, key: CacheKey) -> bool:
        return bool(self.client.exists(self._make_key(key)))

    def stats(self) -> dict:
        return {"backend": "redis"}

# Utilisation
redis_client = redis.Redis(host='localhost', port=6379, db=0)
backend = RedisCache(redis_client, prefix="myapp:catnip:")

catnip = Catnip(cache=CatnipCache(backend=backend))

# Le cache est utilisé automatiquement
catnip.parse("2 + 2")
result = catnip.execute()

Implémentation avec diskcache

import diskcache
from catnip import Catnip
from catnip.cachesys import CatnipCache
from catnip._rs import CacheEntry, CacheKey

class DiskCache:
    """Backend de cache utilisant diskcache."""

    def __init__(self, directory: str):
        self.cache = diskcache.Cache(directory)

    def get(self, key: CacheKey) -> CacheEntry | None:
        value = self.cache.get(key.signature)
        return CacheEntry(value=value, metadata={}) if value else None

    def set(self, key: CacheKey, value, metadata: dict | None = None) -> None:
        self.cache.set(key.signature, value)

    def delete(self, key: CacheKey) -> bool:
        return bool(self.cache.delete(key.signature))

    def clear(self) -> None:
        self.cache.clear()

    def exists(self, key: CacheKey) -> bool:
        return key.signature in self.cache

    def stats(self) -> dict:
        return {"backend": "diskcache"}

# Utilisation
backend = DiskCache("/tmp/catnip_cache")
catnip = Catnip(cache=CatnipCache(backend=backend))

Signature de Cache

Catnip gère les clés de cache automatiquement (xxHash64 sur le source + options). L'hôte n'a pas besoin de calculer ou manipuler les signatures.

Cache avec TTL personnalisé

from catnip import Catnip
from catnip.cachesys import CatnipCache, CacheBackend
from catnip._rs import CacheEntry, CacheKey

class CacheWithTTL:
    """Backend wrapper avec TTL par défaut configurable."""

    def __init__(self, backend: CacheBackend, default_ttl: int = 3600):
        self.backend = backend
        self.default_ttl = default_ttl

    def get(self, key: CacheKey) -> CacheEntry | None:
        return self.backend.get(key)

    def set(self, key: CacheKey, value, metadata: dict | None = None) -> None:
        metadata = metadata or {}
        metadata.setdefault("ttl_seconds", self.default_ttl)
        self.backend.set(key, value, metadata)

    def delete(self, key: CacheKey) -> bool:
        return self.backend.delete(key)

    def clear(self) -> None:
        self.backend.clear()

    def exists(self, key: CacheKey) -> bool:
        return self.backend.exists(key)

    def stats(self) -> dict:
        return self.backend.stats()

# Cache avec expiration après 1 heure
backend = CacheWithTTL(RedisCache(redis_client), default_ttl=3600)
catnip = Catnip(cache=CatnipCache(backend=backend))

Logging Personnalisé

Interface de Logging

L'hôte peut fournir un logger personnalisé pour intercepter les logs de Catnip :

import logging
from catnip import Catnip

# Logger personnalisé avec format JSON
class JSONLogger:
    def __init__(self, name: str):
        self.logger = logging.getLogger(name)
        handler = logging.StreamHandler()
        handler.setFormatter(logging.Formatter('{"level": "%(levelname)s", "msg": "%(message)s"}'))
        self.logger.addHandler(handler)
        self.logger.setLevel(logging.INFO)

    def info(self, msg: str) -> None:
        self.logger.info(msg)

    def warning(self, msg: str) -> None:
        self.logger.warning(msg)

    def error(self, msg: str) -> None:
        self.logger.error(msg)

    def print(self, *args, sep=' ') -> None:
        msg = sep.join(str(arg) for arg in args)
        self.logger.debug(msg)

# Utilisation
from catnip.context import Context

logger = JSONLogger("myapp.catnip")
ctx = Context(logger=logger)
catnip = Catnip(context=ctx)

# Le logger est utilisé par Catnip pour print(), warnings, etc.
catnip.parse('print("hello")')
catnip.execute()

Logging avec Contexte

class ContextualLogger:
    """Logger qui ajoute automatiquement du contexte (user_id, request_id, etc.)."""

    def __init__(self, base_logger, context: dict):
        self.base = base_logger
        self.context = context

    def _format(self, msg: str) -> str:
        ctx = " ".join(f"{k}={v}" for k, v in self.context.items())
        return f"[{ctx}] {msg}"

    def print(self, *args, sep=' ') -> None:
        msg = sep.join(str(arg) for arg in args)
        self.base.debug(self._format(msg))

    def info(self, *args, sep=' ') -> None:
        msg = sep.join(str(arg) for arg in args)
        self.base.info(self._format(msg))

    def error(self, *args, sep=' ') -> None:
        msg = sep.join(str(arg) for arg in args)
        self.base.error(self._format(msg))

    # … autres méthodes (warning, etc.)

# Utilisation
from catnip.context import Context

base_logger = logging.getLogger("catnip")
logger = ContextualLogger(base_logger, {"user_id": 123, "request_id": "abc"})
ctx = Context(logger=logger)
catnip = Catnip(context=ctx)

# Logs: "[user_id=123 request_id=abc] Parsing script.cat"

Gestion des Exceptions

Les exceptions Catnip héritent de CatnipError et sont définies dans catnip.exc :

from catnip import Catnip
from catnip.exc import CatnipError, CatnipSyntaxError, CatnipRuntimeError

catnip = Catnip()

try:
    catnip.parse("invalid syntax !")
    catnip.execute()
except CatnipSyntaxError as e:
    print(f"Erreur de parsing : {e}")
except CatnipRuntimeError as e:
    print(f"Erreur d'exécution : {e}")
except CatnipError as e:
    print(f"Erreur Catnip : {e}")

Hiérarchie : CatnipError > CatnipSyntaxError, CatnipSemanticError, CatnipRuntimeError, CatnipNameError, CatnipTypeError, CatnipPatternError, CatnipArityError.

Exposition de Services Hôte

Injection via Context.globals

L'hôte peut injecter ses services dans les globals du contexte :

from catnip import Catnip
from catnip.context import Context

class HostServices:
    def __init__(self, redis_client):
        self.redis = redis_client

    def cache_get(self, key: str):
        return self.redis.get(key)

    def cache_set(self, key: str, value, ttl: int = 3600):
        self.redis.setex(key, ttl, value)

# Injection dans les globals
ctx = Context()
ctx.globals['host'] = HostServices(redis_client)
catnip = Catnip(context=ctx)

# Les scripts Catnip ont accès aux services via `host`
catnip.parse("""
data = host.cache_get("users")
host.cache_set("result", data, 3600)
""")
catnip.execute()

Résumé

Service Interface Exemples
Cache CacheBackend Redis, diskcache, memcached
Logging Context(logger=...) JSON logger, Syslog
Services context.globals['x'] = … DB, API, notifications

Exemple Minimal

from catnip import Catnip
from catnip.cachesys import CatnipCache
import redis

redis_client = redis.Redis()
backend = RedisCache(redis_client, prefix="myapp:")

catnip = Catnip(cache=CatnipCache(backend=backend))

catnip.parse("x = 2 + 2")   # compilé et mis en cache
catnip.execute()

catnip.parse("x = 2 + 2")   # récupéré depuis le cache
catnip.execute()