Spaces:
Sleeping
Sleeping
File size: 11,976 Bytes
17cdd8f d16a3c3 17cdd8f 88268b9 17cdd8f 88268b9 17cdd8f 88268b9 17cdd8f 6bea231 17cdd8f 44789ee 17cdd8f 5d1b562 d1b3409 5d1b562 d1b3409 a2b8ffe a1c4cd4 5d1b562 ceb0375 5d1b562 2eefc8a 17cdd8f 5d1b562 2eefc8a 5d1b562 17cdd8f 1ae2259 17cdd8f |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 |
import os
from dotenv import load_dotenv
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from fastapi_cache import FastAPICache
from fastapi_cache.backends.inmemory import InMemoryBackend
# from fastapi_cache.coder import PickleCoder
from fastapi_cache.decorator import cache
from typing import Union, Optional, Type, Any
from utils.student import Student
from utils.instructor import Instructor
from utils.course import Course
from utils.enrollment import Enrollment
from utils.logging import logging
from sqlmodel import SQLModel, select
from sqlmodel.sql.expression import SelectOfScalar
from sqlmodel.ext.asyncio.session import AsyncSession
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import Engine
from typing import Dict
from config import (
# ONE_DAY_SEC,
ONE_WEEK_SEC,
ENV_PATH,
USE_REDIS_CACHE,
USE_POSTGRES_DB,
DESCRIPTION,
)
load_dotenv(ENV_PATH)
sms_resource: Dict[str,
Union[Engine, logging.Logger]] = {}
@asynccontextmanager
async def lifespan(_: FastAPI) -> AsyncIterator[None]:
# Cache
if USE_REDIS_CACHE:
from redis import asyncio as aioredis
from fastapi_cache.backends.redis import RedisBackend
url = os.getenv("REDIS_URL")
username = os.getenv("REDIS_USERNAME")
password = os.getenv("REDIS_PASSWORD")
redis = aioredis.from_url(url=url, username=username,
password=password, encoding="utf8", decode_responses=True)
FastAPICache.init(RedisBackend(redis), prefix="fastapi-cache")
else:
# In Memory cache
FastAPICache.init(InMemoryBackend())
# Database
if USE_POSTGRES_DB:
DATABASE_URL = os.getenv("POSTGRES_URL")
connect_args = {
"timeout": 60
}
else: # sqlite
DATABASE_URL = "sqlite+aiosqlite:///database/sms.db"
# Allow a single connection to be accessed from multiple threads.
connect_args = {"check_same_thread": False}
# Define the async engine
engine = create_async_engine(
DATABASE_URL, echo=True, connect_args=connect_args)
sms_resource["engine"] = engine
# Startup actions: create database tables
async with engine.begin() as conn:
await conn.run_sync(SQLModel.metadata.create_all)
# Logger
logger = logging.getLogger(__name__)
sms_resource["logger"] = logger
yield # Application code runs here
# Shutdown actions: close connections, etc.
await engine.dispose()
# FastAPI Object
app = FastAPI(
title='School Management System API',
version='1.0.0',
description=DESCRIPTION,
lifespan=lifespan,
)
app.mount("/assets", StaticFiles(directory="assets"), name="assets")
@app.get('/favicon.ico', include_in_schema=False)
@cache(expire=ONE_WEEK_SEC, namespace='eta_favicon') # Cache for 1 week
async def favicon():
file_name = "favicon.ico"
file_path = os.path.join(app.root_path, "assets", file_name)
return FileResponse(path=file_path, headers={"Content-Disposition": "attachment; filename=" + file_name})
# API
OneResult = Union[Student, Instructor, Course, Enrollment]
OneResultItem = Optional[OneResult]
BulkResult = Dict[str, OneResult]
BulkResultItem = Optional[BulkResult]
Result = Union[OneResult, BulkResult]
ResultItem = Union[OneResultItem, BulkResultItem]
class EndpointResponse(SQLModel):
execution_msg: str
execution_code: int
# result: ResultItem
result: Any
class ErrorResponse(SQLModel):
execution_msg: str
execution_code: int
error: Optional[str]
# Endpoints
# Status endpoint: check if api is online
@app.get('/', tags=['Home'])
async def status_check():
return {"Status": "API is online..."}
async def endpoint_output(endpoint_result: ResultItem, code: int = 0, error: str = None) -> Union[ErrorResponse, EndpointResponse]:
msg = 'Execution failed'
output = ErrorResponse(**{'execution_msg': msg,
'execution_code': code, 'error': error})
try:
if code != 0:
msg = 'Execution was successful'
output = EndpointResponse(
**{'execution_msg': msg,
'execution_code': code, 'result': endpoint_result}
)
except Exception as e:
code = 0
msg = 'Execution failed'
errors = f"Omg, an error occurred. endpoint_output Error: {e} & endpoint_result Error: {error} & endpoint_result: {endpoint_result}"
output = ErrorResponse(**{'execution_msg': msg,
'execution_code': code, 'error': errors})
sms_resource["logger"].error(error)
finally:
return output
# Caching Post requests is challenging
async def sms_posts(instance: ResultItem, idx: str = None, action: str = "add") -> Union[ErrorResponse, EndpointResponse]:
async with AsyncSession(sms_resource["engine"]) as session:
code = 1
error = None
result = None
existing = await session.get(instance.__class__, idx)
# For add action, do db operation if instance is not existing. Other actions, do db operation if instance exists in db
checker = existing is None if action == "add" else existing is not None
try:
if checker:
if action == "add":
session.add(instance) # Not asynchronous
await session.commit()
result = instance
elif action == "delete":
await session.delete(existing) # Asynchronous
await session.commit()
else: # update
vars(existing).update(vars(instance))
session.add(existing) # Not asynchronous
await session.commit()
await session.refresh(existing)
result = existing
except Exception as e:
code = 0
error = str(e)
finally:
return await endpoint_output(result, code, error)
# @cache(expire=ONE_DAY_SEC, namespace='sms_gets') # Cache for 1 day
async def sms_gets(sms_class: Type[Result], action: str = "first", idx: str = None, stmt: SelectOfScalar[Type[Result]] = None) -> Union[ErrorResponse, EndpointResponse]:
async with AsyncSession(sms_resource["engine"]) as session:
code = 1
error = None
result = None
try:
if action == "all":
statement = select(sms_class) if stmt is None else stmt
instance_list = (await session.exec(statement)).all()
if instance_list:
result = {
str(instance.id): instance for instance in instance_list}
elif action == "first":
statement = select(sms_class).where(
sms_class.id == idx) if stmt is None else stmt
result = (await session.exec(statement)).first()
except Exception as e:
code = 0
error = str(e)
finally:
return await endpoint_output(result, code, error)
# Student Routes
@app.post('/api/v1/sms/add_student', tags=['Student'])
async def add_student(student: Student) -> Union[ErrorResponse, EndpointResponse]:
return await sms_posts(student, student.id, action="add")
@app.put('/api/v1/sms/update_student', tags=['Student'])
async def update_student(student: Student) -> Union[ErrorResponse, EndpointResponse]:
return await sms_posts(student, student.id, action="update")
@app.delete('/api/v1/sms/delete_student', tags=['Student'])
async def delete_student(student: Student) -> Union[ErrorResponse, EndpointResponse]:
return await sms_posts(student, student.id, action="delete")
@app.get("/api/v1/sms/students/{id}", tags=['Student'])
async def find_student(id: str) -> Union[ErrorResponse, EndpointResponse]:
return await sms_gets(Student, "first", id)
@app.get("/api/v1/sms/students", tags=['Student'])
async def all_students() -> Union[ErrorResponse, EndpointResponse]:
return await sms_gets(Student, "all")
# Instructor Routes
@app.post('/api/v1/sms/add_instructor', tags=['Instructor'])
async def add_instructor(instructor: Instructor) -> Union[ErrorResponse, EndpointResponse]:
return await sms_posts(instructor, instructor.id, action="add")
@app.put('/api/v1/sms/update_instructor', tags=['Instructor'])
async def update_instructor(instructor: Instructor) -> Union[ErrorResponse, EndpointResponse]:
return await sms_posts(instructor, instructor.id, action="update")
@app.delete('/api/v1/sms/delete_instructor', tags=['Instructor'])
async def delete_instructor(instructor: Instructor) -> Union[ErrorResponse, EndpointResponse]:
return await sms_posts(instructor, instructor.id, action="delete")
@app.get("/api/v1/sms/instructors/{id}", tags=['Instructor'])
async def find_instructor(id: str) -> Union[ErrorResponse, EndpointResponse]:
return await sms_gets(Instructor, "first", id)
@app.get("/api/v1/sms/instructors", tags=['Instructor'])
async def all_instructors() -> Union[ErrorResponse, EndpointResponse]:
return await sms_gets(Instructor, "all")
# Course Routes
@app.post('/api/v1/sms/add_course', tags=['Course'])
async def add_course(course: Course) -> Union[ErrorResponse, EndpointResponse]:
return await sms_posts(course, course.id, action="add")
@app.put('/api/v1/sms/update_course', tags=['Course'])
async def update_course(course: Course) -> Union[ErrorResponse, EndpointResponse]:
return await sms_posts(course, course.id, action="update")
@app.delete('/api/v1/sms/delete_course', tags=['Course'])
async def delete_student(course: Course) -> Union[ErrorResponse, EndpointResponse]:
return await sms_posts(course, course.id, action="delete")
@app.get("/api/v1/sms/courses/{id}", tags=['Course'])
async def find_course(id: str) -> Union[ErrorResponse, EndpointResponse]:
return await sms_gets(Course, "first", id)
@app.get("/api/v1/sms/courses", tags=['Course'])
async def all_courses() -> Union[ErrorResponse, EndpointResponse]:
return await sms_gets(Course, "all")
# Enroll Routes
@app.post('/api/v1/sms/enroll_student', tags=['Enroll'])
async def enroll_student(enrollment: Enrollment) -> Union[ErrorResponse, EndpointResponse]:
return await sms_posts(enrollment, enrollment.id, action="add")
@app.put('/api/v1/sms/update_enrolled_student', tags=['Enroll'])
async def update_enrolled_student(enrollment: Enrollment) -> Union[ErrorResponse, EndpointResponse]:
return await sms_posts(enrollment, enrollment.id, action="update")
@app.delete('/api/v1/sms/delete_enrolled_student', tags=['Enroll'])
async def delete_enrolled_student(enrollment: Enrollment) -> Union[ErrorResponse, EndpointResponse]:
return await sms_posts(enrollment, enrollment.id, action="delete")
@app.get('/api/v1/sms/enrollments/{id}', tags=['Enroll'])
async def find_enrollment_by_id(id: str) -> Union[ErrorResponse, EndpointResponse]:
return await sms_gets(Enrollment, "first", id)
@app.get('/api/v1/sms/enrollments/{student_id}', tags=['Enroll'])
async def find_enrollment_by_student_id(student_id: str) -> Union[ErrorResponse, EndpointResponse]:
stmt = select(Enrollment).where(Enrollment.student_id == student_id)
return await sms_gets(Enrollment, action="all", stmt=stmt)
@app.get('/api/v1/sms/enrollments', tags=['Enroll'])
async def all_enrolled_students() -> Union[ErrorResponse, EndpointResponse]:
return await sms_gets(Enrollment, "all")
@app.put('/api/v1/sms/grade_student', tags=['Grade'])
async def assign_grade(enrollment: Enrollment) -> Union[ErrorResponse, EndpointResponse]:
return await sms_posts(enrollment, enrollment.id, action="update")
|