"""Industry Atlas: `/industries`, `/industries/{slug}` (cached 300 s).""" from __future__ import annotations from typing import Any from fastapi import APIRouter, HTTPException, Request from companyatlas.api import aggregates as agg from companyatlas.api import queries as q from companyatlas.api import serializers as ser from companyatlas.api.common import cached, cached_response from companyatlas.db import connection, fetch_all, fetch_one ORDER = 20 router = APIRouter(prefix="/api/v1", tags=["industries"]) @router.get("/industries", summary="Living index per industry") async def industries(request: Request) -> Any: return cached_response(request, {"items": await agg.cached_industry_rows()}, 300) @router.get("/industries/{slug}", summary="Industry detail") async def industry_detail(slug: str, request: Request) -> Any: slug = slug.strip().lower()[:80] async def produce() -> dict[str, Any] | None: rows = await agg.cached_industry_rows() row = next((r for r in rows if r["slug"] == slug), None) async with connection() as conn: if row is None: tax = await fetch_one(conn, "select slug, name, parent_slug, description from industries where slug = :s", s=slug) if tax is None: return None row = {**tax, "companies": 0, "events_7d": 0, "events_30d": 0, "hiring_momentum_30d": None, "activity_score": None, "ai_adoption": None, "top_event_types": []} where, params = q.company_filters(industry=slug, status="ACTIVE") ids, _total = await q.company_page_ids(conn, where, params, sort="activity", limit=24) companies = [ser.company_card(r) for r in await q.fetch_cards_by_ids(conn, ids, sparkline=True)] events = await agg.live_events(conn, 20, industry=slug) d30 = q.days_ago(30) h = await fetch_one(conn, "select count(*) filter (where j.status = 'open') as open, count(*) filter (where j.first_seen_at >= :d) as new_30d, " "count(*) filter (where j.removed_at >= :d) as removed_30d from jobs j join companies c on c.id = j.company_id " "where cast(:s as text) = any(c.industries)", d=d30, s=slug) or {} series = await fetch_all(conn, "select ms.day, avg(ms.value) as value, avg(ms.confidence) as confidence from metric_series ms " "join companies c on c.id = ms.company_id where ms.metric = 'activity_score' and cast(:s as text) = any(c.industries) " "and ms.day >= :d group by ms.day order by ms.day", s=slug, d=q.days_ago(90).date()) countries = await fetch_all(conn, "select country, count(*) as companies from companies c where cast(:s as text) = any(c.industries) and " "country is not null and status = 'ACTIVE' group by country order by companies desc limit 50", s=slug) children = await fetch_all(conn, "select slug, name from industries where parent_slug = :s order by sort_order, name", s=slug) signals = await fetch_all(conn, "select * from signals where scope = 'industry' and scope_key = :s and status = 'active' order by detected_at desc limit 10", s=slug) return {**row, "companies_total": row["companies"], "companies": companies, "events": events, "hiring": {"open": int(h.get("open") or 0), "new_30d": int(h.get("new_30d") or 0), "removed_30d": int(h.get("removed_30d") or 0), "momentum_30d": row.get("hiring_momentum_30d")}, "series": [ser.metric_point(r) for r in series], "countries": [{"country": r["country"], "companies": int(r["companies"])} for r in countries], "trending": [], "children": children, "signals": [ser.signal(s) for s in signals]} payload = await cached(f"industry:{slug}", 300, produce) if payload is None: raise HTTPException(status_code=404, detail="industry not found") return cached_response(request, payload, 300)