diff --git a/app/db_models/__init__.py b/app/db_models/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/db_models/course.py b/app/db_models/course.py deleted file mode 100644 index 08cd01c..0000000 --- a/app/db_models/course.py +++ /dev/null @@ -1,11 +0,0 @@ -from sqlalchemy.dialects.mysql import TEXT, TINYINT, VARCHAR -from sqlmodel import Field, SQLModel - - -class Course(SQLModel, table=True): - dept: str = Field(primary_key=True, sa_type=VARCHAR(4)) - code_num: str = Field(primary_key=True, sa_type=VARCHAR(4)) - title: str = Field(sa_type=VARCHAR(255)) - desc_text: str = Field(sa_type=TEXT) - credit_min: int = Field(sa_type=TINYINT) - credit_max: int = Field(sa_type=TINYINT) diff --git a/app/db_models/course_attribute.py b/app/db_models/course_attribute.py deleted file mode 100644 index aaf214d..0000000 --- a/app/db_models/course_attribute.py +++ /dev/null @@ -1,19 +0,0 @@ -from sqlalchemy.dialects.mysql import ENUM, VARCHAR -from sqlmodel import Field, SQLModel - -_ATTR_ENUM = [ - "Communication Intensive", - "Culminating Exp/Capstone", - "Data Intensive I", - "Data Intensive II", - "Writing Intensive", - "HASS Inquiry", - "Introductory Level Course", - "PDII Option for Engr Majors", -] - - -class Course_Attribute(SQLModel, table=True): - dept: str = Field(primary_key=True, sa_type=VARCHAR(4)) - code_num: str = Field(primary_key=True, sa_type=VARCHAR(4)) - attr: str = Field(primary_key=True, sa_type=ENUM(*_ATTR_ENUM)) diff --git a/app/db_models/course_relationship.py b/app/db_models/course_relationship.py deleted file mode 100644 index faf24f7..0000000 --- a/app/db_models/course_relationship.py +++ /dev/null @@ -1,12 +0,0 @@ -from sqlalchemy.dialects.mysql import ENUM, VARCHAR -from sqlmodel import Field, SQLModel - -_RELATIONSHIP_ENUM = ["Coreq", "Cross"] - - -class Course_Relationship(SQLModel, table=True): - dept: str = Field(primary_key=True, sa_type=VARCHAR(4)) - code_num: str = Field(primary_key=True, sa_type=VARCHAR(4)) - relationship: str = Field(primary_key=True, sa_type=ENUM(*_RELATIONSHIP_ENUM)) - rel_dept: str = Field(primary_key=True, sa_type=VARCHAR(4)) - rel_code_num: str = Field(primary_key=True, sa_type=VARCHAR(4)) diff --git a/app/db_models/course_restriction.py b/app/db_models/course_restriction.py deleted file mode 100644 index a59deea..0000000 --- a/app/db_models/course_restriction.py +++ /dev/null @@ -1,13 +0,0 @@ -from sqlalchemy.dialects.mysql import ENUM, VARCHAR -from sqlmodel import Field, SQLModel - -_CATEGORY_ENUM = ["Major", "Level", "Classification"] -_RESTRICTION_ENUM = ["Must be", "May not be"] - - -class Course_Restriction(SQLModel, table=True): - dept: str = Field(primary_key=True, sa_type=VARCHAR(4)) - code_num: str = Field(primary_key=True, sa_type=VARCHAR(4)) - category: str = Field(primary_key=True, sa_type=ENUM(*_CATEGORY_ENUM)) - restr_rule: str = Field(primary_key=True, sa_type=ENUM(*_RESTRICTION_ENUM)) - restriction: str = Field(primary_key=True, sa_type=VARCHAR(255)) diff --git a/app/db_models/course_seats.py b/app/db_models/course_seats.py deleted file mode 100644 index 84698c8..0000000 --- a/app/db_models/course_seats.py +++ /dev/null @@ -1,13 +0,0 @@ -from sqlalchemy.dialects.mysql import ENUM, SMALLINT, VARCHAR -from sqlmodel import Field, SQLModel - -_SEM_ENUM = ["Fall", "Spring", "Summer"] - - -class Course_Seats(SQLModel, table=True): - sem_year: int = Field(primary_key=True, sa_type=SMALLINT) - semester: str = Field(primary_key=True, sa_type=ENUM(*_SEM_ENUM)) - dept: str = Field(primary_key=True, sa_type=VARCHAR(4)) - code_num: str = Field(primary_key=True, sa_type=VARCHAR(4)) - seats_filled: int = Field(sa_type=SMALLINT) - seats_total: int = Field(sa_type=SMALLINT) diff --git a/app/db_models/professor.py b/app/db_models/professor.py deleted file mode 100644 index e37abcc..0000000 --- a/app/db_models/professor.py +++ /dev/null @@ -1,12 +0,0 @@ -from sqlalchemy.dialects.mysql import ENUM, SMALLINT, VARCHAR -from sqlmodel import Field, SQLModel - -_SEMESTER_ENUM = ["Fall", "Spring", "Summer"] - - -class Professor(SQLModel, table=True): - sem_year: int = Field(primary_key=True, sa_type=SMALLINT) - semester: str = Field(primary_key=True, sa_type=ENUM(*_SEMESTER_ENUM)) - dept: str = Field(primary_key=True, sa_type=VARCHAR(4)) - code_num: str = Field(primary_key=True, sa_type=VARCHAR(4)) - prof_name: str = Field(primary_key=True, sa_type=VARCHAR(255)) diff --git a/app/dependencies.py b/app/dependencies.py index 8548032..2879e86 100644 --- a/app/dependencies.py +++ b/app/dependencies.py @@ -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): @@ -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 @@ -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 diff --git a/app/main.py b/app/main.py index 5fb676b..39fecc4 100644 --- a/app/main.py +++ b/app/main.py @@ -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: @@ -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) diff --git a/app/routers/course.py b/app/routers/course.py index 9ea7808..77adad4 100644 --- a/app/routers/course.py +++ b/app/routers/course.py @@ -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" @@ -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" @@ -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, @@ -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", ), @@ -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, @@ -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", ) @@ -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, ) ) @@ -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, @@ -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() diff --git a/requirements.txt b/requirements.txt index 963bcbf..84331f5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 @@ -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