"""/explore/types · /explore/{type} — generic listing for every entity type, plus /frameworks, /datasets, /tools aliases.""" from __future__ import annotations from typing import Any from fastapi import APIRouter, Query, Request from aiatlas.api.common import ( ENTITY_COLS, ENTITY_FROM, PAGINATION, TYPE_LABELS, ApiError, Pagination, cached, entity_summary, page, ) from aiatlas.api.routers.entities import detail_for_type from aiatlas.db import connection, fetch_all, fetch_val from aiatlas.ids import ENTITY_TYPES router = APIRouter(prefix="/api/v1", tags=["explore"]) SORTS = {"updated": "e.updated_at desc", "name": "e.canonical_name asc", "quality": "coalesce((e.quality->>'score')::float, 0) desc", "first_seen": "e.first_seen_at desc", "release": "coalesce(e.attributes->>'release_date', e.attributes->>'published_at', e.attributes->>'latest_release_at') desc nulls last", "stars": "(case when e.attributes->>'metric.stars' ~ '^[0-9]+$' then (e.attributes->>'metric.stars')::bigint end) desc nulls last"} @router.get("/explore/types") @cached(300) async def explore_types(request: Request) -> dict[str, Any]: async with connection() as conn: rows = await fetch_all(conn, "select entity_type, count(*) as count from entities where merged_into is null group by 1 order by 2 desc, 1") return {"items": [{"entity_type": r["entity_type"], "count": int(r["count"]), "label": TYPE_LABELS.get(r["entity_type"], r["entity_type"].replace("_", " ").title())} for r in rows]} @router.get("/explore/{type}") @cached(300) async def explore_type(request: Request, type: str, p: Pagination = PAGINATION, q: str | None = Query(None, max_length=200), org: str | None = None, status: str | None = None, sort: str = "updated") -> dict[str, Any]: etype = type.strip().lower().rstrip("s") if type not in ENTITY_TYPES else type aliases = {"companie": "company", "librarie": "library", "universitie": "university", "regulations": "regulation", "hardware": "hardware"} etype = aliases.get(etype, etype) if etype not in ENTITY_TYPES: raise ApiError(404, f"unknown entity type {type!r}") if sort not in SORTS: raise ApiError(400, f"sort must be one of {', '.join(SORTS)}") where = ["e.entity_type = :t", "e.merged_into is null"] params: dict[str, Any] = {"t": etype} if q: where.append("(e.canonical_name ilike :qlike or e.search @@ plainto_tsquery('simple', :q) or exists (select 1 from entity_aliases a where a.entity_id = e.id and a.alias ilike :qlike))") params["q"], params["qlike"] = q, f"%{q}%" if org: where.append("(eo.slug = :org or eo.id = :org or eo.canonical_name ilike :org)") params["org"] = org if status: where.append("e.status = any(cast(:status as text[]))") params["status"] = [s.strip() for s in status.split(",") if s.strip()] 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) items = [] for r in rows: s = entity_summary(r) or {} if etype in ("framework", "library", "runtime", "tool", "agent"): s["kind"] = (s.get("attributes") or {}).get("kind") # canonical framework kind (normalised on read), null when the source did not say items.append(s) out = page(items, int(total or 0), p) out["entity_type"] = etype out["label"] = TYPE_LABELS.get(etype, etype.title()) return out @router.get("/frameworks/{slug}") @cached(300) async def get_framework(request: Request, slug: str) -> dict[str, Any]: return await detail_for_type(slug, ("framework", "library", "runtime")) @router.get("/datasets/{slug}") @cached(300) async def get_dataset(request: Request, slug: str) -> dict[str, Any]: return await detail_for_type(slug, ("dataset",)) @router.get("/tools/{slug}") @cached(300) async def get_tool(request: Request, slug: str) -> dict[str, Any]: return await detail_for_type(slug, ("tool", "agent", "application", "product", "mcp_server"))