Skip to content

Commit 4d4fc1d

Browse files
committed
feat: all schools endpoint
1 parent 27b03a8 commit 4d4fc1d

File tree

3 files changed

+235
-1
lines changed

3 files changed

+235
-1
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,4 @@ migrations/REGENERATION_README.md
5252
testscripts/test_new_endpoints.py
5353
testscripts/check_llm_structure.py
5454
NEW_ENDPOINTS_SUMMARY.md
55+
testscripts/test_unified_endpoint.py

api/main.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from fastapi import FastAPI
22
from fastapi.middleware.cors import CORSMiddleware
3-
from .routers import al, csusb, kctcs, ky, oh, upload
3+
from .routers import al, csusb, kctcs, ky, oh, upload, unified
44
from db_operations.connection import test_all_connections
55

66
app = FastAPI(
@@ -51,6 +51,11 @@
5151
prefix="/upload",
5252
tags=["Data Upload"]
5353
)
54+
app.include_router(
55+
unified.router,
56+
prefix="/unified",
57+
tags=["Unified - Query Any Database/Table"]
58+
)
5459

5560
@app.get("/", tags=["Root"])
5661
async def root():
@@ -66,6 +71,7 @@ async def root():
6671
"endpoints": {
6772
"health": "/health",
6873
"upload": "/upload/",
74+
"unified": "/unified/data",
6975
"databases": {
7076
"AL": "/al/",
7177
"CSUSB": "/csusb/",

api/routers/unified.py

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
from fastapi import APIRouter, HTTPException, Query
2+
from typing import List, Optional, Dict, Any
3+
from ..schemas import CohortRecord, CourseRecord, FinancialAidRecord, LlmRecommendationRecord, AnalysisReadyRecord
4+
from db_operations.connection import get_db_connection, format_records
5+
6+
router = APIRouter()
7+
8+
# Database mappings
9+
DATABASES = {
10+
"AL": "Bishop_State_Community_College",
11+
"CSUSB": "California_State_University_San_Bernardino",
12+
"KCTCS": "Kentucky_Community_and_Technical_College_System",
13+
"KY": "Thomas_More_University",
14+
"OH": "University_of_Akron"
15+
}
16+
17+
# Table mappings with their AR table variants
18+
TABLE_MAPPINGS = {
19+
"cohort": {"table": "cohort", "model": CohortRecord},
20+
"course": {"table": "course", "model": CourseRecord},
21+
"financial_aid": {"table": "financial_aid", "model": FinancialAidRecord},
22+
"llm_recommendations": {"table": "llm_recommendations", "model": LlmRecommendationRecord},
23+
"analysis_ready": {
24+
"AL": "ar_al",
25+
"CSUSB": "ar_csusb",
26+
"KCTCS": "ar_kctcs",
27+
"KY": "ar_ky",
28+
"OH": "ar_oh",
29+
"model": AnalysisReadyRecord
30+
}
31+
}
32+
33+
@router.get("/data", response_model=List[Dict[str, Any]])
34+
async def get_unified_data(
35+
database: str = Query(..., description="Database code: AL, CSUSB, KCTCS, KY, OH"),
36+
table: str = Query(..., description="Table name: cohort, course, financial_aid, llm_recommendations, analysis_ready"),
37+
limit: int = Query(100, ge=1, le=1000, description="Number of records to return"),
38+
offset: int = Query(0, ge=0, description="Number of records to skip"),
39+
student_guid: Optional[str] = Query(None, description="Filter by Student_GUID"),
40+
cohort: Optional[str] = Query(None, description="Filter by Cohort"),
41+
academic_year: Optional[str] = Query(None, description="Filter by Academic_Year"),
42+
institution_id: Optional[int] = Query(None, description="Filter by Institution_ID")
43+
):
44+
"""
45+
Unified endpoint to query any table from any database with optional filters.
46+
47+
Examples:
48+
- /unified/data?database=KY&table=cohort&limit=10
49+
- /unified/data?database=AL&table=course&student_guid=ABC123
50+
- /unified/data?database=CSUSB&table=analysis_ready&cohort=2020
51+
- /unified/data?database=KY&table=llm_recommendations&student_guid=ABC123
52+
"""
53+
54+
# Validate database
55+
if database not in DATABASES:
56+
raise HTTPException(
57+
status_code=400,
58+
detail=f"Invalid database. Must be one of: {', '.join(DATABASES.keys())}"
59+
)
60+
61+
# Validate table
62+
if table not in TABLE_MAPPINGS:
63+
raise HTTPException(
64+
status_code=400,
65+
detail=f"Invalid table. Must be one of: {', '.join(TABLE_MAPPINGS.keys())}"
66+
)
67+
68+
# Get actual table name (handle analysis_ready special case)
69+
if table == "analysis_ready":
70+
actual_table = TABLE_MAPPINGS["analysis_ready"][database]
71+
else:
72+
actual_table = TABLE_MAPPINGS[table]["table"]
73+
74+
# Get database name
75+
db_name = DATABASES[database]
76+
77+
try:
78+
with get_db_connection(db_name) as connection:
79+
cursor = connection.cursor(dictionary=True)
80+
81+
# Build query with filters
82+
query = f"SELECT * FROM {actual_table} WHERE 1=1"
83+
params = []
84+
85+
# Add filters
86+
if student_guid:
87+
# Handle both Student_GUID and student_id columns
88+
if table == "analysis_ready":
89+
query += " AND student_id = %s"
90+
else:
91+
query += " AND Student_GUID = %s"
92+
params.append(student_guid)
93+
94+
if cohort:
95+
query += " AND Cohort = %s"
96+
params.append(cohort)
97+
98+
if academic_year:
99+
query += " AND Academic_Year = %s"
100+
params.append(academic_year)
101+
102+
if institution_id:
103+
query += " AND Institution_ID = %s"
104+
params.append(institution_id)
105+
106+
# Add ordering and pagination
107+
query += " ORDER BY id LIMIT %s OFFSET %s"
108+
params.extend([limit, offset])
109+
110+
cursor.execute(query, params)
111+
records = cursor.fetchall()
112+
cursor.close()
113+
114+
return format_records(records)
115+
116+
except Exception as e:
117+
raise HTTPException(
118+
status_code=500,
119+
detail=f"Error fetching data from {database}.{actual_table}: {str(e)}"
120+
)
121+
122+
@router.get("/data/count")
123+
async def get_unified_count(
124+
database: str = Query(..., description="Database code: AL, CSUSB, KCTCS, KY, OH"),
125+
table: str = Query(..., description="Table name: cohort, course, financial_aid, llm_recommendations, analysis_ready"),
126+
student_guid: Optional[str] = Query(None, description="Filter by Student_GUID"),
127+
cohort: Optional[str] = Query(None, description="Filter by Cohort"),
128+
academic_year: Optional[str] = Query(None, description="Filter by Academic_Year"),
129+
institution_id: Optional[int] = Query(None, description="Filter by Institution_ID")
130+
):
131+
"""
132+
Get count of records matching the filters.
133+
134+
Examples:
135+
- /unified/data/count?database=KY&table=cohort
136+
- /unified/data/count?database=AL&table=course&student_guid=ABC123
137+
"""
138+
139+
# Validate database
140+
if database not in DATABASES:
141+
raise HTTPException(
142+
status_code=400,
143+
detail=f"Invalid database. Must be one of: {', '.join(DATABASES.keys())}"
144+
)
145+
146+
# Validate table
147+
if table not in TABLE_MAPPINGS:
148+
raise HTTPException(
149+
status_code=400,
150+
detail=f"Invalid table. Must be one of: {', '.join(TABLE_MAPPINGS.keys())}"
151+
)
152+
153+
# Get actual table name
154+
if table == "analysis_ready":
155+
actual_table = TABLE_MAPPINGS["analysis_ready"][database]
156+
else:
157+
actual_table = TABLE_MAPPINGS[table]["table"]
158+
159+
# Get database name
160+
db_name = DATABASES[database]
161+
162+
try:
163+
with get_db_connection(db_name) as connection:
164+
cursor = connection.cursor()
165+
166+
# Build query with filters
167+
query = f"SELECT COUNT(*) FROM {actual_table} WHERE 1=1"
168+
params = []
169+
170+
# Add filters
171+
if student_guid:
172+
if table == "analysis_ready":
173+
query += " AND student_id = %s"
174+
else:
175+
query += " AND Student_GUID = %s"
176+
params.append(student_guid)
177+
178+
if cohort:
179+
query += " AND Cohort = %s"
180+
params.append(cohort)
181+
182+
if academic_year:
183+
query += " AND Academic_Year = %s"
184+
params.append(academic_year)
185+
186+
if institution_id:
187+
query += " AND Institution_ID = %s"
188+
params.append(institution_id)
189+
190+
cursor.execute(query, params)
191+
count = cursor.fetchone()[0]
192+
cursor.close()
193+
194+
return {
195+
"database": database,
196+
"table": table,
197+
"count": count,
198+
"filters": {
199+
"student_guid": student_guid,
200+
"cohort": cohort,
201+
"academic_year": academic_year,
202+
"institution_id": institution_id
203+
}
204+
}
205+
206+
except Exception as e:
207+
raise HTTPException(
208+
status_code=500,
209+
detail=f"Error counting records in {database}.{actual_table}: {str(e)}"
210+
)
211+
212+
@router.get("/databases")
213+
async def list_databases():
214+
"""List all available databases."""
215+
return {
216+
"databases": [
217+
{"code": code, "name": name}
218+
for code, name in DATABASES.items()
219+
]
220+
}
221+
222+
@router.get("/tables")
223+
async def list_tables():
224+
"""List all available tables."""
225+
return {
226+
"tables": list(TABLE_MAPPINGS.keys())
227+
}

0 commit comments

Comments
 (0)