Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file removed app/db_models/__init__.py
Empty file.
11 changes: 0 additions & 11 deletions app/db_models/course.py

This file was deleted.

19 changes: 0 additions & 19 deletions app/db_models/course_attribute.py

This file was deleted.

12 changes: 0 additions & 12 deletions app/db_models/course_relationship.py

This file was deleted.

13 changes: 0 additions & 13 deletions app/db_models/course_restriction.py

This file was deleted.

13 changes: 0 additions & 13 deletions app/db_models/course_seats.py

This file was deleted.

12 changes: 0 additions & 12 deletions app/db_models/professor.py

This file was deleted.

29 changes: 18 additions & 11 deletions app/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
from fastapi import Depends, FastAPI
from pydantic_settings import BaseSettings, SettingsConfigDict
from sqlalchemy.engine import Engine
from sqlmodel import Session, SQLModel, create_engine
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
import carpi_data_model.models as models


class _Settings(BaseSettings):
Expand All @@ -22,20 +24,23 @@ class _Settings(BaseSettings):

_settings = _Settings()
_engine: Engine | None = None
_session_maker: sessionmaker | None = None


@asynccontextmanager
async def lifespan_func(app: FastAPI) -> AsyncGenerator[None, None]:
# Initialize database connection pool
global _engine
_engine = create_engine(
url=f"{_settings.db_dialect}+{_settings.db_api}"
+ f"://{_settings.db_username}:{_settings.db_password}"
+ f"@{_settings.db_hostname}/{_settings.db_schema}",
# echo=True,
)
# Creates tables in database based on SQLModel table models
SQLModel.metadata.create_all(_engine)
global _engine, _session_maker
if _engine is None:
_engine = create_engine(
url=f"{_settings.db_dialect}+{_settings.db_api}"
+ f"://{_settings.db_username}:{_settings.db_password}"
+ f"@{_settings.db_hostname}/{_settings.db_schema}",
# echo=True,
)
_session_maker = sessionmaker(_engine)
# Creates tables in database based on SQLAlchemy table models
models.Base.metadata.create_all(_engine)

yield

Expand All @@ -47,7 +52,9 @@ def get_app_settings() -> _Settings:


def get_db_session() -> Generator[Session, None, None]:
with Session(_engine) as session:
if _session_maker is None:
raise RuntimeError("Database engine is not initialized")
with _session_maker() as session:
yield session


Expand Down
9 changes: 3 additions & 6 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from fastapi import APIRouter, FastAPI
from fastapi.middleware.cors import CORSMiddleware

from app import lifespan_func
from app import lifespan_func, logger


def scan_and_include_routers(app: FastAPI) -> None:
Expand All @@ -15,11 +15,8 @@ def scan_and_include_routers(app: FastAPI) -> None:
module = importlib.import_module(f"{package_module_name}.{module_name}")
for attr_name in dir(module):
attr = getattr(module, attr_name)
if (
isinstance(attr, APIRouter)
and getattr(attr, "__module__", None) == module.__name__
):

if isinstance(attr, APIRouter):
logger.info(f"Including router from {module.__name__}")
app.include_router(attr)


Expand Down
68 changes: 40 additions & 28 deletions app/routers/course.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
from enum import Enum

from carpi_data_model.models import (
Attribute,
Course,
Course_Attribute,
Course_Offering,
Subject,
)
from fastapi import APIRouter
from sqlmodel import and_, desc, distinct, func, or_, select
from sqlmodel.sql.expression import Select, SelectOfScalar
from sqlalchemy import and_, desc, distinct, func, or_, select
from sqlalchemy.sql import Select

from app import SessionDep
from app.db_models.course import Course
from app.db_models.course_attribute import Course_Attribute
from app.db_models.course_seats import Course_Seats


class CourseFilter(str, Enum):
departments = "departments"
subjects = "subjects"
attributes = "attributes"
semesters = "semesters"

Expand All @@ -29,21 +33,25 @@ def search_course_query(
dept_filter_regex: str,
attr_filter_regex: str,
sem_filter_regex: str,
) -> Select | SelectOfScalar:
) -> Select:
return (
select(
Course.dept,
Course.subj_code,
Course.code_num,
Course.title,
Course.desc_text,
Course.credit_min,
Course.credit_max,
func.group_concat(
distinct(func.concat(Course_Seats.semester, " ", Course_Seats.sem_year))
distinct(
func.concat(Course_Offering.semester, " ", Course_Offering.sem_year)
)
).label("sem_list"),
func.group_concat(distinct(Course_Attribute.attr)).label("attr_list"),
func.group_concat(distinct(Course_Attribute.attr_code)).label("attr_list"),
func.regexp_like(
func.concat(Course.dept, " ", Course.code_num), search_code_regex, "i"
func.concat(Course.subj_code, " ", Course.code_num),
search_code_regex,
"i",
).label("code_match"),
func.regexp_like(Course.title, search_full_regex, "i").label(
"title_exact_match"
Expand All @@ -60,22 +68,22 @@ def search_course_query(
),
)
.join(
Course_Seats,
Course_Offering,
and_(
Course.dept == Course_Seats.dept,
Course.code_num == Course_Seats.code_num,
Course.subj_code == Course_Offering.subj_code,
Course.code_num == Course_Offering.code_num,
),
)
.outerjoin(
Course_Attribute,
and_(
Course.dept == Course_Attribute.dept,
Course.subj_code == Course_Attribute.subj_code,
Course.code_num == Course_Attribute.code_num,
),
)
.where(func.regexp_like(Course.dept, dept_filter_regex, "i"))
.where(func.regexp_like(Course.subj_code, dept_filter_regex, "i"))
.group_by(
Course.dept,
Course.subj_code,
Course.code_num,
Course.title,
Course.desc_text,
Expand All @@ -85,7 +93,7 @@ def search_course_query(
.having(
or_(
func.regexp_like(
func.concat(Course.dept, " ", Course.code_num),
func.concat(Course.subj_code, " ", Course.code_num),
search_code_regex,
"i",
),
Expand All @@ -96,14 +104,18 @@ def search_course_query(
func.regexp_like(Course.title, search_abbrev_regex, "i"),
),
func.regexp_like(
func.ifnull(func.group_concat(distinct(Course_Attribute.attr)), ""),
func.ifnull(
func.group_concat(distinct(Course_Attribute.attr_code)), ""
),
attr_filter_regex,
"i",
),
func.regexp_like(
func.group_concat(
distinct(
func.concat(Course_Seats.semester, " ", Course_Seats.sem_year)
func.concat(
Course_Offering.semester, " ", Course_Offering.sem_year
)
)
),
sem_filter_regex,
Expand All @@ -113,7 +125,7 @@ def search_course_query(
.order_by(
desc(
func.regexp_like(
func.concat(Course.dept, " ", Course.code_num),
func.concat(Course.subj_code, " ", Course.code_num),
search_code_regex,
"i",
)
Expand All @@ -124,7 +136,7 @@ def search_course_query(
desc(func.regexp_like(Course.title, search_acronym_regex, "i")),
desc(func.regexp_like(Course.title, search_abbrev_regex, "i")),
Course.code_num,
Course.dept,
Course.subj_code,
)
)

Expand Down Expand Up @@ -189,7 +201,7 @@ def search_course(
regex_abbrev = regex_abbrev[:-3]
else:
regex_abbrev = "a^"
results = session.exec(
results = session.execute(
search_course_query(
regex_code,
regex_full,
Expand All @@ -208,12 +220,12 @@ def search_course(
@router.get("/filter/values/{filter}")
def get_filter_values(session: SessionDep, filter: CourseFilter) -> list[str]:
column = None
if filter is CourseFilter.departments:
column = Course.dept
if filter is CourseFilter.subjects:
column = Subject.subj_code
elif filter is CourseFilter.attributes:
column = Course_Attribute.attr
column = Attribute.attr_code
elif filter is CourseFilter.semesters:
column = Course_Seats.semester
column = Course_Offering.semester
else:
return None
return session.exec(select(column).distinct()).all()
return session.execute(select(column).distinct()).scalars().all()
25 changes: 13 additions & 12 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@
# uvloop is an optional dependency for improved performance over asyncio, you may
# uncomment it if using MacOS or any POSIX-compliant OS.

annotated-doc==0.0.4
annotated-types==0.7.0
anyio==4.11.0
certifi==2025.10.5
carpi-data-model @ git+https://github.com/Project-CARPI/database-schema.git
certifi==2025.11.12
click==8.3.0
colorama==0.4.6
dnspython==2.8.0
email-validator==2.3.0
fastapi==0.119.1
fastapi-cli==0.0.14
fastapi==0.121.2
fastapi-cli==0.0.16
fastapi-cloud-cli==0.3.1
greenlet==3.2.4
h11==0.16.0
Expand All @@ -23,27 +25,26 @@ markdown-it-py==4.0.0
MarkupSafe==3.0.3
mdurl==0.1.2
mysql-connector-python==9.5.0
pydantic==2.12.3
pydantic-settings==2.11.0
pydantic_core==2.41.4
pydantic==2.12.4
pydantic-settings==2.12.0
pydantic_core==2.41.5
Pygments==2.19.2
python-dotenv==1.1.1
python-dotenv==1.2.1
python-multipart==0.0.20
PyYAML==6.0.3
rich==14.2.0
rich-toolkit==0.15.1
rignore==0.7.1
sentry-sdk==2.42.1
rignore==0.7.6
sentry-sdk==2.44.0
shellingham==1.5.4
sniffio==1.3.1
SQLAlchemy==2.0.44
sqlmodel==0.0.27
starlette==0.48.0
starlette==0.49.3
typer==0.20.0
typing-inspection==0.4.2
typing_extensions==4.15.0
urllib3==2.5.0
# uvloop==0.22.1
uvicorn==0.38.0
# uvloop==0.22.1
watchfiles==1.1.1
websockets==15.0.1