Files
mono/artdag/common/README.md
giles 1a74d811f7 Incorporate art-dag-mono repo into artdag/ subfolder
Merges full history from art-dag/mono.git into the monorepo
under the artdag/ directory. Contains: core (DAG engine),
l1 (Celery rendering server), l2 (ActivityPub registry),
common (shared templates/middleware), client (CLI), test (e2e).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

git-subtree-dir: artdag
git-subtree-mainline: 1a179de547
git-subtree-split: 4c2e716558
2026-02-27 09:07:23 +00:00

6.6 KiB

artdag-common

Shared components for Art-DAG L1 (celery) and L2 (activity-pub) servers.

Features

  • Jinja2 Templating: Unified template environment with shared base templates
  • Reusable Components: Cards, tables, pagination, DAG visualization, media preview
  • Authentication Middleware: Cookie and JWT token parsing
  • Content Negotiation: HTML/JSON/ActivityPub format detection
  • Utility Functions: Hash truncation, file size formatting, status colors

Installation

pip install -e /path/to/artdag-common

# Or add to requirements.txt
-e file:../common

Quick Start

from fastapi import FastAPI, Request
from artdag_common import create_jinja_env, render

app = FastAPI()

# Initialize templates with app-specific directory
templates = create_jinja_env("app/templates")

@app.get("/")
async def home(request: Request):
    return render(templates, "home.html", request, title="Home")

Package Structure

artdag_common/
├── __init__.py              # Package exports
├── constants.py             # CDN URLs, colors, configs
├── rendering.py             # Jinja2 environment and helpers
├── middleware/
│   ├── auth.py              # Authentication utilities
│   └── content_negotiation.py  # Accept header parsing
├── models/
│   ├── requests.py          # Shared request models
│   └── responses.py         # Shared response models
├── utils/
│   ├── formatting.py        # Text/date formatting
│   ├── media.py             # Media type detection
│   └── pagination.py        # Pagination helpers
└── templates/
    ├── base.html            # Base layout template
    └── components/
        ├── badge.html       # Status/type badges
        ├── card.html        # Info cards
        ├── dag.html         # Cytoscape DAG visualization
        ├── media_preview.html  # Video/image/audio preview
        ├── pagination.html  # HTMX pagination
        └── table.html       # Styled tables

Jinja2 Templates

Base Template

The base.html template provides:

  • Dark theme with Tailwind CSS
  • HTMX integration
  • Navigation slot
  • Content block
  • Optional Cytoscape.js block
{% extends "base.html" %}

{% block title %}My Page{% endblock %}

{% block content %}
<h1>Hello World</h1>
{% endblock %}

Reusable Components

Card

{% include "components/card.html" %}
<!-- Usage in your template -->
<div class="...card styles...">
    {% block card_title %}Title{% endblock %}
    {% block card_content %}Content{% endblock %}
</div>

Badge

Status and type badges with appropriate colors:

{% from "components/badge.html" import status_badge, type_badge %}

{{ status_badge("completed") }}  <!-- Green -->
{{ status_badge("failed") }}     <!-- Red -->
{{ type_badge("video") }}

DAG Visualization

Interactive Cytoscape.js graph:

{% include "components/dag.html" %}

Requires passing nodes and edges data to template context.

Media Preview

Responsive media preview with format detection:

{% include "components/media_preview.html" %}

Supports video, audio, and image formats.

Pagination

HTMX-powered infinite scroll pagination:

{% include "components/pagination.html" %}

Template Rendering

Full Page Render

from artdag_common import render

@app.get("/runs/{run_id}")
async def run_detail(run_id: str, request: Request):
    run = get_run(run_id)
    return render(templates, "runs/detail.html", request, run=run)

Fragment Render (HTMX)

from artdag_common import render_fragment

@app.get("/runs/{run_id}/status")
async def run_status_fragment(run_id: str):
    run = get_run(run_id)
    html = render_fragment(templates, "components/status.html", status=run.status)
    return HTMLResponse(html)

Authentication Middleware

UserContext

from artdag_common.middleware.auth import UserContext, get_user_from_cookie

@app.get("/profile")
async def profile(request: Request):
    user = get_user_from_cookie(request)
    if not user:
        return RedirectResponse("/login")
    return {"username": user.username, "actor_id": user.actor_id}

Token Parsing

from artdag_common.middleware.auth import get_user_from_header, decode_jwt_claims

@app.get("/api/me")
async def api_me(request: Request):
    user = get_user_from_header(request)
    if not user:
        raise HTTPException(401, "Not authenticated")
    return {"user": user.username}

Content Negotiation

Detect what response format the client wants:

from artdag_common.middleware.content_negotiation import wants_html, wants_json, wants_activity_json

@app.get("/users/{username}")
async def user_profile(username: str, request: Request):
    user = get_user(username)

    if wants_activity_json(request):
        return ActivityPubActor(user)
    elif wants_json(request):
        return user.dict()
    else:
        return render(templates, "users/profile.html", request, user=user)

Constants

CDN URLs

from artdag_common import TAILWIND_CDN, HTMX_CDN, CYTOSCAPE_CDN

# Available in templates as globals:
# {{ TAILWIND_CDN }}
# {{ HTMX_CDN }}
# {{ CYTOSCAPE_CDN }}

Node Colors

from artdag_common import NODE_COLORS

# {
#     "SOURCE": "#3b82f6",    # Blue
#     "EFFECT": "#22c55e",    # Green
#     "OUTPUT": "#a855f7",    # Purple
#     "ANALYSIS": "#f59e0b",  # Amber
#     "_LIST": "#6366f1",     # Indigo
#     "default": "#6b7280",   # Gray
# }

Status Colors

STATUS_COLORS = {
    "completed": "bg-green-600",
    "cached": "bg-blue-600",
    "running": "bg-yellow-600",
    "pending": "bg-gray-600",
    "failed": "bg-red-600",
}

Custom Jinja2 Filters

The following filters are available in all templates:

Filter Usage Description
truncate_hash {{ hash|truncate_hash }} Shorten hash to 16 chars with ellipsis
format_size {{ bytes|format_size }} Format bytes as KB/MB/GB
status_color {{ status|status_color }} Get Tailwind class for status

Example:

<span class="{{ run.status|status_color }}">
    {{ run.status }}
</span>

<code>{{ content_hash|truncate_hash }}</code>

<span>{{ file_size|format_size }}</span>

Development

cd /root/art-dag/common

# Install in development mode
pip install -e .

# Run tests
pytest

Dependencies

  • fastapi>=0.100.0 - Web framework
  • jinja2>=3.1.0 - Templating engine
  • pydantic>=2.0.0 - Data validation