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:
174
artdag_common/middleware/content_negotiation.py
Normal file
174
artdag_common/middleware/content_negotiation.py
Normal file
@@ -0,0 +1,174 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user