Initial artdag-common shared library
Shared components for L1 and L2 servers: - Jinja2 template system with base template and components - Middleware for auth and content negotiation - Pydantic models for requests/responses - Utility functions for pagination, media, formatting - Constants for Tailwind/HTMX/Cytoscape CDNs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
160
artdag_common/rendering.py
Normal file
160
artdag_common/rendering.py
Normal file
@@ -0,0 +1,160 @@
|
||||
"""
|
||||
Jinja2 template rendering system for Art-DAG servers.
|
||||
|
||||
Provides a unified template environment that can load from:
|
||||
1. The shared artdag_common/templates directory
|
||||
2. App-specific template directories
|
||||
|
||||
Usage:
|
||||
from artdag_common import create_jinja_env, render
|
||||
|
||||
# In app initialization
|
||||
templates = create_jinja_env("app/templates")
|
||||
|
||||
# In route handler
|
||||
return render(templates, "runs/detail.html", request, run=run, user=user)
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
from fastapi import Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from jinja2 import Environment, ChoiceLoader, FileSystemLoader, PackageLoader, select_autoescape
|
||||
|
||||
from .constants import (
|
||||
TAILWIND_CDN,
|
||||
HTMX_CDN,
|
||||
CYTOSCAPE_CDN,
|
||||
DAGRE_CDN,
|
||||
CYTOSCAPE_DAGRE_CDN,
|
||||
TAILWIND_CONFIG,
|
||||
NODE_COLORS,
|
||||
STATUS_COLORS,
|
||||
)
|
||||
|
||||
|
||||
def create_jinja_env(*template_dirs: Union[str, Path]) -> Environment:
|
||||
"""
|
||||
Create a Jinja2 environment with the shared templates and optional app-specific dirs.
|
||||
|
||||
Args:
|
||||
*template_dirs: Additional template directories to search (app-specific)
|
||||
|
||||
Returns:
|
||||
Configured Jinja2 Environment
|
||||
|
||||
Example:
|
||||
env = create_jinja_env("/app/templates", "/app/custom")
|
||||
"""
|
||||
loaders = []
|
||||
|
||||
# Add app-specific directories first (higher priority)
|
||||
for template_dir in template_dirs:
|
||||
path = Path(template_dir)
|
||||
if path.exists():
|
||||
loaders.append(FileSystemLoader(str(path)))
|
||||
|
||||
# Add shared templates from this package (lower priority, fallback)
|
||||
loaders.append(PackageLoader("artdag_common", "templates"))
|
||||
|
||||
env = Environment(
|
||||
loader=ChoiceLoader(loaders),
|
||||
autoescape=select_autoescape(["html", "xml"]),
|
||||
trim_blocks=True,
|
||||
lstrip_blocks=True,
|
||||
)
|
||||
|
||||
# Add global context available to all templates
|
||||
env.globals.update({
|
||||
"TAILWIND_CDN": TAILWIND_CDN,
|
||||
"HTMX_CDN": HTMX_CDN,
|
||||
"CYTOSCAPE_CDN": CYTOSCAPE_CDN,
|
||||
"DAGRE_CDN": DAGRE_CDN,
|
||||
"CYTOSCAPE_DAGRE_CDN": CYTOSCAPE_DAGRE_CDN,
|
||||
"TAILWIND_CONFIG": TAILWIND_CONFIG,
|
||||
"NODE_COLORS": NODE_COLORS,
|
||||
"STATUS_COLORS": STATUS_COLORS,
|
||||
})
|
||||
|
||||
# Add custom filters
|
||||
env.filters["truncate_hash"] = truncate_hash
|
||||
env.filters["format_size"] = format_size
|
||||
env.filters["status_color"] = status_color
|
||||
|
||||
return env
|
||||
|
||||
|
||||
def render(
|
||||
env: Environment,
|
||||
template_name: str,
|
||||
request: Request,
|
||||
status_code: int = 200,
|
||||
**context: Any,
|
||||
) -> HTMLResponse:
|
||||
"""
|
||||
Render a template to an HTMLResponse.
|
||||
|
||||
Args:
|
||||
env: Jinja2 environment
|
||||
template_name: Template file path (e.g., "runs/detail.html")
|
||||
request: FastAPI request object
|
||||
status_code: HTTP status code (default 200)
|
||||
**context: Template context variables
|
||||
|
||||
Returns:
|
||||
HTMLResponse with rendered content
|
||||
"""
|
||||
template = env.get_template(template_name)
|
||||
html = template.render(request=request, **context)
|
||||
return HTMLResponse(html, status_code=status_code)
|
||||
|
||||
|
||||
def render_fragment(
|
||||
env: Environment,
|
||||
template_name: str,
|
||||
**context: Any,
|
||||
) -> str:
|
||||
"""
|
||||
Render a template fragment to a string (for HTMX partial updates).
|
||||
|
||||
Args:
|
||||
env: Jinja2 environment
|
||||
template_name: Template file path
|
||||
**context: Template context variables
|
||||
|
||||
Returns:
|
||||
Rendered HTML string
|
||||
"""
|
||||
template = env.get_template(template_name)
|
||||
return template.render(**context)
|
||||
|
||||
|
||||
# Custom Jinja2 filters
|
||||
|
||||
def truncate_hash(value: str, length: int = 16) -> str:
|
||||
"""Truncate a hash to specified length with ellipsis."""
|
||||
if not value:
|
||||
return ""
|
||||
if len(value) <= length:
|
||||
return value
|
||||
return f"{value[:length]}..."
|
||||
|
||||
|
||||
def format_size(size_bytes: Optional[int]) -> str:
|
||||
"""Format file size in human-readable form."""
|
||||
if size_bytes is None:
|
||||
return "Unknown"
|
||||
if size_bytes < 1024:
|
||||
return f"{size_bytes} B"
|
||||
elif size_bytes < 1024 * 1024:
|
||||
return f"{size_bytes / 1024:.1f} KB"
|
||||
elif size_bytes < 1024 * 1024 * 1024:
|
||||
return f"{size_bytes / (1024 * 1024):.1f} MB"
|
||||
else:
|
||||
return f"{size_bytes / (1024 * 1024 * 1024):.1f} GB"
|
||||
|
||||
|
||||
def status_color(status: str) -> str:
|
||||
"""Get Tailwind CSS class for a status."""
|
||||
return STATUS_COLORS.get(status, STATUS_COLORS["pending"])
|
||||
Reference in New Issue
Block a user