396 lines
20 KiB
Python
396 lines
20 KiB
Python
"""
|
|
Pydantic response models for the Hadith Scholar API.
|
|
|
|
v2.0 changes:
|
|
- All fields that Neo4j/PG can return as null are now Optional with defaults.
|
|
- Added PaginationMeta / PaginatedResponse for paginated list endpoints.
|
|
- All existing model_config / json_schema_extra examples preserved.
|
|
"""
|
|
from pydantic import BaseModel, Field
|
|
from typing import Optional
|
|
from datetime import datetime
|
|
|
|
|
|
# ── Pagination (NEW in v2.0) ───────────────────────────────────────────────
|
|
|
|
class PaginationMeta(BaseModel):
|
|
total: int = Field(description="Total matching items")
|
|
page: int = Field(description="Current page (1-indexed)")
|
|
per_page: int = Field(description="Items per page")
|
|
pages: int = Field(description="Total pages")
|
|
|
|
model_config = {
|
|
"json_schema_extra": {
|
|
"examples": [{"total": 6986, "page": 1, "per_page": 20, "pages": 350}]
|
|
}
|
|
}
|
|
|
|
|
|
class PaginatedResponse(BaseModel):
|
|
meta: PaginationMeta
|
|
data: list
|
|
|
|
|
|
# ── Hadith ─────────────────────────────────────────────────────────────────
|
|
|
|
class HadithSummary(BaseModel):
|
|
id: str = Field(description="Unique hadith UUID")
|
|
collection: Optional[str] = Field(None, description="Collection name in English")
|
|
hadith_number: Optional[int] = Field(None, description="Hadith number within collection")
|
|
grade: Optional[str] = Field(None, description="Grading: Sahih, Hasan, Da'if, etc.")
|
|
arabic_text: Optional[str] = Field(None, description="Arabic text (truncated in lists)")
|
|
sanad_text: Optional[str] = Field(None, description="Sanad (chain) text only")
|
|
matn_text: Optional[str] = Field(None, description="Matn (body) text only")
|
|
|
|
model_config = {
|
|
"json_schema_extra": {
|
|
"examples": [{
|
|
"id": "dcf8df41-3185-4e20-a9af-db3696a48c79",
|
|
"collection": "Sahih Bukhari",
|
|
"hadith_number": 1,
|
|
"grade": "Sahih",
|
|
"arabic_text": "حَدَّثَنَا الْحُمَيْدِيُّ عَبْدُ اللَّهِ بْنُ الزُّبَيْرِ...",
|
|
"sanad_text": "حَدَّثَنَا الْحُمَيْدِيُّ...",
|
|
"matn_text": "إِنَّمَا الأَعْمَالُ بِالنِّيَّاتِ...",
|
|
}]
|
|
}
|
|
}
|
|
|
|
|
|
class TopicTag(BaseModel):
|
|
topic_arabic: str = Field("", description="Topic name in Arabic")
|
|
topic_english: str = Field("", description="Topic name in English")
|
|
category: str = Field("", description="Topic category (فقه, عقيدة, سيرة, etc.)")
|
|
|
|
|
|
class NarratorInChain(BaseModel):
|
|
order: Optional[int] = Field(None, description="Position in chain (1 = compiler-end)")
|
|
name_arabic: str = Field(description="Narrator Arabic name")
|
|
name_transliterated: str = Field("", description="Latin transliteration")
|
|
entity_type: str = Field("", description="PERSON, KUNYA, NISBA, TITLE")
|
|
transmission_verb: Optional[str] = Field(None, description="حدثنا, أخبرنا, عن, سمعت, etc.")
|
|
generation: Optional[str] = Field(None, description="صحابي, تابعي, etc.")
|
|
reliability_grade: Optional[str] = Field(None, description="ثقة, صدوق, ضعيف, etc.")
|
|
|
|
model_config = {
|
|
"json_schema_extra": {
|
|
"examples": [{
|
|
"order": 1,
|
|
"name_arabic": "الْحُمَيْدِيُّ",
|
|
"name_transliterated": "al-Humaydi",
|
|
"entity_type": "NISBA",
|
|
"transmission_verb": "حَدَّثَنَا",
|
|
"generation": "تابع التابعين",
|
|
"reliability_grade": "ثقة",
|
|
}]
|
|
}
|
|
}
|
|
|
|
|
|
class HadithDetail(BaseModel):
|
|
id: str = Field(description="Unique hadith UUID")
|
|
collection: Optional[str] = Field(None, description="Collection name")
|
|
hadith_number: Optional[int] = Field(None, description="Hadith number")
|
|
book_number: Optional[int] = Field(None, description="Book number within collection")
|
|
grade: Optional[str] = Field(None, description="Grading")
|
|
arabic_text: Optional[str] = Field(None, description="Full Arabic text")
|
|
english_text: Optional[str] = Field(None, description="English translation")
|
|
urdu_text: Optional[str] = Field(None, description="Urdu translation")
|
|
sanad_text: Optional[str] = Field(None, description="Sanad (chain) text only")
|
|
matn_text: Optional[str] = Field(None, description="Matn (body) text only")
|
|
narrator_chain: list[NarratorInChain] = Field(default_factory=list, description="Ordered narrator chain from Neo4j graph")
|
|
topics: list[TopicTag] = Field(default_factory=list, description="Topic tags for searchability")
|
|
|
|
model_config = {
|
|
"json_schema_extra": {
|
|
"examples": [{
|
|
"id": "dcf8df41-3185-4e20-a9af-db3696a48c79",
|
|
"collection": "Sahih Bukhari",
|
|
"hadith_number": 1,
|
|
"grade": "Sahih",
|
|
"arabic_text": "حَدَّثَنَا الْحُمَيْدِيُّ عَبْدُ اللَّهِ بْنُ الزُّبَيْرِ...",
|
|
"sanad_text": "حَدَّثَنَا الْحُمَيْدِيُّ...",
|
|
"matn_text": "إِنَّمَا الأَعْمَالُ بِالنِّيَّاتِ...",
|
|
"narrator_chain": [
|
|
{"order": 1, "name_arabic": "الْحُمَيْدِيُّ", "name_transliterated": "al-Humaydi", "entity_type": "NISBA", "transmission_verb": "حَدَّثَنَا"},
|
|
{"order": 2, "name_arabic": "سُفْيَانُ", "name_transliterated": "Sufyan", "entity_type": "PERSON", "transmission_verb": "حَدَّثَنَا"},
|
|
],
|
|
"topics": [
|
|
{"topic_arabic": "النية", "topic_english": "Intention", "category": "فقه"},
|
|
]
|
|
}]
|
|
}
|
|
}
|
|
|
|
|
|
# ── Narrator ───────────────────────────────────────────────────────────────
|
|
|
|
class NarratorSummary(BaseModel):
|
|
name_arabic: str = Field(description="Primary Arabic name")
|
|
name_transliterated: str = Field("", description="Latin transliteration")
|
|
entity_type: str = Field("", description="PERSON, KUNYA, NISBA, TITLE")
|
|
generation: Optional[str] = Field(None, description="طبقة: صحابي، تابعي، تابع التابعين")
|
|
reliability_grade: Optional[str] = Field(None, description="جرح وتعديل grade: ثقة، صدوق، ضعيف")
|
|
hadith_count: int = Field(0, description="Number of hadiths this narrator appears in")
|
|
|
|
model_config = {
|
|
"json_schema_extra": {
|
|
"examples": [{
|
|
"name_arabic": "أَبُو هُرَيْرَةَ",
|
|
"name_transliterated": "Abu Hurayrah",
|
|
"entity_type": "KUNYA",
|
|
"generation": "صحابي",
|
|
"reliability_grade": "ثقة",
|
|
"hadith_count": 5374
|
|
}]
|
|
}
|
|
}
|
|
|
|
|
|
class NameForm(BaseModel):
|
|
"""Alternative name forms for a narrator (kunya, nisba, laqab, etc.)."""
|
|
name: str = Field(description="Alternative name form")
|
|
type: str = Field(description="Name type: PERSON, KUNYA, NISBA, TITLE")
|
|
|
|
|
|
class FamilyInfo(BaseModel):
|
|
father: Optional[str] = None
|
|
mother: Optional[str] = None
|
|
spouse: Optional[str] = None
|
|
children: list[str] = Field(default_factory=list)
|
|
|
|
|
|
class PlaceRelation(BaseModel):
|
|
place: str = Field(description="Place name in Arabic")
|
|
relation: str = Field(description="BORN_IN, LIVED_IN, DIED_IN, or TRAVELED_TO")
|
|
|
|
model_config = {
|
|
"json_schema_extra": {
|
|
"examples": [{"place": "المدينة", "relation": "LIVED_IN"}]
|
|
}
|
|
}
|
|
|
|
|
|
class NarratorProfile(BaseModel):
|
|
"""Complete narrator profile — the mobile app profile page."""
|
|
name_arabic: str = Field(description="Primary Arabic name")
|
|
name_transliterated: str = Field("", description="Latin transliteration")
|
|
entity_type: str = Field("", description="PERSON, KUNYA, NISBA, TITLE")
|
|
full_nasab: Optional[str] = Field(None, description="Full lineage: فلان بن فلان بن فلان")
|
|
kunya: Optional[str] = Field(None, description="أبو/أم name (e.g. أبو هريرة)")
|
|
nisba: Optional[str] = Field(None, description="Attributional name (e.g. البخاري، المدني، الزهري)")
|
|
laqab: Optional[str] = Field(None, description="Title or epithet (e.g. أمير المؤمنين في الحديث)")
|
|
generation: Optional[str] = Field(None, description="طبقة: صحابي، تابعي، تابع التابعين، أتباع تابع التابعين")
|
|
reliability_grade: Optional[str] = Field(None, description="جرح وتعديل: ثقة، ثقة حافظ، صدوق، ضعيف، متروك")
|
|
reliability_detail: Optional[str] = Field(None, description="Extended grading explanation from scholars")
|
|
birth_year_hijri: Optional[int] = Field(None, description="Birth year (Hijri calendar)")
|
|
death_year_hijri: Optional[int] = Field(None, description="Death year (Hijri calendar)")
|
|
birth_year_ce: Optional[int] = Field(None, description="Birth year (CE)")
|
|
death_year_ce: Optional[int] = Field(None, description="Death year (CE)")
|
|
biography_summary_arabic: Optional[str] = Field(None, description="2-3 sentence biography in Arabic")
|
|
biography_summary_english: Optional[str] = Field(None, description="2-3 sentence biography in English")
|
|
total_hadiths_narrated_approx: Optional[int] = Field(None, description="Approximate total hadiths narrated across all collections")
|
|
hadith_count: int = Field(0, description="Hadiths in current database")
|
|
hadiths: list[HadithSummary] = Field(default_factory=list, description="Sample hadiths narrated (max 50)")
|
|
teachers: list[NarratorSummary] = Field(default_factory=list, description="Known teachers / شيوخ")
|
|
students: list[NarratorSummary] = Field(default_factory=list, description="Known students / تلاميذ")
|
|
name_forms: list[NameForm] = Field(default_factory=list, description="Alternative name forms")
|
|
family: Optional[FamilyInfo] = Field(None, description="Family info if known")
|
|
places: list[PlaceRelation] = Field(default_factory=list, description="Associated places (born, lived, died, traveled)")
|
|
tribes: list[str] = Field(default_factory=list, description="Tribal affiliations (e.g. قريش، دوس، الأنصار)")
|
|
bio_verified: bool = Field(False, description="Whether biography has been manually verified against classical sources")
|
|
|
|
model_config = {
|
|
"json_schema_extra": {
|
|
"examples": [{
|
|
"name_arabic": "أَبُو هُرَيْرَةَ",
|
|
"name_transliterated": "Abu Hurayrah",
|
|
"entity_type": "KUNYA",
|
|
"full_nasab": "عبد الرحمن بن صخر الدوسي",
|
|
"kunya": "أبو هريرة",
|
|
"nisba": "الدوسي",
|
|
"laqab": None,
|
|
"generation": "صحابي",
|
|
"reliability_grade": "ثقة",
|
|
"reliability_detail": "صحابي جليل، أكثر الصحابة رواية للحديث",
|
|
"birth_year_hijri": None,
|
|
"death_year_hijri": 57,
|
|
"birth_year_ce": None,
|
|
"death_year_ce": 676,
|
|
"biography_summary_arabic": "أبو هريرة الدوسي، صحابي جليل، أكثر الصحابة رواية للحديث النبوي. أسلم عام خيبر ولازم النبي ﷺ.",
|
|
"biography_summary_english": "Abu Hurayrah al-Dawsi, a prominent Companion and the most prolific narrator of hadith.",
|
|
"total_hadiths_narrated_approx": 5374,
|
|
"hadith_count": 142,
|
|
"teachers": [{"name_arabic": "النبي ﷺ", "name_transliterated": "Prophet Muhammad", "entity_type": "TITLE", "generation": "نبي", "reliability_grade": None, "hadith_count": 0}],
|
|
"students": [{"name_arabic": "الزهري", "name_transliterated": "al-Zuhri", "entity_type": "NISBA", "generation": "تابعي", "reliability_grade": "ثقة", "hadith_count": 0}],
|
|
"places": [{"place": "المدينة", "relation": "LIVED_IN"}],
|
|
"tribes": ["دوس"],
|
|
"bio_verified": False,
|
|
}]
|
|
}
|
|
}
|
|
|
|
|
|
# ── Isnad Chain (D3-ready) ─────────────────────────────────────────────────
|
|
|
|
class IsnadNode(BaseModel):
|
|
name_arabic: str = Field(description="Narrator Arabic name")
|
|
name_transliterated: str = Field("", description="Latin transliteration")
|
|
entity_type: str = Field("", description="PERSON, KUNYA, NISBA, TITLE")
|
|
generation: Optional[str] = Field(None, description="صحابي, تابعي, etc.")
|
|
reliability_grade: Optional[str] = Field(None, description="ثقة, صدوق, ضعيف, etc.")
|
|
|
|
|
|
class IsnadLink(BaseModel):
|
|
source: str = Field(description="name_arabic of narrator who heard")
|
|
target: str = Field(description="name_arabic of narrator who transmitted")
|
|
transmission_verb: Optional[str] = Field(None, description="حدثنا, عن, أخبرنا, etc.")
|
|
|
|
|
|
class IsnadChain(BaseModel):
|
|
hadith_id: str = Field(description="Hadith UUID")
|
|
collection: Optional[str] = Field(None, description="Collection name")
|
|
hadith_number: Optional[int] = Field(None, description="Hadith number")
|
|
nodes: list[IsnadNode] = Field(default_factory=list, description="Narrators in the chain")
|
|
links: list[IsnadLink] = Field(default_factory=list, description="Directed edges: source heard from target")
|
|
|
|
model_config = {
|
|
"json_schema_extra": {
|
|
"examples": [{
|
|
"hadith_id": "dcf8df41-3185-4e20-a9af-db3696a48c79",
|
|
"collection": "Sahih Bukhari",
|
|
"hadith_number": 1,
|
|
"nodes": [
|
|
{"name_arabic": "الْحُمَيْدِيُّ", "name_transliterated": "al-Humaydi", "entity_type": "NISBA", "generation": "تابع التابعين", "reliability_grade": "ثقة"},
|
|
{"name_arabic": "سُفْيَانُ بْنُ عُيَيْنَةَ", "name_transliterated": "Sufyan ibn Uyaynah", "entity_type": "PERSON", "generation": "تابع التابعين", "reliability_grade": "ثقة"},
|
|
{"name_arabic": "يَحْيَى بْنُ سَعِيدٍ", "name_transliterated": "Yahya ibn Sa'id al-Ansari", "entity_type": "PERSON", "generation": "تابعي", "reliability_grade": "ثقة"},
|
|
{"name_arabic": "عُمَرُ بْنُ الْخَطَّابِ", "name_transliterated": "Umar ibn al-Khattab", "entity_type": "PERSON", "generation": "صحابي", "reliability_grade": "ثقة"},
|
|
],
|
|
"links": [
|
|
{"source": "الْحُمَيْدِيُّ", "target": "سُفْيَانُ بْنُ عُيَيْنَةَ", "transmission_verb": "حَدَّثَنَا"},
|
|
{"source": "سُفْيَانُ بْنُ عُيَيْنَةَ", "target": "يَحْيَى بْنُ سَعِيدٍ", "transmission_verb": "حَدَّثَنَا"},
|
|
{"source": "يَحْيَى بْنُ سَعِيدٍ", "target": "عُمَرُ بْنُ الْخَطَّابِ", "transmission_verb": "عن"},
|
|
]
|
|
}]
|
|
}
|
|
}
|
|
|
|
|
|
# ── Relationships / Who Met Who ────────────────────────────────────────────
|
|
|
|
class NarratorInteraction(BaseModel):
|
|
narrator_a: str = Field(description="First narrator Arabic name")
|
|
narrator_a_transliterated: str = Field("", description="First narrator transliteration")
|
|
narrator_b: str = Field(description="Second narrator Arabic name")
|
|
narrator_b_transliterated: str = Field("", description="Second narrator transliteration")
|
|
relationship_type: str = Field("", description="NARRATED_FROM, TEACHER_OF, HEARD_BY, STUDENT_OF")
|
|
shared_hadith_count: int = Field(0, description="Number of hadiths connecting them")
|
|
hadith_ids: list[str] = Field(default_factory=list, description="IDs of shared hadiths (max 20)")
|
|
|
|
model_config = {
|
|
"json_schema_extra": {
|
|
"examples": [{
|
|
"narrator_a": "الزهري",
|
|
"narrator_a_transliterated": "al-Zuhri",
|
|
"narrator_b": "أنس بن مالك",
|
|
"narrator_b_transliterated": "Anas ibn Malik",
|
|
"relationship_type": "NARRATED_FROM",
|
|
"shared_hadith_count": 23,
|
|
"hadith_ids": ["abc-123", "def-456"]
|
|
}]
|
|
}
|
|
}
|
|
|
|
|
|
class NarratorConnection(BaseModel):
|
|
narrator: str = Field(description="Connected narrator Arabic name")
|
|
narrator_transliterated: str = Field("", description="Transliteration")
|
|
connection_type: str = Field(description="Relationship type")
|
|
direction: str = Field(description="'incoming' (they → this) or 'outgoing' (this → them)")
|
|
|
|
|
|
class NarratorNetwork(BaseModel):
|
|
center: NarratorSummary
|
|
connections: list[NarratorConnection] = Field(default_factory=list)
|
|
total_connections: int = 0
|
|
|
|
|
|
class PathNode(BaseModel):
|
|
name_arabic: str
|
|
name_transliterated: str = ""
|
|
generation: Optional[str] = None
|
|
|
|
|
|
class WhoMetWhoResult(BaseModel):
|
|
narrator_a: str
|
|
narrator_b: str
|
|
path: list[PathNode] = Field(default_factory=list)
|
|
path_length: Optional[int] = None
|
|
relationship_types: list[str] = Field(default_factory=list)
|
|
|
|
|
|
# ── Search ─────────────────────────────────────────────────────────────────
|
|
|
|
class SemanticSearchResult(BaseModel):
|
|
hadith: HadithSummary = Field(description="Matching hadith")
|
|
score: float = Field(description="Cosine similarity score (0-1, higher = more relevant)")
|
|
collection: str = Field("", description="Collection name")
|
|
|
|
model_config = {
|
|
"json_schema_extra": {
|
|
"examples": [{
|
|
"hadith": {
|
|
"id": "abc-123",
|
|
"collection": "Sahih Bukhari",
|
|
"hadith_number": 1,
|
|
"grade": "Sahih",
|
|
"arabic_text": "إِنَّمَا الأَعْمَالُ بِالنِّيَّاتِ..."
|
|
},
|
|
"score": 0.9234,
|
|
"collection": "Sahih Bukhari"
|
|
}]
|
|
}
|
|
}
|
|
|
|
|
|
class FullTextSearchResult(BaseModel):
|
|
hadith: HadithSummary = Field(description="Matching hadith")
|
|
score: float = Field(description="Elasticsearch relevance score")
|
|
highlights: list[str] = Field(default_factory=list, description="Text fragments with <em> highlighted matches")
|
|
|
|
model_config = {
|
|
"json_schema_extra": {
|
|
"examples": [{
|
|
"hadith": {
|
|
"id": "abc-123",
|
|
"collection": "Sahih Muslim",
|
|
"hadith_number": 1599,
|
|
"grade": "Sahih",
|
|
"arabic_text": "..."
|
|
},
|
|
"score": 12.45,
|
|
"highlights": ["...عن <em>الصلاة</em> في المسجد..."]
|
|
}]
|
|
}
|
|
}
|
|
|
|
|
|
class CombinedSearchResult(BaseModel):
|
|
hadith: HadithSummary
|
|
semantic_score: Optional[float] = None
|
|
fulltext_score: Optional[float] = None
|
|
combined_score: float = 0.0
|
|
source: str = Field(description="semantic, fulltext, or both")
|
|
|
|
|
|
# ── Stats ──────────────────────────────────────────────────────────────────
|
|
|
|
class SystemStats(BaseModel):
|
|
hadiths_pg: Optional[int] = None
|
|
narrators_neo4j: Optional[int] = None
|
|
places_neo4j: Optional[int] = None
|
|
tribes_neo4j: Optional[int] = None
|
|
relationships_neo4j: Optional[int] = None
|
|
embeddings_qdrant: Optional[int] = None
|
|
documents_es: Optional[int] = None
|