05/18/2026 Catchup - linksync project work and TicTacToe evaluations on different coding LLMs with OpenCode.

This commit is contained in:
DavidSaylor
2026-05-18 19:55:48 -05:00
parent aed69afdfd
commit c5d3912070
544 changed files with 140434 additions and 364 deletions

View File

@@ -1,36 +1,46 @@
"""
LinkSyncServer - Link CRUD Endpoints
LinkSyncServer - Link CRUD Endpoints with SQLAlchemy
"""
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from fastapi import APIRouter, Depends, HTTPException, status, Query, Request
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy import func, or_
from typing import List, Optional
import uuid
import logging
import hashlib
from models.base import Base, Bookmark, User, AuditLog, get_engine, create_engine
from pydantic import BaseModel, Field
import os
router = APIRouter(prefix="/api/links", tags=["Links"])
# Logging
logger = logging.getLogger(__name__)
class BookmarkCreate(BaseModel):
url: str
title: str
description: Optional[str] = None
notes: Optional[str] = None
tags: Optional[List[str]] = None
favicon_url: Optional[str] = None
path: Optional[str] = None
visit_count: int = 0
is_bookmarked: bool = False
url: str = Field(..., description="Bookmark URL")
title: str = Field(..., min_length=1, max_length=255, description="Bookmark title")
description: Optional[str] = Field(None, max_length=500, description="Optional description")
notes: Optional[str] = Field(None, max_length=2000, description="Optional notes")
tags: Optional[List[str]] = Field(default_factory=list, description="List of tag names")
favicon_url: Optional[str] = Field(None, max_length=512, description="Favicon URL")
path: Optional[str] = Field(None, max_length=512, description="Folder path")
visit_count: int = Field(ge=0, description="Visit counter")
is_bookmarked: bool = Field(default=False, description="Bookmark flag")
class BookmarkUpdate(BaseModel):
url: Optional[str] = None
title: Optional[str] = None
description: Optional[str] = None
notes: Optional[str] = None
tags: Optional[List[str]] = None
favicon_url: Optional[str] = None
path: Optional[str] = None
visit_count: Optional[int] = None
url: Optional[str] = Field(None, description="New URL")
title: Optional[str] = Field(None, min_length=1, max_length=255)
description: Optional[str] = Field(None, max_length=500)
notes: Optional[str] = Field(None, max_length=2000)
tags: Optional[List[str]] = Field(None)
favicon_url: Optional[str] = Field(None, max_length=512)
path: Optional[str] = Field(None, max_length=512)
visit_count: Optional[int] = Field(None, ge=0)
is_bookmarked: Optional[bool] = None
@@ -48,128 +58,301 @@ class BookmarkResponse(BaseModel):
visit_count: int
is_bookmarked: bool
source_set_id: Optional[str]
user_id: Optional[str]
def get_db():
def get_db_session():
"""Get database session."""
from models.base import get_engine
db = get_engine()
return db
try:
return sessionmaker(get_engine())()
except Exception:
return None
def mock_create_bookmark(data: BookmarkCreate) -> dict:
"""Create bookmark (mock implementation for demo)."""
bookmark = {
"id": str(uuid.uuid4()),
"url": data.url,
"title": data.title,
"description": data.description,
"notes": data.notes,
"tags": data.tags or [],
"favicon_url": data.favicon_url,
"path": data.path,
"created_at": "2026-05-11T00:00:00Z",
"updated_at": "2026-05-11T00:00:00Z",
"visit_count": data.visit_count,
"is_bookmarked": data.is_bookmarked,
"source_set_id": None
}
return bookmark
def get_current_user(request: Request):
"""Get current authenticated user."""
SECRET_KEY = os.environ.get("SECRET_KEY")
auth_header = request.headers.get("Authorization") or request.headers.get("authorization")
if auth_header and auth_header.startswith("Bearer "):
token = auth_header[7:]
try:
import jwt
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
return {"username": payload.get("sub"), "id": payload.get("sub")}
except Exception:
pass
def mock_get_bookmarks() -> List[dict]:
"""Get all bookmarks (mock implementation)."""
return [
{
"id": str(uuid.uuid4()),
"url": "https://example.com",
"title": "Example",
"description": "An example website",
"notes": "",
"tags": ["example", "demo"],
"favicon_url": None,
"path": "/Home",
"created_at": "2026-05-11T00:00:00Z",
"updated_at": "2026-05-11T00:00:00Z",
"visit_count": 0,
"is_bookmarked": False,
"source_set_id": None
}
]
def mock_get_bookmark(bookmark_id: str) -> dict | None:
"""Get single bookmark by ID."""
# Mock implementation
if bookmark_id == "mock-id":
return {
"id": "mock-id",
"url": "https://example.com",
"title": "Example",
"description": "An example website",
"notes": "",
"tags": ["example", "demo"],
"favicon_url": None,
"path": "/Home",
"created_at": "2026-05-11T00:00:00Z",
"updated_at": "2026-05-11T00:00:00Z",
"visit_count": 0,
"is_bookmarked": False,
"source_set_id": None
}
return None
def mock_update_bookmark(bookmark_id: str, data: BookmarkUpdate) -> dict | None:
"""Update bookmark."""
# Mock implementation
return mock_get_bookmark(bookmark_id)
def mock_delete_bookmark(bookmark_id: str) -> bool:
"""Delete bookmark."""
return True
return {"username": "guest"}
@router.get("/", response_model=List[BookmarkResponse])
async def list_bookmarks(limit: int = 20, offset: int = 0):
"""List all bookmarks."""
bookmarks = mock_get_bookmarks()
return bookmarks[offset:offset + limit]
async def list_bookmarks(
limit: int = Query(20, le=100, ge=1, description="Number of results per page"),
offset: int = Query(0, ge=0, description="Offset for pagination"),
search: Optional[str] = Query(None, description="Search query"),
tags_filter: Optional[List[str]] = Query(None, description="Filter by tags"),
path_filter: Optional[str] = Query(None, description="Filter by folder path")
):
"""List all bookmarks with optional filters."""
db = get_db_session()
if not db:
return []
query = Bookmark.query
# Search filter
if search:
query = query.filter((Bookmark.title.contains(search)) |
(Bookmark.description.contains(search)) |
(Bookmark.url.contains(search)))
# Tag filter
if tags_filter:
or_clause = or_(*[Bookmark.tags.contains(tag) for tag in tags_filter])
query = query.filter(or_clause)
# Path filter
if path_filter:
query = query.filter(Bookmark.path.contains(path_filter))
bookmarks = query.limit(limit).offset(offset).all()
return bookmarks
@router.get("/{bookmark_id}", response_model=BookmarkResponse)
async def get_bookmark(bookmark_id: str):
"""Get bookmark by ID."""
bookmark = mock_get_bookmark(bookmark_id)
db = get_db_session()
if not db:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Bookmark not found")
bookmark = db.query(Bookmark).filter(Bookmark.id == bookmark_id).first()
if not bookmark:
raise HTTPException(status_code=404, detail="Bookmark not found")
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Bookmark not found")
return bookmark
@router.post("/", response_model=BookmarkResponse, status_code=status.HTTP_201_CREATED)
async def create_bookmark(data: BookmarkCreate):
async def create_bookmark(data: BookmarkCreate, request: Request):
"""Create new bookmark."""
bookmark = mock_create_bookmark(data)
db = get_db_session()
if not db:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Database unavailable")
bookmark = Bookmark(
url=data.url,
title=data.title,
description=data.description,
notes=data.notes,
tags=data.tags or [],
favicon_url=data.favicon_url,
path=data.path,
visit_count=data.visit_count,
is_bookmarked=data.is_bookmarked
)
bookmark_id = f"{data.url[:20]}-{uuid.uuid4()[:8]}"
bookmark = db.add(bookmark)
db.commit()
db.refresh(bookmark)
# Get user for audit log
user = get_current_user(request)
# Create audit log (optional)
try:
audit = AuditLog(
action="create",
entity_type="Bookmark",
entity_id=bookmark_id,
old_value=None,
new_value=bookmark.dict(),
user_id=user.get("id")
)
db.add(audit)
db.commit()
except Exception:
pass
return bookmark
@router.put("/{bookmark_id}", response_model=BookmarkResponse)
async def update_bookmark(
bookmark_id: str,
data: BookmarkUpdate
data: BookmarkUpdate,
request: Request
):
"""Update bookmark."""
bookmark = mock_update_bookmark(bookmark_id, data)
db = get_db_session()
if not db:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Database unavailable")
bookmark = db.query(Bookmark).filter(Bookmark.id == bookmark_id).first()
if not bookmark:
raise HTTPException(status_code=404, detail="Bookmark not found")
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Bookmark not found")
# Update fields
for field_name, value in data.dict().items():
if value is not None:
setattr(bookmark, field_name, value)
db.commit()
db.refresh(bookmark)
# Get user for audit log
user = get_current_user(request)
# Create audit log
try:
old_data = Bookmark(id=bookmark_id, url=bookmark.url, title=bookmark.title).dict()
audit = AuditLog(
action="update",
entity_type="Bookmark",
entity_id=bookmark_id,
old_value=old_data,
new_value=bookmark.dict(),
user_id=user.get("id")
)
db.add(audit)
db.commit()
except Exception:
pass
return bookmark
@router.delete("/{bookmark_id}", response_model=dict)
async def delete_bookmark(bookmark_id: str):
async def delete_bookmark(bookmark_id: str, request: Request):
"""Delete bookmark."""
success = mock_delete_bookmark(bookmark_id)
if not success:
raise HTTPException(status_code=404, detail="Bookmark not found")
return {"message": "Bookmark deleted successfully"}
db = get_db_session()
if not db:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Database unavailable")
bookmark = db.query(Bookmark).filter(Bookmark.id == bookmark_id).first()
if not bookmark:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Bookmark not found")
db.delete(bookmark)
db.commit()
# Get user for audit log
user = get_current_user(request)
# Create audit log
try:
audit = AuditLog(
action="delete",
entity_type="Bookmark",
entity_id=bookmark_id,
old_value=bookmark.dict(),
new_value=None,
user_id=user.get("id")
)
db.add(audit)
db.commit()
except Exception:
pass
return {"message": "Bookmark deleted successfully", "deleted_id": bookmark_id}
@router.post("/{bookmark_id}/tags")
async def add_tags(bookmark_id: str, tags: List[str], request: Request):
"""Add tags to bookmark."""
db = get_db_session()
if not db:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Database unavailable")
bookmark = db.query(Bookmark).filter(Bookmark.id == bookmark_id).first()
if not bookmark:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Bookmark not found")
for tag in tags:
if tag.lower() not in [t.lower() for t in bookmark.tags]:
bookmark.tags.append(tag)
db.commit()
db.refresh(bookmark)
return bookmark
@router.delete("/{bookmark_id}/tags")
async def remove_tags(bookmark_id: str, tags_to_remove: List[str], request: Request):
"""Remove tags from bookmark."""
db = get_db_session()
if not db:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Database unavailable")
bookmark = db.query(Bookmark).filter(Bookmark.id == bookmark_id).first()
if not bookmark:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Bookmark not found")
bookmark.tags = [t for t in bookmark.tags if t.lower() not in [tag.lower() for tag in tags_to_remove]]
db.commit()
db.refresh(bookmark)
return bookmark
@router.get("/{bookmark_id}/stats")
async def get_bookmark_stats(bookmark_id: str, request: Request):
"""Get bookmark statistics."""
db = get_db_session()
if not db:
return {}
bookmark = db.query(Bookmark).filter(Bookmark.id == bookmark_id).first()
if not bookmark:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Bookmark not found")
# Get visit count
visits = db.query("SELECT COUNT(*) FROM visits WHERE bookmark_id = :bookmark_id")
visit_count = visits.execute({"bookmark_id": bookmark_id})
return {
"bookmark_id": bookmark_id,
"visit_count": visit_count[0][0],
"last_visited": visits.execute({"bookmark_id": bookmark_id})
}
# Audit log helper (optional)
def create_audit_log(action: str, entity_type: str, entity_id: str, old_value: dict, new_value: dict):
"""Create audit log entry."""
db = get_db_session()
if not db:
return
try:
audit = AuditLog(
action=action,
entity_type=entity_type,
entity_id=entity_id,
old_value=old_value,
new_value=new_value,
ip_address=request.client.host if hasattr(request, 'client') and hasattr(request.client, 'host') else None
)
db.add(audit)
db.commit()
except Exception:
pass