138 lines
5.5 KiB
Python
138 lines
5.5 KiB
Python
"""
|
|
Isnad chain endpoints — chain visualization data for hadith detail views.
|
|
"""
|
|
from fastapi import APIRouter, Query, HTTPException
|
|
|
|
from app.services.database import db
|
|
from app.models.schemas import IsnadChain, IsnadNode, IsnadLink
|
|
|
|
router = APIRouter(prefix="/chains", tags=["Isnad Chains"])
|
|
|
|
|
|
@router.get("/hadith/{hadith_id}", response_model=IsnadChain,
|
|
summary="Get isnad chain for a hadith",
|
|
description="Returns the complete isnad (chain of narration) as a graph structure "
|
|
"with nodes (narrators) and links (transmission relationships). "
|
|
"Ready for visualization with D3.js, vis.js, Cytoscape.js, or any graph library. "
|
|
"Each node includes narrator metadata (generation, reliability); "
|
|
"each link includes the transmission verb (حدثنا، عن، أخبرنا).")
|
|
async def get_isnad_chain(hadith_id: str):
|
|
"""
|
|
Get the full isnad chain for a hadith as a graph (nodes + links)
|
|
ready for visualization (D3.js, vis.js, etc.).
|
|
"""
|
|
# Get hadith info
|
|
hadith = db.neo4j_query_one("""
|
|
MATCH (h:Hadith {id: $hid})
|
|
RETURN h.id AS id, h.collection AS collection, h.hadith_number AS hadith_number
|
|
""", {"hid": hadith_id})
|
|
|
|
if not hadith:
|
|
raise HTTPException(status_code=404, detail="Hadith not found in graph")
|
|
|
|
# Get chain nodes
|
|
nodes = db.neo4j_query("""
|
|
MATCH (n:Narrator)-[r:APPEARS_IN]->(h:Hadith {id: $hid})
|
|
RETURN n.name_arabic AS name_arabic,
|
|
n.name_transliterated AS name_transliterated,
|
|
n.entity_type AS entity_type,
|
|
n.generation AS generation,
|
|
n.reliability_grade AS reliability_grade,
|
|
r.chain_order AS chain_order
|
|
ORDER BY r.chain_order
|
|
""", {"hid": hadith_id})
|
|
|
|
# Get chain links (NARRATED_FROM within this hadith's narrators)
|
|
links = db.neo4j_query("""
|
|
MATCH (a:Narrator)-[r1:APPEARS_IN]->(h:Hadith {id: $hid})
|
|
MATCH (b:Narrator)-[r2:APPEARS_IN]->(h)
|
|
MATCH (a)-[nf:NARRATED_FROM]->(b)
|
|
WHERE $hid IN nf.hadith_ids
|
|
RETURN a.name_arabic AS source,
|
|
b.name_arabic AS target,
|
|
nf.transmission_verb AS transmission_verb
|
|
""", {"hid": hadith_id})
|
|
|
|
# If no NARRATED_FROM edges with hadith_id, fall back to chain order
|
|
if not links and len(nodes) > 1:
|
|
sorted_nodes = sorted(nodes, key=lambda n: n.get("chain_order") or 999)
|
|
links = []
|
|
for i in range(len(sorted_nodes) - 1):
|
|
links.append({
|
|
"source": sorted_nodes[i]["name_arabic"],
|
|
"target": sorted_nodes[i + 1]["name_arabic"],
|
|
"transmission_verb": None,
|
|
})
|
|
|
|
return IsnadChain(
|
|
hadith_id=str(hadith["id"]),
|
|
collection=hadith["collection"] or "",
|
|
hadith_number=hadith["hadith_number"] or 0,
|
|
nodes=[IsnadNode(**n) for n in nodes],
|
|
links=[IsnadLink(**l) for l in links],
|
|
)
|
|
|
|
|
|
@router.get("/narrator/{name_arabic}", response_model=list[IsnadChain],
|
|
summary="Get all chains for a narrator",
|
|
description="Returns all isnad chains that include a specific narrator. "
|
|
"Useful for visualizing how a narrator connects to the Prophet ﷺ "
|
|
"through different transmission paths. "
|
|
"Example: `/chains/narrator/الزهري`")
|
|
async def get_narrator_chains(
|
|
name_arabic: str,
|
|
limit: int = Query(10, ge=1, le=50, description="Maximum chains to return"),
|
|
):
|
|
"""
|
|
Get all isnad chains that include a specific narrator.
|
|
Useful for seeing how a narrator connects to the Prophet ﷺ.
|
|
"""
|
|
hadith_ids = db.neo4j_query("""
|
|
MATCH (n:Narrator {name_arabic: $name})-[:APPEARS_IN]->(h:Hadith)
|
|
RETURN h.id AS id
|
|
LIMIT $limit
|
|
""", {"name": name_arabic, "limit": limit})
|
|
|
|
chains = []
|
|
for row in hadith_ids:
|
|
chain = await get_isnad_chain(str(row["id"]))
|
|
chains.append(chain)
|
|
|
|
return chains
|
|
|
|
|
|
@router.get("/common-chains", response_model=list[dict],
|
|
summary="Find shared chains between two narrators",
|
|
description="Find hadiths where both narrators appear in the same isnad chain. "
|
|
"Useful for verifying narrator relationships and finding corroborating chains. "
|
|
"Example: `/chains/common-chains?narrator_a=الزهري&narrator_b=أنس بن مالك`")
|
|
async def find_common_chains(
|
|
narrator_a: str = Query(
|
|
..., description="First narrator (Arabic). Example: الزهري",
|
|
examples=["الزهري"],
|
|
),
|
|
narrator_b: str = Query(
|
|
..., description="Second narrator (Arabic). Example: أنس بن مالك",
|
|
examples=["أنس بن مالك"],
|
|
),
|
|
limit: int = Query(10, ge=1, le=50, description="Maximum results"),
|
|
):
|
|
"""
|
|
Find hadiths where both narrators appear in the same chain.
|
|
Useful for verifying narrator relationships.
|
|
"""
|
|
rows = db.neo4j_query("""
|
|
MATCH (a:Narrator)-[:APPEARS_IN]->(h:Hadith)<-[:APPEARS_IN]-(b:Narrator)
|
|
WHERE a.name_arabic CONTAINS $name_a
|
|
AND b.name_arabic CONTAINS $name_b
|
|
AND a <> b
|
|
RETURN h.id AS hadith_id,
|
|
h.collection AS collection,
|
|
h.hadith_number AS hadith_number,
|
|
a.name_arabic AS narrator_a,
|
|
b.name_arabic AS narrator_b
|
|
LIMIT $limit
|
|
""", {"name_a": narrator_a, "name_b": narrator_b, "limit": limit})
|
|
|
|
return [dict(r) for r in rows]
|