gabcares commited on
Commit
4d1c551
·
verified ·
1 Parent(s): 7de70bc

Upload 18 files

Browse files

Student Management System codebase

Dockerfile ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11.9-slim
2
+
3
+ # Copy requirements file
4
+ COPY requirements.txt .
5
+
6
+ # Update pip
7
+ RUN pip --timeout=3000 install --no-cache-dir --upgrade pip
8
+
9
+ # Install dependecies
10
+ RUN pip --timeout=3000 install --no-cache-dir -r requirements.txt
11
+
12
+ # Make api
13
+ RUN mkdir -p /api/
14
+
15
+ # Set app as the working directory
16
+ WORKDIR /api
17
+
18
+ # Copy api
19
+ COPY . .
20
+
21
+ # Expose app port Huggingface
22
+ EXPOSE 7860
23
+
24
+ # Start application
25
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
__init__.py ADDED
File without changes
assets/favicon.ico ADDED
config.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+
3
+ # ENV when using standalone uvicorn server running FastAPI in api directory
4
+ ENV_PATH = Path("../env/api.env")
5
+
6
+ ONE_DAY_SEC = 24*60*60
7
+
8
+ ONE_WEEK_SEC = ONE_DAY_SEC*7
9
+
10
+ # Cache
11
+ USE_REDIS_CACHE = True # Change to True to use Redis Cache
12
+
13
+ # Postgres
14
+ USE_POSTGRES_DB = True # Change to True to use Posgres DB
15
+
16
+
17
+ DESCRIPTION = """
18
+ This API is the backend of the student management system.\n
19
+
20
+ Manages students, instructors, courses, and enrollments within an educational institution.\n
21
+
22
+ Provides functionalities to manage students, instructors, and courses, as well as
23
+ to enroll students in courses and assign grades.\n
24
+
25
+ - Add, remove, and update students and instructors
26
+ - Add, remove, and update courses
27
+ - Enroll students in courses
28
+ - Assign grades to students for specific courses
29
+ - Retrieve a list of students enrolled in a specific course
30
+ - Retrieve a list of courses a specific student is enrolled in
31
+
32
+
33
+ ### GitHub
34
+ [![GitHub Logo](https://github.com/favicon.ico) D0nG4667](https://github.com/D0nG4667/Student-Management-System)
35
+
36
+ ### Explore this api below
37
+ ⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️
38
+
39
+
40
+ © 2024, Made with 💖 [Gabriel Okundaye](https://www.linkedin.com/in/dr-gabriel-okundaye)
41
+ """
main.py ADDED
@@ -0,0 +1,340 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+
4
+ from collections.abc import AsyncIterator
5
+ from contextlib import asynccontextmanager
6
+
7
+ from fastapi import FastAPI
8
+ from fastapi.responses import FileResponse
9
+ from fastapi.staticfiles import StaticFiles
10
+ from fastapi_cache import FastAPICache
11
+ from fastapi_cache.backends.inmemory import InMemoryBackend
12
+ # from fastapi_cache.coder import PickleCoder
13
+ from fastapi_cache.decorator import cache
14
+
15
+ from typing import Union, Optional, Type
16
+ from utils.student import Student
17
+ from utils.instructor import Instructor
18
+ from utils.course import Course
19
+ from utils.enrollment import Enrollment
20
+
21
+ from utils.logging import logging
22
+
23
+ from sqlmodel import SQLModel, select
24
+ from sqlmodel.sql.expression import SelectOfScalar
25
+ from sqlmodel.ext.asyncio.session import AsyncSession
26
+
27
+ from sqlalchemy.ext.asyncio import create_async_engine
28
+ from sqlalchemy import Engine
29
+
30
+ from typing import Dict
31
+
32
+
33
+ from config import (
34
+ # ONE_DAY_SEC,
35
+ ONE_WEEK_SEC,
36
+ ENV_PATH,
37
+ USE_REDIS_CACHE,
38
+ USE_POSTGRES_DB,
39
+ DESCRIPTION,
40
+ )
41
+
42
+ load_dotenv(ENV_PATH)
43
+
44
+ sms_resource: Dict[str,
45
+ Union[Engine, logging.Logger]] = {}
46
+
47
+
48
+ @asynccontextmanager
49
+ async def lifespan(_: FastAPI) -> AsyncIterator[None]:
50
+ # Cache
51
+ if USE_REDIS_CACHE:
52
+ from redis import asyncio as aioredis
53
+ from fastapi_cache.backends.redis import RedisBackend
54
+ url = os.getenv("REDIS_URL")
55
+ username = os.getenv("REDIS_USERNAME")
56
+ password = os.getenv("REDIS_PASSWORD")
57
+ redis = aioredis.from_url(url=url, username=username,
58
+ password=password, encoding="utf8", decode_responses=True)
59
+ FastAPICache.init(RedisBackend(redis), prefix="fastapi-cache")
60
+ else:
61
+ # In Memory cache
62
+ FastAPICache.init(InMemoryBackend())
63
+
64
+ # Database
65
+ if USE_POSTGRES_DB:
66
+ DATABASE_URL = os.getenv("POSTGRES_URL")
67
+ connect_args = {
68
+ "timeout": 60
69
+ }
70
+ else: # sqlite
71
+ DATABASE_URL = "sqlite+aiosqlite:///sms.db"
72
+ # Allow a single connection to be accessed from multiple threads.
73
+ connect_args = {"check_same_thread": False}
74
+
75
+ # Define the async engine
76
+ engine = create_async_engine(
77
+ DATABASE_URL, echo=True, connect_args=connect_args)
78
+
79
+ sms_resource["engine"] = engine
80
+
81
+ # Startup actions: create database tables
82
+ async with engine.begin() as conn:
83
+ await conn.run_sync(SQLModel.metadata.create_all)
84
+
85
+ # Logger
86
+ logger = logging.getLogger(__name__)
87
+
88
+ sms_resource["logger"] = logger
89
+
90
+ yield # Application code runs here
91
+
92
+ # Shutdown actions: close connections, etc.
93
+ await engine.dispose()
94
+
95
+
96
+ # FastAPI Object
97
+ app = FastAPI(
98
+ title='School Management System API',
99
+ version='1.0.0',
100
+ description=DESCRIPTION,
101
+ lifespan=lifespan,
102
+ )
103
+
104
+ app.mount("/assets", StaticFiles(directory="assets"), name="assets")
105
+
106
+
107
+ @app.get('/favicon.ico', include_in_schema=False)
108
+ @cache(expire=ONE_WEEK_SEC, namespace='eta_favicon') # Cache for 1 week
109
+ async def favicon():
110
+ file_name = "favicon.ico"
111
+ file_path = os.path.join(app.root_path, "assets", file_name)
112
+ return FileResponse(path=file_path, headers={"Content-Disposition": "attachment; filename=" + file_name})
113
+
114
+
115
+ # API
116
+
117
+ OneResult = Union[Student, Instructor, Course, Enrollment]
118
+ OneResultItem = Optional[OneResult]
119
+
120
+ BulkResult = Dict[str, OneResult]
121
+ BulkResultItem = Optional[BulkResult]
122
+
123
+ Result = Union[OneResult, BulkResult]
124
+ ResultItem = Union[OneResultItem, BulkResultItem]
125
+
126
+
127
+ class EndpointResponse(SQLModel):
128
+ execution_msg: str
129
+ execution_code: int
130
+ result: ResultItem
131
+
132
+
133
+ class ErrorResponse(SQLModel):
134
+ execution_msg: str
135
+ execution_code: int
136
+ error: Optional[str]
137
+
138
+
139
+ # Endpoints
140
+
141
+ # Status endpoint: check if api is online
142
+ @app.get('/', tags=['Home'])
143
+ async def status_check():
144
+ return {"Status": "API is online..."}
145
+
146
+
147
+ async def endpoint_output(endpoint_result: Result, code: int = 0, error: str = None) -> Union[ErrorResponse, EndpointResponse]:
148
+ msg = 'Execution failed'
149
+ output = ErrorResponse(**{'execution_msg': msg,
150
+ 'execution_code': code, 'error': error})
151
+
152
+ try:
153
+ if code != 0:
154
+ msg = 'Execution was successful'
155
+ output = EndpointResponse(
156
+ **{'execution_msg': msg,
157
+ 'execution_code': code, 'result': endpoint_result}
158
+ )
159
+
160
+ except Exception as e:
161
+ code = 0
162
+ msg = 'Execution failed'
163
+ errors = f"Omg, an error occurred. endpoint_output Error: {e} & endpoint_result Error: {error} & endpoint_result: {endpoint_result}"
164
+ output = ErrorResponse(**{'execution_msg': msg,
165
+ 'execution_code': code, 'error': errors})
166
+
167
+ sms_resource["logger"].error(error)
168
+
169
+ finally:
170
+ return output
171
+
172
+
173
+ # Caching Post requests is challenging
174
+ async def sms_posts(instance: Result, idx: str = None, action: str = "add") -> Union[ErrorResponse, EndpointResponse]:
175
+ async with AsyncSession(sms_resource["engine"]) as session:
176
+ code = 0
177
+ error = None
178
+ existing = await session.get(instance.__class__, idx)
179
+
180
+ # For add action, do db operation if instance is not existing. Other actions, do db operation if instance exists in db
181
+ checker = existing is None if action == "add" else existing is not None
182
+
183
+ try:
184
+ if checker:
185
+ if action == "delete":
186
+ session.delete(instance)
187
+ else: # add or update use add
188
+ session.add(instance)
189
+ await session.commit()
190
+ await session.refresh(instance)
191
+ code = 1
192
+ except Exception as e:
193
+ error = e
194
+
195
+ finally:
196
+ return await endpoint_output(instance, code, error)
197
+
198
+
199
+ # @cache(expire=ONE_DAY_SEC, namespace='sms_gets') # Cache for 1 day
200
+ async def sms_gets(sms_class: Type[Result], action: str = "first", idx: str = None, stmt: SelectOfScalar[Type[Result]] = None) -> Union[ErrorResponse, EndpointResponse]:
201
+ async with AsyncSession(sms_resource["engine"]) as session:
202
+ result = None
203
+ error = None
204
+ code = 1
205
+ try:
206
+ if action == "all":
207
+ statement = select(sms_class) if stmt is None else stmt
208
+ instance_list = (await session.exec(statement)).all()
209
+ if instance_list:
210
+ result = {
211
+ str(instance.id): instance for instance in instance_list}
212
+ elif action == "first":
213
+ statement = select(sms_class).where(
214
+ sms_class.id == idx) if stmt is None else stmt
215
+ result = (await session.exec(statement)).first()
216
+
217
+ except Exception as e:
218
+ code = 0
219
+ error = e
220
+ finally:
221
+ return await endpoint_output(result, code, error)
222
+
223
+
224
+ # Student Routes
225
+
226
+ @app.post('/api/v1/sms/add_student', tags=['Student'])
227
+ async def add_student(student: Student) -> Union[ErrorResponse, EndpointResponse]:
228
+ return await sms_posts(student, student.id, action="add")
229
+
230
+
231
+ @app.put('/api/v1/sms/update_student', tags=['Student'])
232
+ async def update_student(student: Student) -> Union[ErrorResponse, EndpointResponse]:
233
+ return await sms_posts(student, student.id, action="update")
234
+
235
+
236
+ @app.delete('/api/v1/sms/delete_student', tags=['Student'])
237
+ async def delete_student(student: Student) -> Union[ErrorResponse, EndpointResponse]:
238
+ return await sms_posts(student, student.id, action="delete")
239
+
240
+
241
+ @app.get("/api/v1/sms/students/{id}", tags=['Student'])
242
+ async def find_student(id: str) -> Union[ErrorResponse, EndpointResponse]:
243
+ return await sms_gets(Student, "first", id)
244
+
245
+
246
+ @app.get("/api/v1/sms/students", tags=['Student'])
247
+ async def all_students() -> Union[ErrorResponse, EndpointResponse]:
248
+ return await sms_gets(Student, "all")
249
+
250
+
251
+ # Instructor Routes
252
+
253
+ @app.post('/api/v1/sms/add_instructor', tags=['Instructor'])
254
+ async def add_instructor(instructor: Instructor) -> Union[ErrorResponse, EndpointResponse]:
255
+ return await sms_posts(instructor, instructor.id, action="add")
256
+
257
+
258
+ @app.put('/api/v1/sms/update_instructor', tags=['Instructor'])
259
+ async def update_instructor(instructor: Instructor) -> Union[ErrorResponse, EndpointResponse]:
260
+ return await sms_posts(instructor, instructor.id, action="update")
261
+
262
+
263
+ @app.delete('/api/v1/sms/delete_instructor', tags=['Instructor'])
264
+ async def delete_student(instructor: Instructor) -> Union[ErrorResponse, EndpointResponse]:
265
+ return await sms_posts(instructor, instructor.id, action="delete")
266
+
267
+
268
+ @app.get("/api/v1/sms/instructors/{id}", tags=['Instructor'])
269
+ async def find_instructor(id: str) -> Union[ErrorResponse, EndpointResponse]:
270
+ return await sms_gets(Instructor, "first", id)
271
+
272
+
273
+ @app.get("/api/v1/sms/instructors", tags=['Instructor'])
274
+ async def all_instructors() -> Union[ErrorResponse, EndpointResponse]:
275
+ return await sms_gets(Instructor, "all")
276
+
277
+
278
+ # Course Routes
279
+
280
+ @app.post('/api/v1/sms/add_course', tags=['Course'])
281
+ async def add_course(course: Course) -> Union[ErrorResponse, EndpointResponse]:
282
+ return await sms_posts(course, course.id, action="add")
283
+
284
+
285
+ @app.put('/api/v1/sms/update_course', tags=['Course'])
286
+ async def update_course(course: Course) -> Union[ErrorResponse, EndpointResponse]:
287
+ return await sms_posts(course, course.id, action="update")
288
+
289
+
290
+ @app.delete('/api/v1/sms/delete_course', tags=['Course'])
291
+ async def delete_student(course: Course) -> Union[ErrorResponse, EndpointResponse]:
292
+ return await sms_posts(course, course.id, action="delete")
293
+
294
+
295
+ @app.get("/api/v1/sms/courses/{id}", tags=['Course'])
296
+ async def find_course(id: str) -> Union[ErrorResponse, EndpointResponse]:
297
+ return await sms_gets(Course, "first", id)
298
+
299
+
300
+ @app.get("/api/v1/sms/courses", tags=['Course'])
301
+ async def all_courses() -> Union[ErrorResponse, EndpointResponse]:
302
+ return await sms_gets(Course, "all")
303
+
304
+
305
+ # Enroll Routes
306
+
307
+ @app.post('/api/v1/sms/enroll_student', tags=['Enroll'])
308
+ async def enroll_student(enrollment: Enrollment) -> Union[ErrorResponse, EndpointResponse]:
309
+ return await sms_posts(enrollment, enrollment.id, action="add")
310
+
311
+
312
+ @app.put('/api/v1/sms/update_enrolled_student', tags=['Enroll'])
313
+ async def update_enrolled_student(enrollment: Enrollment) -> Union[ErrorResponse, EndpointResponse]:
314
+ return await sms_posts(enrollment, enrollment.id, action="update")
315
+
316
+
317
+ @app.delete('/api/v1/sms/delete_enrolled_student', tags=['Enroll'])
318
+ async def delete_enrolled_student(enrollment: Enrollment) -> Union[ErrorResponse, EndpointResponse]:
319
+ return await sms_posts(enrollment, enrollment.id, action="delete")
320
+
321
+
322
+ @app.get('/api/v1/sms/enrollments/{id}', tags=['Enroll'])
323
+ async def find_enrollment_by_id(id: str) -> Union[ErrorResponse, EndpointResponse]:
324
+ return await sms_gets(Enrollment, "first", id)
325
+
326
+
327
+ @app.get('/api/v1/sms/enrollments/{student_id}', tags=['Enroll'])
328
+ async def find_enrollment_by_student_id(student_id: str) -> Union[ErrorResponse, EndpointResponse]:
329
+ stmt = select(Enrollment).where(Enrollment.student_id == student_id)
330
+ return await sms_gets(Enrollment, action="all", stmt=stmt)
331
+
332
+
333
+ @app.get('/api/v1/sms/enrollments', tags=['Enroll'])
334
+ async def all_enrolled_students() -> Union[ErrorResponse, EndpointResponse]:
335
+ return await sms_gets(Enrollment, "all")
336
+
337
+
338
+ @app.put('/api/v1/sms/grade_student', tags=['Grade'])
339
+ async def assign_grade(enrollment: Enrollment) -> Union[ErrorResponse, EndpointResponse]:
340
+ return await sms_posts(enrollment, enrollment.id, action="update")
requirements.txt ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ aioredis==1.3.1
2
+ aiosqlite==0.20.0
3
+ annotated-types==0.7.0
4
+ anyio==4.4.0
5
+ argon2-cffi==23.1.0
6
+ argon2-cffi-bindings==21.2.0
7
+ arrow==1.3.0
8
+ asn1crypto==1.5.1
9
+ asttokens==2.4.1
10
+ async-lru==2.0.4
11
+ async-timeout==4.0.3
12
+ asyncpg==0.29.0
13
+ attrs==23.2.0
14
+ Babel==2.14.0
15
+ beautifulsoup4==4.12.3
16
+ bleach==6.1.0
17
+ Brotli==1.1.0
18
+ cached-property==1.5.2
19
+ certifi==2024.7.4
20
+ cffi==1.16.0
21
+ charset-normalizer==3.3.2
22
+ click==8.1.7
23
+ colorama==0.4.6
24
+ comm==0.2.2
25
+ debugpy==1.8.2
26
+ decorator==5.1.1
27
+ defusedxml==0.7.1
28
+ entrypoints==0.4
29
+ exceptiongroup==1.2.2
30
+ executing==2.0.1
31
+ fastapi==0.112.1
32
+ fastapi-cache2==0.2.2
33
+ fastjsonschema==2.20.0
34
+ fqdn==1.5.1
35
+ greenlet==3.0.3
36
+ h11==0.14.0
37
+ h2==4.1.0
38
+ hiredis==3.0.0
39
+ hpack==4.0.0
40
+ httpcore==1.0.5
41
+ httpx==0.27.0
42
+ hyperframe==6.0.1
43
+ idna==3.7
44
+ importlib_metadata==8.2.0
45
+ importlib_resources==6.4.0
46
+ isoduration==20.11.0
47
+ jedi==0.19.1
48
+ Jinja2==3.1.4
49
+ json5==0.9.25
50
+ jsonpointer==3.0.0
51
+ jsonschema==4.23.0
52
+ jsonschema-specifications==2023.12.1
53
+ MarkupSafe==2.1.5
54
+ matplotlib-inline==0.1.7
55
+ mistune==3.0.2
56
+ nest_asyncio==1.6.0
57
+ overrides==7.7.0
58
+ packaging==24.1
59
+ pandocfilters==1.5.0
60
+ parso==0.8.4
61
+ pendulum==3.0.0
62
+ pg8000==1.31.2
63
+ pickleshare==0.7.5
64
+ pkgutil_resolve_name==1.3.10
65
+ platformdirs==4.2.2
66
+ prometheus_client==0.20.0
67
+ prompt_toolkit==3.0.47
68
+ psutil==6.0.0
69
+ # psycopg2==2.9.9
70
+ psycopg2-binary==2.9.9
71
+ pure_eval==0.2.3
72
+ pycparser==2.22
73
+ pydantic==2.8.2
74
+ pydantic_core==2.20.1
75
+ Pygments==2.18.0
76
+ PySocks==1.7.1
77
+ python-dateutil==2.9.0
78
+ python-dotenv==1.0.1
79
+ python-json-logger==2.0.7
80
+ pytz==2024.1
81
+ PyYAML==6.0.1
82
+ pyzmq==24.0.1
83
+ redis==5.0.8
84
+ referencing==0.35.1
85
+ requests==2.32.3
86
+ rfc3339-validator==0.1.4
87
+ rfc3986-validator==0.1.1
88
+ rpds-py==0.19.1
89
+ scramp==1.4.5
90
+ Send2Trash==1.8.3
91
+ setuptools==69.5.1
92
+ six==1.16.0
93
+ sniffio==1.3.1
94
+ soupsieve==2.5
95
+ SQLAlchemy==2.0.31
96
+ sqlmodel==0.0.21
97
+ stack-data==0.6.2
98
+ starlette==0.38.2
99
+ tenacity==9.0.0
100
+ terminado==0.18.1
101
+ time-machine==2.15.0
102
+ tinycss2==1.3.0
103
+ tomli==2.0.1
104
+ tornado==6.4.1
105
+ traitlets==5.14.3
106
+ types-python-dateutil==2.9.0.20240316
107
+ typing_extensions==4.12.2
108
+ typing-utils==0.1.0
109
+ tzdata==2024.1
110
+ uri-template==1.3.0
111
+ urllib3==2.2.2
112
+ uvicorn==0.30.6
113
+ wcwidth==0.2.13
114
+ webcolors==24.6.0
115
+ webencodings==0.5.1
116
+ websocket-client==1.8.0
117
+ wheel==0.43.0
118
+ zipp==3.19.2
119
+ zstandard==0.22.0
utils/__init__.py ADDED
File without changes
utils/course.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List
2
+ from sqlmodel import SQLModel, Field, Relationship
3
+
4
+ from utils.enums.course_name_id import CourseNameId
5
+ from .instructor import Instructor
6
+ from .enrollment import Enrollment
7
+
8
+
9
+ class Course(SQLModel, table=True):
10
+ """
11
+ Represents a course, including its name, unique identifier, enrolled students, and instructors.
12
+
13
+ Attributes:
14
+ id (str): A unique identifier for the course.
15
+ course_name (str): The name of the course.
16
+ enrolled_students (Dict[str, Enrollment]): A dictionary mapping student IDs to `Enrollment` instances for students enrolled in the course.
17
+ instructors (Dict[str, Instructor]): A dictionary mapping instructor IDs to `Instructor` instances for those teaching the course, or None if no instructors are assigned.
18
+
19
+ Notes:
20
+ Attributes defaults to an empty dictionary using default_factory. This ensures that each instance of the class gets a new dictionary instead of sharing a single instance across all instances of the model.
21
+ """
22
+
23
+ id: str = Field(
24
+ default=CourseNameId.INTRO_TO_PROGRAMMING.course_id, primary_key=True)
25
+ course_name: str = Field(
26
+ default=CourseNameId.INTRO_TO_PROGRAMMING.course_name)
27
+
28
+ enrollments: List[Enrollment] = Relationship(
29
+ back_populates="course", cascade_delete=True)
30
+ instructors: List[Instructor] = Relationship(back_populates="course")
31
+
32
+ def __str__(self) -> str:
33
+ """
34
+ Returns a string representation of the course.
35
+
36
+ Returns:
37
+ str: A string describing the course, including the course name, course ID, and the number of enrolled students.
38
+ """
39
+ return f"Course(course_name: {self.course_name}, id: {self.id}, enrolled_students: {self.enrollments}, total_students_enrolled: {len(self.enrollments)}, instructors: {self.instructors}, total_instructors: {len(self.instructors)})"
utils/enrollment.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+ from utils.enums.grade import Grade
3
+
4
+ from .student import Student
5
+
6
+ from sqlmodel import SQLModel, Field, Relationship
7
+
8
+
9
+ class Enrollment(SQLModel, table=True):
10
+ """
11
+ Represents an Enrollment, including the student, the course they are enrolled in, and the assigned grade.
12
+
13
+ Attributes:
14
+ id: (int): The enrollment ID
15
+ student_id (str): The ID of the student who is enrolled in the course.
16
+ course_id (str): The ID of the course in which the student is enrolled.
17
+ grade (Grade): The grade assigned to the student for the course. Default if NO_GRADE with enum value of None if no grade has been assigned yet.
18
+ """
19
+
20
+ id: int = Field(primary_key=True)
21
+ student_id: str = Field(foreign_key="student.id")
22
+ course_id: str = Field(foreign_key="course.id")
23
+ grade: Grade = Field(sa_column=Field(sa_type=Grade))
24
+
25
+ course: "Course" = Relationship(
26
+ back_populates="enrollments")
27
+
28
+ def assign_grade(self, grade: Grade) -> None:
29
+ """
30
+ Assigns a grade to the student for the course.
31
+
32
+ Args:
33
+ grade (Grade): The grade to assign to the student.
34
+ """
35
+ self.grade = grade
36
+
37
+ def __str__(self) -> str:
38
+ """
39
+ Returns a string representation of the Enrollment.
40
+
41
+ Returns:
42
+ str: A description of the enrollment including the student id_number, course_id, and grade.
43
+ """
44
+ return f"Enrollment(student id: {self.student_id}, course: {self.course_id}, grade: {self.grade.value})"
utils/enums/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ from .course_name_id import CourseNameId
2
+ from .department import Department
3
+ from .grade import Grade
4
+ from .major import Major
5
+
utils/enums/course_name_id.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from enum import Enum
2
+
3
+
4
+ class CourseNameId(Enum):
5
+ """
6
+ Enum representing various courses with their corresponding IDs.
7
+ """
8
+ INTRO_TO_PROGRAMMING = ("Introduction to Programming", "CS101")
9
+ DATA_STRUCTURES = ("Data Structures", "CS102")
10
+ ALGORITHMS = ("Algorithms", "CS201")
11
+ OPERATING_SYSTEMS = ("Operating Systems", "CS202")
12
+ DATABASE_SYSTEMS = ("Database Systems", "CS301")
13
+ LINEAR_ALGEBRA = ("Linear Algebra", "MATH101")
14
+ CALCULUS = ("Calculus", "MATH102")
15
+ ORGANIC_CHEMISTRY = ("Organic Chemistry", "CHEM101")
16
+ PHYSICS_I = ("Physics I", "PHYS101")
17
+ PHYSICS_II = ("Physics II", "PHYS102")
18
+ MICROECONOMICS = ("Microeconomics", "ECON101")
19
+ MACROECONOMICS = ("Macroeconomics", "ECON102")
20
+ INTRO_TO_PSYCHOLOGY = ("Introduction to Psychology", "PSYCH101")
21
+ SOCIOLOGY_THEORY = ("Sociological Theory", "SOC101")
22
+ AMERICAN_LITERATURE = ("American Literature", "ENGL101")
23
+ WORLD_HISTORY = ("World History", "HIST101")
24
+ CONSTITUTIONAL_LAW = ("Constitutional Law", "LAW101")
25
+ BIOCHEMISTRY = ("Biochemistry", "BIOCHEM101")
26
+ ENGINEERING_MECHANICS = ("Engineering Mechanics", "MECH101")
27
+ ART_HISTORY = ("Art History", "ART101")
28
+ MUSIC_THEORY = ("Music Theory", "MUSIC101")
29
+ ANATOMY = ("Anatomy", "BIO101")
30
+
31
+ def __init__(self, course_name: str, course_id: str) -> None:
32
+ self._course_name = course_name
33
+ self._course_id = course_id
34
+
35
+ @property
36
+ def course_name(self) -> str:
37
+ return self._course_name
38
+
39
+ @property
40
+ def course_id(self) -> str:
41
+ return self._course_id
42
+
43
+ def __str__(self) -> str:
44
+ return f"CourseNameId(course_name: {self._course_name}, course_id: {self._course_id})"
utils/enums/department.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from enum import Enum
2
+
3
+
4
+ class Department(Enum):
5
+ """
6
+ Enum representing valid departments.
7
+ """
8
+ MATHEMATICS = "Mathematics"
9
+ PHYSICS = "Physics"
10
+ CHEMISTRY = "Chemistry"
11
+ COMPUTER_SCIENCE = "Computer Science"
12
+ BIOLOGICAL_SCIENCES = "Biological Sciences"
13
+ ENGLISH = "English Language and Literature"
14
+ HISTORY = "History"
15
+ ECONOMICS = "Economics"
16
+ BUSINESS_ADMINISTRATION = "Business Administration"
17
+ PSYCHOLOGY = "Psychology"
18
+ SOCIOLOGY = "Sociology"
19
+ ENGINEERING = "Engineering"
20
+ ART_AND_DESIGN = "Art and Design"
21
+ MUSIC = "Music"
22
+ THEATER_ARTS = "Theater Arts"
23
+ PHILOSOPHY = "Philosophy"
24
+ POLITICAL_SCIENCE = "Political Science"
25
+ EDUCATION = "Education"
26
+ ENVIRONMENTAL_SCIENCE = "Environmental Science"
27
+ NURSING = "Nursing"
28
+ LAW = "Law"
29
+ MEDICINE = "Medicine"
30
+ DENTISTRY = "Dentistry"
31
+ ARCHITECTURE = "Architecture"
32
+ PHARMACY = "Pharmacy"
33
+ ASTRONOMY = "Astronomy"
34
+ LINGUISTICS = "Linguistics"
35
+ ANTHROPOLOGY = "Anthropology"
36
+ COMPUTER_ENGINEERING = "Computer Engineering"
37
+ ELECTRICAL_ENGINEERING = "Electrical Engineering"
38
+ MECHANICAL_ENGINEERING = "Mechanical Engineering"
39
+ CIVIL_ENGINEERING = "Civil Engineering"
40
+ CHEMICAL_ENGINEERING = "Chemical Engineering"
41
+ MATERIALS_SCIENCE = "Materials Science"
42
+ STATISTICS = "Statistics"
43
+ PUBLIC_HEALTH = "Public Health"
44
+ SOCIAL_WORK = "Social Work"
45
+ INTERNATIONAL_RELATIONS = "International Relations"
46
+ MEDIA_STUDIES = "Media Studies"
47
+ RELIGIOUS_STUDIES = "Religious Studies"
utils/enums/grade.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from enum import Enum
2
+
3
+
4
+ class Grade(str, Enum):
5
+ """
6
+ Enum representing possible letter grades.
7
+ """
8
+ A_PLUS = "A+"
9
+ A = "A"
10
+ A_MINUS = "A-"
11
+ B_PLUS = "B+"
12
+ B = "B"
13
+ B_MINUS = "B-"
14
+ C_PLUS = "C+"
15
+ C = "C"
16
+ C_MINUS = "C-"
17
+ D_PLUS = "D+"
18
+ D = "D"
19
+ D_MINUS = "D-"
20
+ F = "F"
21
+ PASS = "Pass"
22
+ FAIL = "Fail"
23
+ NO_GRADE = None
utils/enums/major.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from enum import Enum
2
+
3
+
4
+ class Major(str, Enum):
5
+ """
6
+ Enum representing various academic majors.
7
+ Add others.
8
+ """
9
+ COMPUTER_SCIENCE = "Computer Science"
10
+ ELECTRICAL_ENGINEERING = "Electrical Engineering"
11
+ MECHANICAL_ENGINEERING = "Mechanical Engineering"
12
+ CIVIL_ENGINEERING = "Civil Engineering"
13
+ CHEMICAL_ENGINEERING = "Chemical Engineering"
14
+ BIOLOGY = "Biology"
15
+ PHYSICS = "Physics"
16
+ CHEMISTRY = "Chemistry"
17
+ MATHEMATICS = "Mathematics"
18
+ BUSINESS_ADMINISTRATION = "Business Administration"
19
+ ECONOMICS = "Economics"
20
+ PSYCHOLOGY = "Psychology"
21
+ SOCIOLOGY = "Sociology"
22
+ ENGLISH = "English"
23
+ HISTORY = "History"
24
+ POLITICAL_SCIENCE = "Political Science"
25
+ PHILOSOPHY = "Philosophy"
26
+ ART_AND_DESIGN = "Art and Design"
27
+ MUSIC = "Music"
28
+ NURSING = "Nursing"
29
+ LAW = "Law"
30
+ MEDICINE = "Medicine"
31
+ ARCHITECTURE = "Architecture"
32
+ ENVIRONMENTAL_SCIENCE = "Environmental Science"
utils/instructor.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing_extensions import Self, Optional
2
+ from pydantic import model_validator
3
+ from sqlmodel import Field, Relationship
4
+
5
+
6
+ from .person import Person
7
+ from utils.enums.department import Department
8
+
9
+
10
+ class Instructor(Person, table=True):
11
+ """
12
+ Represents an Instructor, inheriting from Person, with an additional department field.
13
+
14
+ Attributes:
15
+ department (Department): The instructor's department.
16
+
17
+ Methods:
18
+ set_id() -> Self: An SQLmodel validator that automatically sets the instructor's ID with a "INS" prefix through handle_id method in the Person class.
19
+ """
20
+
21
+ department: Department = Field(sa_column=Field(sa_type=Department))
22
+
23
+ course_id: Optional[str] = Field(
24
+ default=None, foreign_key="course.id")
25
+ course: Optional["Course"] = Relationship(
26
+ back_populates="instructors")
27
+
28
+ @model_validator(mode='after')
29
+ def set_id(self) -> Self:
30
+ self.handle_id(prefix="INS")
31
+ return self
32
+
33
+ def __str__(self) -> str:
34
+ """
35
+ Returns a string representation of the instructor.
36
+
37
+ Returns:
38
+ str: A description of the instructor including their name, ID number, and department.
39
+ """
40
+
41
+ return f"Instructor(name: {self.name}, id: {self.id}, department: {self.department.value})"
utils/logging.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ import logging
2
+
3
+ logging.basicConfig(level=logging.ERROR,
4
+ format='%(asctime)s - %(levelname)s - %(message)s')
utils/person.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import uuid
2
+ from typing import Optional
3
+ from typing_extensions import Self
4
+
5
+ from pydantic import model_validator
6
+ from sqlmodel import SQLModel, Field
7
+
8
+
9
+ class Person(SQLModel):
10
+ """
11
+ A class representing a person with a first name, last name, and optional ID number.
12
+
13
+ Attributes:
14
+ id (str, optional): An optional identifier for the person, generated if not provided.
15
+ first_name (str): The first name of the person.
16
+ last_name (str): The last name of the person.
17
+ name (str, optional): The full name of the person, automatically generated from first and last name.
18
+ """
19
+ id: Optional[str] = Field(default=None, primary_key=True)
20
+ first_name: str
21
+ last_name: str
22
+ name: Optional[str] = Field(default=None)
23
+
24
+ @model_validator(mode='after')
25
+ def set_name(self) -> Self:
26
+ """
27
+ Sets the full name of the person by combining the first and last name.
28
+
29
+ Returns:
30
+ Self: The instance with the name attribute set.
31
+ """
32
+
33
+ self.name = f"{self.first_name} {self.last_name}"
34
+
35
+ return self
36
+
37
+ @model_validator(mode='after')
38
+ def set_id(self) -> Self:
39
+ """
40
+ Sets a unique ID for the person if one is not provided.
41
+
42
+ Returns:
43
+ Self: The instance with the id attribute set.
44
+ """
45
+
46
+ self.handle_id()
47
+ return self
48
+
49
+ def handle_id(self, prefix: str = "PER") -> None:
50
+ """
51
+ Generates or returns a unique identifier with a given prefix.
52
+
53
+ Args:
54
+ prefix (str): The prefix to be added to the ID.
55
+ id (Optional[str]): An optional ID. If provided, it is returned as-is.
56
+
57
+ Returns:
58
+ str: A unique identifier with the given prefix. If no ID is provided, a new ID is generated using UUID.
59
+ """
60
+
61
+ # Use id if give else generate id
62
+ if self.id is None or self.id == "":
63
+ # Generate a random UUID and take only the first 8 characters
64
+ short_uuid = str(uuid.uuid4())[:8]
65
+
66
+ # Add a custom prefix
67
+ self.id = f"{prefix}-{short_uuid}"
68
+
69
+ def __str__(self) -> str:
70
+ """
71
+ Returns a string representation of the person.
72
+
73
+ Returns:
74
+ str: A description of the person including their name and ID number.
75
+ """
76
+ return f"Person(name: {self.name}, id: {self.id})"
utils/student.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+ from pydantic import model_validator
3
+ from sqlmodel import Field
4
+ from typing_extensions import Self
5
+
6
+ from .person import Person
7
+ from utils.enums.major import Major
8
+
9
+
10
+ class Student(Person, table=True):
11
+ """
12
+ Represents a Student, inheriting from Person, with an additional major field.
13
+
14
+ Attributes:
15
+ major (Major): The major that the student is pursuing.
16
+
17
+ Methods:
18
+ set_id() -> Self: An SQLModel model validator that automatically sets the student's ID with a "STU" prefix through handle_id method in the Person class.
19
+ """
20
+
21
+ major: Major = Field(sa_column=Field(sa_type=Major))
22
+
23
+ @model_validator(mode='after')
24
+ def set_id(self) -> Self:
25
+ self.handle_id(prefix="STU")
26
+
27
+ return self
28
+
29
+ def __str__(self) -> str:
30
+ """
31
+ Returns a string representation of the student.
32
+
33
+ Returns:
34
+ str: A description of the student including their name, ID number, and major.
35
+ """
36
+
37
+ return f"Student(name: {self.name}, id: {self.id}, major: {self.major.value})"