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>
175 lines
3.7 KiB
Python
175 lines
3.7 KiB
Python
"""
|
|
Content negotiation utilities.
|
|
|
|
Helps determine what response format the client wants.
|
|
"""
|
|
|
|
from enum import Enum
|
|
from typing import Optional
|
|
|
|
from fastapi import Request
|
|
|
|
|
|
class ContentType(Enum):
|
|
"""Response content types."""
|
|
HTML = "text/html"
|
|
JSON = "application/json"
|
|
ACTIVITY_JSON = "application/activity+json"
|
|
XML = "application/xml"
|
|
|
|
|
|
def wants_html(request: Request) -> bool:
|
|
"""
|
|
Check if the client wants HTML response.
|
|
|
|
Returns True if:
|
|
- Accept header contains text/html
|
|
- Accept header contains application/xhtml+xml
|
|
- No Accept header (browser default)
|
|
|
|
Args:
|
|
request: FastAPI request
|
|
|
|
Returns:
|
|
True if HTML is preferred
|
|
"""
|
|
accept = request.headers.get("accept", "")
|
|
|
|
# No accept header usually means browser
|
|
if not accept:
|
|
return True
|
|
|
|
# Check for HTML preferences
|
|
if "text/html" in accept:
|
|
return True
|
|
if "application/xhtml" in accept:
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def wants_json(request: Request) -> bool:
|
|
"""
|
|
Check if the client wants JSON response.
|
|
|
|
Returns True if:
|
|
- Accept header contains application/json
|
|
- Accept header does NOT contain text/html
|
|
- Request has .json suffix (convention)
|
|
|
|
Args:
|
|
request: FastAPI request
|
|
|
|
Returns:
|
|
True if JSON is preferred
|
|
"""
|
|
accept = request.headers.get("accept", "")
|
|
|
|
# Explicit JSON preference
|
|
if "application/json" in accept:
|
|
# But not if HTML is also requested (browsers often send both)
|
|
if "text/html" not in accept:
|
|
return True
|
|
|
|
# Check URL suffix convention
|
|
if request.url.path.endswith(".json"):
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def wants_activity_json(request: Request) -> bool:
|
|
"""
|
|
Check if the client wants ActivityPub JSON-LD response.
|
|
|
|
Used for federation with other ActivityPub servers.
|
|
|
|
Args:
|
|
request: FastAPI request
|
|
|
|
Returns:
|
|
True if ActivityPub format is preferred
|
|
"""
|
|
accept = request.headers.get("accept", "")
|
|
|
|
if "application/activity+json" in accept:
|
|
return True
|
|
if "application/ld+json" in accept:
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def get_preferred_type(request: Request) -> ContentType:
|
|
"""
|
|
Determine the preferred content type from Accept header.
|
|
|
|
Args:
|
|
request: FastAPI request
|
|
|
|
Returns:
|
|
ContentType enum value
|
|
"""
|
|
if wants_activity_json(request):
|
|
return ContentType.ACTIVITY_JSON
|
|
if wants_json(request):
|
|
return ContentType.JSON
|
|
return ContentType.HTML
|
|
|
|
|
|
def is_htmx_request(request: Request) -> bool:
|
|
"""
|
|
Check if this is an HTMX request (partial page update).
|
|
|
|
HTMX requests set the HX-Request header.
|
|
|
|
Args:
|
|
request: FastAPI request
|
|
|
|
Returns:
|
|
True if this is an HTMX request
|
|
"""
|
|
return request.headers.get("HX-Request") == "true"
|
|
|
|
|
|
def get_htmx_target(request: Request) -> Optional[str]:
|
|
"""
|
|
Get the HTMX target element ID.
|
|
|
|
Args:
|
|
request: FastAPI request
|
|
|
|
Returns:
|
|
Target element ID or None
|
|
"""
|
|
return request.headers.get("HX-Target")
|
|
|
|
|
|
def get_htmx_trigger(request: Request) -> Optional[str]:
|
|
"""
|
|
Get the HTMX trigger element ID.
|
|
|
|
Args:
|
|
request: FastAPI request
|
|
|
|
Returns:
|
|
Trigger element ID or None
|
|
"""
|
|
return request.headers.get("HX-Trigger")
|
|
|
|
|
|
def is_ios_request(request: Request) -> bool:
|
|
"""
|
|
Check if request is from iOS device.
|
|
|
|
Useful for video format selection (iOS prefers MP4).
|
|
|
|
Args:
|
|
request: FastAPI request
|
|
|
|
Returns:
|
|
True if iOS user agent detected
|
|
"""
|
|
user_agent = request.headers.get("user-agent", "").lower()
|
|
return "iphone" in user_agent or "ipad" in user_agent
|