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")