Build custom loaders to load templates from databases, APIs, or other sources.
Loader Protocol
Implement two methods:
class Loader:
def get_source(self, name: str) -> tuple[str, str | None]:
"""Return (source, filename) for template."""
...
def list_templates(self) -> list[str]:
"""Return list of all template names."""
...
Database Loader
Load templates from a database:
from kida import Environment, TemplateNotFoundError
class DatabaseLoader:
def __init__(self, connection):
self.conn = connection
def get_source(self, name: str) -> tuple[str, str | None]:
row = self.conn.execute(
"SELECT source FROM templates WHERE name = ?",
(name,)
).fetchone()
if not row:
raise TemplateNotFoundError(f"Template '{name}' not found")
# Return (source, filename for error messages)
return row[0], f"db://{name}"
def list_templates(self) -> list[str]:
rows = self.conn.execute(
"SELECT name FROM templates"
).fetchall()
return sorted(row[0] for row in rows)
# Usage
env = Environment(loader=DatabaseLoader(db_connection))
template = env.get_template("page.html")
Redis Loader
Load templates from Redis:
import redis
from kida import TemplateNotFoundError
class RedisLoader:
def __init__(self, host="localhost", port=6379, prefix="templates:"):
self.client = redis.Redis(host=host, port=port)
self.prefix = prefix
def get_source(self, name: str) -> tuple[str, str | None]:
key = f"{self.prefix}{name}"
source = self.client.get(key)
if source is None:
raise TemplateNotFoundError(f"Template '{name}' not found")
return source.decode("utf-8"), f"redis://{key}"
def list_templates(self) -> list[str]:
pattern = f"{self.prefix}*"
keys = self.client.keys(pattern)
return sorted(k.decode().removeprefix(self.prefix) for k in keys)
# Usage
env = Environment(loader=RedisLoader())
HTTP Loader
Load templates from a remote server:
import httpx
from kida import TemplateNotFoundError
class HTTPLoader:
def __init__(self, base_url: str):
self.base_url = base_url.rstrip("/")
self.client = httpx.Client()
def get_source(self, name: str) -> tuple[str, str | None]:
url = f"{self.base_url}/{name}"
try:
response = self.client.get(url)
response.raise_for_status()
except httpx.HTTPError:
raise TemplateNotFoundError(f"Template '{name}' not found")
return response.text, url
def list_templates(self) -> list[str]:
# Could fetch index from server
return []
# Usage
env = Environment(loader=HTTPLoader("https://templates.example.com"))
Chained Loader
Try multiple loaders in order:
from kida import TemplateNotFoundError
class ChainLoader:
def __init__(self, loaders: list):
self.loaders = loaders
def get_source(self, name: str) -> tuple[str, str | None]:
for loader in self.loaders:
try:
return loader.get_source(name)
except TemplateNotFoundError:
continue
raise TemplateNotFoundError(f"Template '{name}' not found in any loader")
def list_templates(self) -> list[str]:
templates = set()
for loader in self.loaders:
templates.update(loader.list_templates())
return sorted(templates)
# Usage: Try database first, then filesystem
env = Environment(
loader=ChainLoader([
DatabaseLoader(db),
FileSystemLoader("templates/"),
])
)
Prefix Loader
Route templates by prefix:
class PrefixLoader:
def __init__(self, loaders: dict):
self.loaders = loaders # {"prefix/": loader}
def get_source(self, name: str) -> tuple[str, str | None]:
for prefix, loader in self.loaders.items():
if name.startswith(prefix):
template_name = name[len(prefix):]
return loader.get_source(template_name)
raise TemplateNotFoundError(f"No loader for '{name}'")
def list_templates(self) -> list[str]:
templates = []
for prefix, loader in self.loaders.items():
templates.extend(
prefix + t for t in loader.list_templates()
)
return sorted(templates)
# Usage
env = Environment(
loader=PrefixLoader({
"db/": DatabaseLoader(db),
"files/": FileSystemLoader("templates/"),
})
)
# Loads from database
env.get_template("db/page.html")
# Loads from filesystem
env.get_template("files/base.html")
Caching Layer
Add caching to any loader:
from functools import lru_cache
class CachedLoader:
def __init__(self, loader, maxsize=128):
self.loader = loader
self._get_source = lru_cache(maxsize=maxsize)(
self._get_source_uncached
)
def _get_source_uncached(self, name: str):
return self.loader.get_source(name)
def get_source(self, name: str) -> tuple[str, str | None]:
return self._get_source(name)
def list_templates(self) -> list[str]:
return self.loader.list_templates()
def clear_cache(self):
self._get_source.cache_clear()
# Usage
env = Environment(
loader=CachedLoader(DatabaseLoader(db), maxsize=256)
)
Thread Safety
Loaders should be thread-safe:
import threading
class ThreadSafeLoader:
def __init__(self, loader):
self.loader = loader
self._lock = threading.Lock()
def get_source(self, name: str) -> tuple[str, str | None]:
with self._lock:
return self.loader.get_source(name)
def list_templates(self) -> list[str]:
with self._lock:
return self.loader.list_templates()
Best Practices
Raise TemplateNotFoundError
from kida import TemplateNotFoundError
def get_source(self, name):
if not self._exists(name):
# ✅ Raise proper exception
raise TemplateNotFoundError(f"Template '{name}' not found")
# ❌ Don't return None
# return None
Include Source Location
def get_source(self, name):
source = self._load(name)
# Include meaningful filename for error messages
filename = f"db://{name}" # or full path, URL, etc.
return source, filename
Handle Encoding
def get_source(self, name):
data = self._load_bytes(name)
source = data.decode("utf-8") # Handle encoding
return source, name
See Also
- Loading Templates — FileSystemLoader, DictLoader
- API Reference — Loader protocol
- Architecture — Template compilation pipeline