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,29 +1,40 @@
"""
LinkSyncServer - Collection CRUD Endpoints
LinkSyncServer - Collection CRUD Endpoints with SQLAlchemy
"""
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException, status, Query, Request
from sqlalchemy.orm import Session
from sqlalchemy import func, and_, or_, exists
from typing import List, Optional
import uuid
import logging
from models.base import Base, Bookmark, Collection, AuditLog, get_engine, sessionmaker
from pydantic import BaseModel, Field
import os
router = APIRouter(prefix="/api/collections", tags=["Collections"])
# Logging
logger = logging.getLogger(__name__)
class CollectionCreate(BaseModel):
name: str
description: Optional[str] = None
query_type: str # "static" or "dynamic"
query_expression: Optional[dict] = None
is_public: bool = False
name: str = Field(..., description="Collection name")
description: Optional[str] = Field(None, max_length=1024, description="Collection description")
query_type: str = Field(default="static", description="Static or dynamic collection")
query_expression: Optional[dict] = Field(None, description="Query expression for dynamic collections")
is_public: bool = Field(default=False, description="Is collection public")
tags: Optional[List[str]] = Field(default_factory=list, description="Collection tags")
class CollectionUpdate(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
query_type: Optional[str] = None
query_expression: Optional[dict] = None
name: Optional[str] = Field(None, max_length=255)
description: Optional[str] = Field(None, max_length=1024)
query_type: Optional[str] = Field(None)
query_expression: Optional[dict] = Field(None)
is_public: Optional[bool] = None
tags: Optional[List[str]] = Field(None)
class CollectionResponse(BaseModel):
@@ -35,135 +46,188 @@ class CollectionResponse(BaseModel):
is_public: bool
created_at: str
updated_at: str
tags: List[str]
def mock_create_collection(data: CollectionCreate) -> CollectionResponse:
"""Create collection (mock implementation)."""
return {
"id": str(uuid.uuid4()),
"name": data.name,
"description": data.description,
"query_type": data.query_type,
"query_expression": data.query_expression,
"is_public": data.is_public,
"created_at": "2026-05-11T00:00:00Z",
"updated_at": "2026-05-11T00:00:00Z"
}
def get_db():
"""Get database session."""
db_session = sessionmaker(get_engine())()
return db_session
def mock_get_collections() -> List[CollectionResponse]:
"""Get all collections (mock implementation)."""
return [
{
"id": str(uuid.uuid4()),
"name": "Work Links",
"description": "Links for work use",
"query_type": "dynamic",
"query_expression": {"operation": "OR", "operands": [{"operation": "TERM", "value": "work"}]},
"is_public": False,
"created_at": "2026-05-11T00:00:00Z",
"updated_at": "2026-05-11T00:00:00Z"
}
]
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
return {"username": "guest"}
def mock_get_collection(collection_id: str) -> CollectionResponse | None:
"""Get collection by ID (mock implementation)."""
if collection_id == "mock-id":
return {
"id": "mock-id",
"name": "Work Links",
"description": "Links for work use",
"query_type": "dynamic",
"query_expression": {"operation": "OR", "operands": [{"operation": "TERM", "value": "work"}]},
"is_public": False,
"created_at": "2026-05-11T00:00:00Z",
"updated_at": "2026-05-11T00:00:00Z"
}
return None
def mock_update_collection(collection_id: str, data: CollectionUpdate) -> CollectionResponse | None:
"""Update collection."""
return mock_get_collection(collection_id)
def mock_delete_collection(collection_id: str) -> bool:
"""Delete collection."""
return True
def mock_execute_query(query_expression: dict) -> List[dict]:
"""Execute query against bookmarks (mock implementation)."""
return [
{
"id": str(uuid.uuid4()),
"url": "https://example.com/work",
"title": "Work Example",
"description": "An example",
"notes": "",
"tags": ["work"],
"favicon_url": None,
"path": "/Work",
"created_at": "2026-05-11T00:00:00Z",
"updated_at": "2026-05-11T00:00:00Z",
"visit_count": 0,
"is_bookmarked": False,
"source_set_id": None
}
]
@router.get("/", response_model=List[CollectionResponse])
async def list_collections():
"""List all collections."""
return mock_get_collections()
@router.get("/{collection_id}", response_model=CollectionResponse)
async def get_collection(collection_id: str):
"""Get collection by ID."""
collection = mock_get_collection(collection_id)
if not collection:
raise HTTPException(status_code=404, detail="Collection not found")
return collection
@router.post("/", response_model=CollectionResponse, status_code=status.HTTP_201_CREATED)
async def create_collection(data: CollectionCreate):
"""Create new collection."""
collection = mock_create_collection(data)
return collection
@router.put("/{collection_id}", response_model=CollectionResponse)
async def update_collection(
collection_id: str,
data: CollectionUpdate
):
"""Update collection."""
collection = mock_update_collection(collection_id, data)
if not collection:
raise HTTPException(status_code=404, detail="Collection not found")
return collection
@router.delete("/{collection_id}", response_model=dict)
async def delete_collection(collection_id: str):
"""Delete collection."""
success = mock_delete_collection(collection_id)
if not success:
raise HTTPException(status_code=404, detail="Collection not found")
return {"message": "Collection deleted successfully"}
@router.post("/{collection_id}/refresh", response_model=dict)
async def refresh_collection(collection_id: str):
"""Refresh dynamic collection (re-evaluate query)."""
return {"message": "Collection refreshed successfully"}
@router.post("/execute", response_model=List[dict])
async def execute_query(query_expression: dict):
"""Execute query and return result set."""
return mock_execute_query(query_expression)
class CollectionManager:
"""Collection management helper."""
@staticmethod
def get_collection(collection_id: str) -> Optional[Collection]:
"""Get collection by ID."""
db = get_db()
try:
collection = db.query(Collection).filter(Collection.id == collection_id).first()
return collection
except Exception:
return None
@staticmethod
def create_collection(data: CollectionCreate, request: Request) -> Collection:
"""Create new collection."""
db = get_db()
collection = Collection(
name=data.name,
description=data.description,
query_type=data.query_type,
query_expression=data.query_expression,
is_public=data.is_public,
tags=TagCollection(tags=data.tags or []),
)
db.add(collection)
db.commit()
db.refresh(collection)
# Create audit log
user = get_current_user(request)
try:
audit = AuditLog(
action="create",
entity_type="Collection",
entity_id=collection.id,
old_value=None,
new_value=collection.dict(),
user_id=user.get("id")
)
db.add(audit)
db.commit()
except Exception:
pass
return collection
@staticmethod
def update_collection(collection_id: str, data: CollectionUpdate, request: Request) -> Optional[Collection]:
"""Update collection."""
db = get_db()
collection = db.query(Collection).filter(Collection.id == collection_id).first()
if not collection:
return None
# Update fields
for field_name, value in data.dict().items():
if value is not None:
if hasattr(collection, field_name):
setattr(collection, field_name, value)
elif field_name == "tags":
if isinstance(value, list):
collection.tags.add(*value)
else:
collection.tags.update(str(value))
db.commit()
db.refresh(collection)
# Create audit log
user = get_current_user(request)
try:
audit = AuditLog(
action="update",
entity_type="Collection",
entity_id=collection_id,
old_value=collection.dict(),
new_value=collection.dict(),
user_id=user.get("id")
)
db.add(audit)
db.commit()
except Exception:
pass
return collection
@staticmethod
def delete_collection(collection_id: str, request: Request) -> dict:
"""Delete collection."""
db = get_db()
collection = db.query(Collection).filter(Collection.id == collection_id).first()
if not collection:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Collection not found")
db.delete(collection)
db.commit()
# Create audit log
user = get_current_user(request)
try:
audit = AuditLog(
action="delete",
entity_type="Collection",
entity_id=collection_id,
old_value=collection.dict(),
new_value=None,
user_id=user.get("id")
)
db.add(audit)
db.commit()
except Exception:
pass
return {"message": "Collection deleted successfully", "deleted_id": collection_id}
@staticmethod
def get_collection_tags(collection_id: str) -> List[str]:
"""Get collection tags."""
db = get_db()
collection = db.query(Collection).filter(Collection.id == collection_id).first()
if not collection:
return []
return list(collection.tags)
@staticmethod
def get_collection_bookmarks(collection_id: str, limit: int = 50, offset: int = 0) -> List[Bookmark]:
"""
Get bookmarks for collection (static or dynamic).
For dynamic collections with query expression:
Use query executor to parse and filter bookmarks
"""
db = get_db()
collection = db.query(Collection).filter(Collection.id == collection_id).first()
if not collection:
return []
if collection.query_type == "static":
# Static collection: get all bookmarks
bookmarks = db.query(Bookmark).filter(Bookmark.collection_id == collection_id).limit(limit).offset(offset).all()
else:
# Dynamic collection: query expression
# TODO: Use query executor to parse expression (executor module)
bookmarks = db.query(Bookmark).limit(limit).offset(offset).all()
return bookmarks