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:
giles
2026-01-11 07:07:59 +00:00
commit fd97812e3d
21 changed files with 1905 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
"""
Shared Pydantic models for Art-DAG servers.
"""
from .requests import (
PaginationParams,
PublishRequest,
StorageConfigRequest,
MetadataUpdateRequest,
)
from .responses import (
PaginatedResponse,
ErrorResponse,
SuccessResponse,
)
__all__ = [
"PaginationParams",
"PublishRequest",
"StorageConfigRequest",
"MetadataUpdateRequest",
"PaginatedResponse",
"ErrorResponse",
"SuccessResponse",
]

View File

@@ -0,0 +1,74 @@
"""
Request models shared across L1 and L2 servers.
"""
from typing import Optional, List, Dict, Any
from pydantic import BaseModel, Field
from ..constants import DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE
class PaginationParams(BaseModel):
"""Common pagination parameters."""
page: int = Field(default=1, ge=1, description="Page number (1-indexed)")
limit: int = Field(
default=DEFAULT_PAGE_SIZE,
ge=1,
le=MAX_PAGE_SIZE,
description="Items per page"
)
@property
def offset(self) -> int:
"""Calculate offset for database queries."""
return (self.page - 1) * self.limit
class PublishRequest(BaseModel):
"""Request to publish content to L2/storage."""
name: str = Field(..., min_length=1, max_length=255)
description: Optional[str] = Field(default=None, max_length=2000)
tags: List[str] = Field(default_factory=list)
storage_id: Optional[str] = Field(default=None, description="Target storage provider")
class MetadataUpdateRequest(BaseModel):
"""Request to update content metadata."""
name: Optional[str] = Field(default=None, max_length=255)
description: Optional[str] = Field(default=None, max_length=2000)
tags: Optional[List[str]] = Field(default=None)
metadata: Optional[Dict[str, Any]] = Field(default=None)
class StorageConfigRequest(BaseModel):
"""Request to configure a storage provider."""
provider_type: str = Field(..., description="Provider type (pinata, web3storage, local, etc.)")
name: str = Field(..., min_length=1, max_length=100)
api_key: Optional[str] = Field(default=None)
api_secret: Optional[str] = Field(default=None)
endpoint: Optional[str] = Field(default=None)
config: Optional[Dict[str, Any]] = Field(default_factory=dict)
is_default: bool = Field(default=False)
class RecipeRunRequest(BaseModel):
"""Request to run a recipe."""
recipe_id: str = Field(..., description="Recipe content hash or ID")
inputs: Dict[str, str] = Field(..., description="Map of input name to content hash")
features: List[str] = Field(
default=["beats", "energy"],
description="Analysis features to extract"
)
class PlanRequest(BaseModel):
"""Request to generate an execution plan."""
recipe_yaml: str = Field(..., description="Recipe YAML content")
input_hashes: Dict[str, str] = Field(..., description="Map of input name to content hash")
features: List[str] = Field(default=["beats", "energy"])
class ExecutePlanRequest(BaseModel):
"""Request to execute a generated plan."""
plan_json: str = Field(..., description="JSON-serialized execution plan")
run_id: Optional[str] = Field(default=None, description="Optional run ID for tracking")

View File

@@ -0,0 +1,96 @@
"""
Response models shared across L1 and L2 servers.
"""
from typing import Optional, List, Dict, Any, Generic, TypeVar
from pydantic import BaseModel, Field
T = TypeVar("T")
class PaginatedResponse(BaseModel, Generic[T]):
"""Generic paginated response."""
data: List[Any] = Field(default_factory=list)
pagination: Dict[str, Any] = Field(default_factory=dict)
@classmethod
def create(
cls,
items: List[Any],
page: int,
limit: int,
total: int,
) -> "PaginatedResponse":
"""Create a paginated response."""
return cls(
data=items,
pagination={
"page": page,
"limit": limit,
"total": total,
"has_more": page * limit < total,
"total_pages": (total + limit - 1) // limit,
}
)
class ErrorResponse(BaseModel):
"""Standard error response."""
error: str = Field(..., description="Error message")
detail: Optional[str] = Field(default=None, description="Detailed error info")
code: Optional[str] = Field(default=None, description="Error code")
class SuccessResponse(BaseModel):
"""Standard success response."""
success: bool = Field(default=True)
message: Optional[str] = Field(default=None)
data: Optional[Dict[str, Any]] = Field(default=None)
class RunStatus(BaseModel):
"""Run execution status."""
run_id: str
status: str = Field(..., description="pending, running, completed, failed")
recipe: Optional[str] = None
plan_id: Optional[str] = None
output_hash: Optional[str] = None
output_ipfs_cid: Optional[str] = None
total_steps: int = 0
cached_steps: int = 0
completed_steps: int = 0
error: Optional[str] = None
class CacheItemResponse(BaseModel):
"""Cached content item response."""
content_hash: str
media_type: Optional[str] = None
size: Optional[int] = None
name: Optional[str] = None
description: Optional[str] = None
tags: List[str] = Field(default_factory=list)
ipfs_cid: Optional[str] = None
created_at: Optional[str] = None
class RecipeResponse(BaseModel):
"""Recipe response."""
recipe_id: str
name: str
description: Optional[str] = None
inputs: List[Dict[str, Any]] = Field(default_factory=list)
outputs: List[str] = Field(default_factory=list)
node_count: int = 0
created_at: Optional[str] = None
class StorageProviderResponse(BaseModel):
"""Storage provider configuration response."""
storage_id: str
provider_type: str
name: str
is_default: bool = False
is_connected: bool = False
usage_bytes: Optional[int] = None
pin_count: int = 0