akif/arclet-boards
Arclet Boards — private Kanban workspaces with AppID, DataK3, shared web and MCP actions.
A remix template · 1 remixes · Remix akif/arclet-boards
app/domain/model.py
"""Entities, validation and access rules, independent of HTTP and storage."""
from dataclasses import dataclass
from datetime import date, datetime, timezone
from enum import Enum
import re
class AppError(Exception):
status = 422
class Unauthenticated(AppError):
status = 401
class NotFound(AppError):
status = 404
class Forbidden(AppError):
status = 403
class Conflict(AppError):
status = 409
class Unavailable(AppError):
status = 503
class Invalid(AppError):
pass
class Role(str, Enum):
OWNER = "owner"
MEMBER = "member"
VIEWER = "viewer"
@dataclass(frozen=True)
class Person:
sub: str
email: str
@dataclass
class Workspace:
id: str
title: str
owner_sub: str
owner_email: str
created_at: str
updated_at: str
version: int = 1
@dataclass
class WorkspaceMembership:
id: str
workspace_id: str
email: str
user_sub: str | None
role: str
invited_by: str
created_at: str
updated_at: str
@dataclass
class Board:
id: str
workspace_id: str
title: str
description: str
color: str
owner_sub: str
owner_email: str
created_at: str
updated_at: str
version: int = 1
@dataclass
class BoardMembership:
id: str
board_id: str
email: str
user_sub: str | None
role: str
invited_by: str
created_at: str
updated_at: str
@dataclass
class BoardList:
id: str
board_id: str
title: str
position: int
is_done: bool
created_at: str
updated_at: str
version: int = 1
@dataclass
class Card:
id: str
board_id: str
list_id: str
title: str
description: str
assignee: str | None
due_date: str | None
position: int
created_by: str
created_at: str
updated_at: str
version: int = 1
@dataclass
class CardLabel:
id: str
board_id: str
card_id: str
name: str
color: str
created_at: str
@dataclass
class CardActivity:
id: str
board_id: str
card_id: str
actor_sub: str
actor_email: str
action: str
changes_json: str
created_at: str
@dataclass
class CardChecklistItem:
id: str
board_id: str
card_id: str
title: str
completed: bool
position: int
created_at: str
updated_at: str
version: int = 1
@dataclass
class CardDependency:
id: str
board_id: str
card_id: str
depends_on_id: str
created_by: str
created_at: str
@dataclass
class CardEmbedding:
id: str
board_id: str
card_id: str
content_hash: str
model: str
dimensions: int
embedding: str
updated_at: str
COLORS = ("indigo", "blue", "teal", "amber", "rose", "violet", "slate")
def now() -> str:
return datetime.now(timezone.utc).isoformat(timespec="microseconds")
def text(value: str, label: str, limit: int = 160, allow_empty: bool = False) -> str:
if not isinstance(value, str):
raise Invalid(f"{label} must be text")
value = value.strip()
if (not value and not allow_empty) or len(value) > limit or "\x00" in value:
raise Invalid(f"{label} must contain {'0' if allow_empty else '1'}–{limit} characters")
return value
def email(value: str) -> str:
value = text(value, "Email", 254).lower()
if not re.fullmatch(r"[^\s@]+@[^\s@]+\.[^\s@]+", value):
raise Invalid("Enter a valid email address")
return value
def color(value: str) -> str:
if value not in COLORS:
raise Invalid("Choose a supported color")
return value
def member_role(value: str) -> str:
if value not in (Role.MEMBER.value, Role.VIEWER.value):
raise Invalid("Choose member or viewer; ownership cannot be reassigned")
return value
def due_date(value: str | None) -> str | None:
if value is None or value == "":
return None
try:
parsed = date.fromisoformat(value)
except (ValueError, TypeError):
raise Invalid("Due date must be YYYY-MM-DD") from None
if parsed.isoformat() != value:
raise Invalid("Due date must be YYYY-MM-DD")
return value
def require_role(role: str | None, permission: str = "view") -> None:
if role is None:
raise NotFound("Resource not found")
if permission == "owner" and role != Role.OWNER.value:
raise Forbidden("Only the owner can manage members")
if permission == "edit" and role == Role.VIEWER.value:
raise Forbidden("Viewers have read-only access")
def check_version(row: dict, expected: int) -> None:
if row["version"] != expected:
raise Conflict("Changed since you opened it. Refresh and try again.")
def overdue(card: dict, is_done: bool, today: date | None = None) -> bool:
return bool(card.get("due_date") and not is_done and
date.fromisoformat(str(card["due_date"])) < (today or datetime.now(timezone.utc).date()))