"""/papers listing and /papers/{slug} alias.""" from __future__ import annotations from typing import Any from fastapi import APIRouter, Query, Request from aiatlas.api.common import ( ENTITY_COLS, ENTITY_FROM, PAGINATION, ApiError, Pagination, cached, entity_summary, page, parse_date, ) from aiatlas.api.routers.entities import detail_for_type from aiatlas.db import connection, fetch_all, fetch_val router = APIRouter(prefix="/api/v1/papers", tags=["papers"]) SORTS = {"published": "e.attributes->>'published_at' desc nulls last", "updated": "e.updated_at desc", "name": "e.canonical_name asc", "citations": "(case when e.attributes->>'metric.citations' ~ '^[0-9]+$' then (e.attributes->>'metric.citations')::bigint end) desc nulls last"} @router.get("") @cached(300) async def list_papers(request: Request, p: Pagination = PAGINATION, q: str | None = Query(None, max_length=200), category: str | None = None, org: str | None = None, since: str | None = None, until: str | None = None, sort: str = "published") -> dict[str, Any]: if sort not in SORTS: raise ApiError(400, f"sort must be one of {', '.join(SORTS)}") where = ["e.entity_type = 'paper'", "e.merged_into is null"] params: dict[str, Any] = {} if q: where.append("(e.canonical_name ilike :qlike or e.search @@ plainto_tsquery('english', :q))") params["q"], params["qlike"] = q, f"%{q}%" if category: where.append("(e.attributes->>'primary_category' = :cat or e.attributes->'categories' ? :cat)") params["cat"] = category if org: where.append("(eo.slug = :org or eo.id = :org or exists (select 1 from relations r join entities x on x.id = r.object_id where r.subject_id = e.id and r.valid_to is null and (x.slug = :org or x.id = :org)))") params["org"] = org if since: where.append("e.attributes->>'published_at' >= :since") params["since"] = parse_date(since, "since").isoformat() # type: ignore[union-attr] if until: where.append("e.attributes->>'published_at' <= :until") params["until"] = parse_date(until, "until").isoformat() # type: ignore[union-attr] where_sql = " and ".join(where) async with connection() as conn: rows = await fetch_all(conn, f"select {ENTITY_COLS} from {ENTITY_FROM} where {where_sql} order by {SORTS[sort]}, e.id limit :lim offset :off", lim=p.limit, off=p.offset, **params) total = await fetch_val(conn, f"select count(*) from {ENTITY_FROM} where {where_sql}", **params) return page([entity_summary(r) for r in rows], int(total or 0), p) @router.get("/{slug}") @cached(300) async def get_paper(request: Request, slug: str) -> dict[str, Any]: return await detail_for_type(slug, ("paper",))