From f953ae3dfe710924996e72388f6d0ba053e98312 Mon Sep 17 00:00:00 2001 From: Lucas Jensen Date: Thu, 2 May 2024 18:30:45 -0700 Subject: [PATCH] added docstrings to event controller and moved constants --- server/app/constants/__init__.py | 9 +++++ server/app/controllers/__init__.py | 4 +- server/app/controllers/base_controller.py | 18 ++++++--- server/app/controllers/controller.py | 10 ++++- server/app/controllers/events.py | 47 +++++++++++++++++++++-- server/app/db/conn.py | 6 +++ server/app/db/events.py | 10 +---- server/app/db/group.py | 2 +- server/app/db/musicians.py | 2 +- server/app/db/users.py | 2 +- server/app/main.py | 4 +- server/app/models/event.py | 4 -- server/app/models/group.py | 3 -- server/app/models/musician.py | 3 -- server/app/models/user.py | 3 -- server/app/scripts/seed.py | 15 ++++++-- 16 files changed, 100 insertions(+), 42 deletions(-) create mode 100644 server/app/constants/__init__.py diff --git a/server/app/constants/__init__.py b/server/app/constants/__init__.py new file mode 100644 index 0000000..4042b25 --- /dev/null +++ b/server/app/constants/__init__.py @@ -0,0 +1,9 @@ +ALLOWED_FILES_TYPES = ["image/jpeg", "image/png"] +ONE_MB = 1000000 + +# sql table names +SERIES_TABLE = "series" +EVENT_TABLE = "events" +GROUP_TABLE = "group_table" +MUSICIAN_TABLE = "musicians" +USER_TABLE = "users" diff --git a/server/app/controllers/__init__.py b/server/app/controllers/__init__.py index e719f08..f7d4b9d 100644 --- a/server/app/controllers/__init__.py +++ b/server/app/controllers/__init__.py @@ -1,3 +1,3 @@ -from .controller import Controller +from .controller import MainController -controller = Controller() +controller = MainController() diff --git a/server/app/controllers/base_controller.py b/server/app/controllers/base_controller.py index 81c67f5..5c4cc67 100644 --- a/server/app/controllers/base_controller.py +++ b/server/app/controllers/base_controller.py @@ -6,20 +6,25 @@ from pathlib import Path from fastapi import HTTPException, UploadFile, status from icecream import ic +from app.constants import ALLOWED_FILES_TYPES, ONE_MB from app.db.base_queries import BaseQueries -ALLOWED_FILES_TYPES = ["image/jpeg", "image/png"] -MAX_FILE_SIZE = 1000000 # 1 MB - class BaseController: + """ + A generic controller class which includes logging, image verification, and other common methods. + Model-specific controllers should inherit from this class and this class should not be instantiated directly. + """ + def __init__(self) -> None: self.db: BaseQueries = None # type: ignore self.ALL_FILES = ALLOWED_FILES_TYPES - self.MAX_FILE_SIZE = MAX_FILE_SIZE + self.MAX_FILE_SIZE = ONE_MB async def verify_image(self, file: UploadFile) -> bytes: - print("verifying image") + """ + Verifies that the file is an image and is within the maximum file size. + """ if file.content_type not in self.ALL_FILES: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -34,6 +39,9 @@ class BaseController: return image_file def log_error(self, e: Exception) -> None: + """ + Logs an error to a timestamped text file in the logs directory. + """ curr_dir = Path(__file__).parent log_dir = curr_dir / "logs" log_dir.mkdir(exist_ok=True) diff --git a/server/app/controllers/controller.py b/server/app/controllers/controller.py index 6b25c48..ea5875b 100644 --- a/server/app/controllers/controller.py +++ b/server/app/controllers/controller.py @@ -13,7 +13,15 @@ from app.models.musician import Musician from app.models.user import User -class Controller: +class MainController: + """ + The main controller and entry point for all API requests. + All methods are either pass-throughs to the appropriate controller or + are used to coordinate multiple controllers. + + token-based authentication is handled here as needed per the nature of the data being accessed. + """ + def __init__(self) -> None: self.event_controller = EventController() self.musician_controller = MusicianController() diff --git a/server/app/controllers/events.py b/server/app/controllers/events.py index b7585ce..fe848e4 100644 --- a/server/app/controllers/events.py +++ b/server/app/controllers/events.py @@ -10,11 +10,25 @@ from app.models.event import Event, EventSeries, NewEventSeries class EventController(BaseController): + """ + Handles all event-related operations and serves as an intermediate controller between + the main controller and the model layer. + Inherits from BaseController, which provides logging and other generic methods. + + Testing: pass a mocked EventQueries object to the constructor. + """ + def __init__(self, eq=event_queries) -> None: super().__init__() self.db: EventQueries = eq - def _all_series(self, data: list[dict]) -> list[EventSeries]: + def _all_series(self, data: list[dict]) -> dict[str, EventSeries]: + """ + Helper method to instantiate EventSeries objects from sql rows (a list of dictionaries). + Instantiation is done by destructuring the dictionary into the EventSeries constructor. + Should not be called directly; use get_all_series() instead. + series.name is a required and unique field and can reliably be used as a key in a dictionary. + """ all_series: dict[str, EventSeries] = {} for event_series_row in data: @@ -24,13 +38,20 @@ class EventController(BaseController): if event_series_row.get("event_id"): all_series[series_name].events.append(Event(**event_series_row)) - return [series for series in all_series.values()] + return all_series async def get_all_series(self) -> list[EventSeries]: + """ + Attempts to create and return a list of EventSeries objects and is consumed by the main controller. + Will trigger a 500 status code if any exception is raised, and log the error to a timestamped text file. + + The list of EventSeries is created by calling the _all_series() helper method, which provided this data + as a list of dicts. + """ series_data = await self.db.select_all_series() try: - return self._all_series(series_data) + return [series for series in self._all_series(series_data).values()] except Exception as e: self.log_error(e) raise HTTPException( @@ -39,6 +60,9 @@ class EventController(BaseController): ) async def get_one_series_by_id(self, series_id: int) -> EventSeries: + """ + Builds and returns a single EventSeries object by its numeric ID. + """ if not (data := await self.db.select_one_series_by_id(series_id)): raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Event not found" @@ -54,6 +78,9 @@ class EventController(BaseController): ) async def create_series(self, series: NewEventSeries) -> EventSeries: + """ + Takes a NewEventSeries object and passes it to the database layer for insertion. + """ try: inserted_id = await self.db.insert_one_series(series) for new_event in series.events: @@ -66,12 +93,19 @@ class EventController(BaseController): ) async def add_series_poster(self, series_id, poster: UploadFile) -> EventSeries: + """ + Adds (or updates) a poster image to a series. + Actual image storage is done with Cloudinary and the public ID is stored in the database. + """ series = await self.get_one_series_by_id(series_id) series.poster_id = await self._upload_poster(poster) await self.db.update_series_poster(series) return await self.get_one_series_by_id(series.series_id) async def _upload_poster(self, poster: UploadFile) -> str: + """ + Uploads a poster image to Cloudinary and returns the public ID for storage in the database. + """ image_file = await self.verify_image(poster) try: data = uploader.upload(image_file) @@ -83,12 +117,17 @@ class EventController(BaseController): ) async def delete_series(self, id: int) -> None: + """ + Ensures an EventSeries object exists and then deletes it from the database + """ series = await self.get_one_series_by_id(id) await self.db.delete_one_series(series) async def update_series(self, route_id: int, series: EventSeries) -> EventSeries: + """ + Updates an existing EventSeries object in the database. + """ if route_id != series.series_id: - print("error") raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="ID in URL does not match ID in request body", diff --git a/server/app/db/conn.py b/server/app/db/conn.py index a3f281a..b597fe3 100644 --- a/server/app/db/conn.py +++ b/server/app/db/conn.py @@ -9,6 +9,12 @@ class DBException(Exception): def connect_db() -> mysql.connector.MySQLConnection: + """ + Connects to the MySQL database using credentials from the .env file. + Returns a MySQLConnection object which can be used by the database query layer. + + Credential values are validated and an exception is raised if any are missing. + """ load_dotenv() host = os.getenv("DB_HOST") user = os.getenv("DB_USER") diff --git a/server/app/db/events.py b/server/app/db/events.py index 211a375..2d3cf57 100644 --- a/server/app/db/events.py +++ b/server/app/db/events.py @@ -2,15 +2,9 @@ from asyncio import gather from icecream import ic +from app.constants import EVENT_TABLE, SERIES_TABLE from app.db.base_queries import BaseQueries -from app.models.event import ( - EVENT_TABLE, - SERIES_TABLE, - Event, - EventSeries, - NewEvent, - NewEventSeries, -) +from app.models.event import Event, EventSeries, NewEvent, NewEventSeries class EventQueries(BaseQueries): diff --git a/server/app/db/group.py b/server/app/db/group.py index f2a43a4..d1f4c45 100644 --- a/server/app/db/group.py +++ b/server/app/db/group.py @@ -1,5 +1,5 @@ +from app.constants import GROUP_TABLE from app.db.base_queries import BaseQueries -from app.models.group import GROUP_TABLE class GroupQueries(BaseQueries): diff --git a/server/app/db/musicians.py b/server/app/db/musicians.py index bbbafff..518813c 100644 --- a/server/app/db/musicians.py +++ b/server/app/db/musicians.py @@ -1,8 +1,8 @@ from icecream import ic +from app.constants import MUSICIAN_TABLE from app.db.base_queries import BaseQueries from app.db.conn import connect_db -from app.models.musician import MUSICIAN_TABLE class MusicianQueries(BaseQueries): diff --git a/server/app/db/users.py b/server/app/db/users.py index 77401eb..6b59e0b 100644 --- a/server/app/db/users.py +++ b/server/app/db/users.py @@ -1,5 +1,5 @@ +from app.constants import USER_TABLE from app.db.base_queries import BaseQueries -from app.models.user import USER_TABLE class UserQueries(BaseQueries): diff --git a/server/app/main.py b/server/app/main.py index 92b8b15..6bfae60 100644 --- a/server/app/main.py +++ b/server/app/main.py @@ -3,7 +3,7 @@ from asyncio import gather from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from app.controllers import Controller +from app.controllers import MainController from app.models.tgd import TheGrapefruitsDuo from app.routers.contact import router as contact_router from app.routers.events import router as event_router @@ -23,7 +23,7 @@ app.include_router(contact_router) app.include_router(event_router) app.include_router(user_router) -controller = Controller() +controller = MainController() origins = [ "http://localhost:3000", diff --git a/server/app/models/event.py b/server/app/models/event.py index d931c53..7315ff7 100644 --- a/server/app/models/event.py +++ b/server/app/models/event.py @@ -30,7 +30,3 @@ class EventSeries(NewEventSeries): series_id: int events: list[Event] poster_id: Optional[str] = None - - -SERIES_TABLE = "series" -EVENT_TABLE = "events" diff --git a/server/app/models/group.py b/server/app/models/group.py index 5b5ff1e..4a36fbf 100644 --- a/server/app/models/group.py +++ b/server/app/models/group.py @@ -5,6 +5,3 @@ class Group(BaseModel): name: str bio: str id: int | None = None - - -GROUP_TABLE = "group_table" diff --git a/server/app/models/musician.py b/server/app/models/musician.py index 8a10938..8af42d5 100644 --- a/server/app/models/musician.py +++ b/server/app/models/musician.py @@ -11,6 +11,3 @@ class NewMusician(BaseModel): class Musician(NewMusician): id: int - - -MUSICIAN_TABLE = "musicians" diff --git a/server/app/models/user.py b/server/app/models/user.py index 3aa8bed..b803219 100644 --- a/server/app/models/user.py +++ b/server/app/models/user.py @@ -8,6 +8,3 @@ class User(BaseModel): email: str sub: Optional[str] = None id: int | None = None - - -USER_TABLE = "users" diff --git a/server/app/scripts/seed.py b/server/app/scripts/seed.py index af77044..7458ba1 100644 --- a/server/app/scripts/seed.py +++ b/server/app/scripts/seed.py @@ -2,11 +2,18 @@ from datetime import datetime from dotenv import load_dotenv +from app.constants import ( + EVENT_TABLE, + GROUP_TABLE, + MUSICIAN_TABLE, + SERIES_TABLE, + USER_TABLE, +) from app.db.conn import connect_db -from app.models.event import EVENT_TABLE, SERIES_TABLE, Event, EventSeries -from app.models.group import GROUP_TABLE, Group -from app.models.musician import MUSICIAN_TABLE, NewMusician -from app.models.user import USER_TABLE, User +from app.models.event import Event, EventSeries +from app.models.group import Group +from app.models.musician import NewMusician +from app.models.user import User margarite: NewMusician = NewMusician( name="Margarite Waddell",