switched docstring format to one-line-sphinx

This commit is contained in:
Lucas Jensen
2024-05-05 13:32:31 -07:00
parent ab0fae44f7
commit e1a29b398e
12 changed files with 300 additions and 243 deletions

View File

@@ -2,10 +2,16 @@ import smtplib
from email.mime.text import MIMEText from email.mime.text import MIMEText
from os import getenv from os import getenv
HOST = "grapefruitswebsite@gmail.com" from app.constants import HOST
def send_email(subject: str, body: str) -> None: def send_email(subject: str, body: str) -> None:
"""
Sends an email using the Gmail SMTP server.
:param str subject: The subject of the email
:param str body: The body of the email
"""
password = getenv("APP_PASSWORD") password = getenv("APP_PASSWORD")
email = getenv("EMAIL") email = getenv("EMAIL")
msg = MIMEText(body) msg = MIMEText(body)

View File

@@ -1,7 +1,6 @@
# Set your Cloudinary credentials # Set your Cloudinary credentials
# ============================== # ==============================
from pprint import pprint
from dotenv import load_dotenv from dotenv import load_dotenv
@@ -22,10 +21,20 @@ uploader = cloudinary.uploader
class CloudinaryException(Exception): class CloudinaryException(Exception):
"""
Custom exception for Cloudinary errors.
"""
pass pass
def delete_image(public_id: str) -> None: def delete_image(public_id: str) -> None:
"""
Deletes an image from the Cloudinary cloud.
:param str public_id: The public ID of the image to delete
:raises CloudinaryException: If the image deletion fails
"""
result = uploader.destroy(public_id) result = uploader.destroy(public_id)
if result.get("result") != "ok": if result.get("result") != "ok":
@@ -33,16 +42,25 @@ def delete_image(public_id: str) -> None:
def get_image_data(public_id: str) -> dict: def get_image_data(public_id: str) -> dict:
"""
Retrieves the metadata for an image from the Cloudinary cloud.
:param str public_id: The public ID of the image to retrieve
:return dict: The metadata for the image
"""
data = cloudinary.api.resource(public_id) data = cloudinary.api.resource(public_id)
return data return data
def get_image_url(public_id: str) -> str: def get_image_url(public_id: str) -> str:
"""
Retrieves the URL for an image from the Cloudinary cloud.
:param str public_id: The public ID of the image to retrieve
:raises CloudinaryException: If the image URL retrieval fails
:return str: The URL of the image
"""
url = cloudinary.utils.cloudinary_url(public_id)[0] url = cloudinary.utils.cloudinary_url(public_id)[0]
if url is None: if url is None:
raise CloudinaryException("Failed to get image URL") raise CloudinaryException("Failed to get image URL")
return url return url
if __name__ == "__main__":
image_id = "coco_copy_jywbxm"

View File

@@ -7,3 +7,6 @@ EVENT_TABLE = "events"
GROUP_TABLE = "group_table" GROUP_TABLE = "group_table"
MUSICIAN_TABLE = "musicians" MUSICIAN_TABLE = "musicians"
USER_TABLE = "users" USER_TABLE = "users"
# contact form email
HOST = "grapefruitswebsite@gmail.com"

View File

@@ -17,21 +17,21 @@ class BaseController:
""" """
def __init__(self) -> None: def __init__(self) -> None:
"""
Initializes the BaseController with a BaseQueries object.
"""
self.db: BaseQueries = None # type: ignore self.db: BaseQueries = None # type: ignore
self.ALL_FILES = ALLOWED_FILES_TYPES self.ALL_FILES = ALLOWED_FILES_TYPES
self.MAX_FILE_SIZE = ONE_MB self.MAX_FILE_SIZE = ONE_MB
def verify_image(self, file: UploadFile) -> bytes: def verify_image(self, file: UploadFile) -> bytes:
"""Verifies that the file is an image and is within the maximum file size. """
Verifies that the file is an image and is within the maximum file size.
Args: :param UploadFile file: The file to be verified
file (UploadFile): The file to be verified :raises HTTPException: If the file type is not allowed (status code 400)
:raises HTTPException: If the file size exceeds the maximum (status code 400)
Raises: :return bytes: The image file as bytes
HTTPException: If the file is not an image or exceeds the maximum file size (status code 400)
Returns:
bytes: The file contents as bytes
""" """
if file.content_type not in self.ALL_FILES: if file.content_type not in self.ALL_FILES:
raise HTTPException( raise HTTPException(
@@ -48,10 +48,10 @@ class BaseController:
return image_file return image_file
def log_error(self, e: Exception) -> None: def log_error(self, e: Exception) -> None:
"""Logs an error to a timestamped text file in the logs directory. """
Logs an error to a timestamped text file in the logs directory.
Args: :param Exception e: The exception to be logged
e (Exception): Any exception object
""" """
curr_dir = Path(__file__).parent curr_dir = Path(__file__).parent
log_dir = curr_dir / "logs" log_dir = curr_dir / "logs"

View File

@@ -1,3 +1,5 @@
from typing import Optional
from fastapi import HTTPException, UploadFile, status from fastapi import HTTPException, UploadFile, status
from fastapi.security import HTTPAuthorizationCredentials from fastapi.security import HTTPAuthorizationCredentials
from icecream import ic from icecream import ic
@@ -22,19 +24,11 @@ from app.models.user import User
class MainController: class MainController:
""" """
The main controller and entry point for all API requests. 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.
All methods are asynchronous to facilitate asynchronous calls from the Router layer.
token-based authentication is handled here as needed per the nature of the data being accessed.
Testing: pass mocked sub-controllers to the constructor.
""" """
def __init__( def __init__(
self, self,
event_controller=event_controller, event_controller: EventController = event_controller,
musicians_controller=musicians_controller, musicians_controller=musicians_controller,
user_controller=user_controller, user_controller=user_controller,
group_controller=group_controller, group_controller=group_controller,
@@ -47,9 +41,20 @@ class MainController:
self.oauth_token = oauth_token self.oauth_token = oauth_token
async def get_musicians(self) -> list[Musician]: async def get_musicians(self) -> list[Musician]:
"""
Retrieves all musicians and returns them as a list.
:return list[Musician]: _description_
"""
return self.musician_controller.get_musicians() return self.musician_controller.get_musicians()
async def get_musician(self, musician_id: int) -> Musician: async def get_musician(self, musician_id: int) -> Musician:
"""
Retrieves a single musician by numeric ID.
:param int musician_id: The ID of the musician to retrieve
:return Musician: The musician object for a response body
"""
return self.musician_controller.get_musician(musician_id) return self.musician_controller.get_musician(musician_id)
async def update_musician( async def update_musician(
@@ -57,8 +62,18 @@ class MainController:
musician: Musician, musician: Musician,
url_param_id: int, url_param_id: int,
token: HTTPAuthorizationCredentials, token: HTTPAuthorizationCredentials,
file: UploadFile | None = None, file: Optional[UploadFile] = None,
) -> Musician: ) -> Musician:
"""
Updates a musician in the database and returns the updated musician object.
:param Musician musician: The musician object to update
:param int url_param_id: The ID of the musician in the URL
:param HTTPAuthorizationCredentials token: The OAuth token
:param Optional[UploadFile] file: The new headshot file, defaults to None
:raises HTTPException: If the ID in the URL does not match the ID in the request body (status code 400)
:return Musician: The updated musician object which is suitable for a response body
"""
if musician.id != url_param_id: if musician.id != url_param_id:
raise HTTPException( raise HTTPException(
@@ -74,14 +89,32 @@ class MainController:
) )
async def get_events(self) -> list[EventSeries]: async def get_events(self) -> list[EventSeries]:
"""
Retrieves all event series and returns them as a list.
:return list[EventSeries]: a list of EventSeries objects for a response body
"""
return self.event_controller.get_all_series() return self.event_controller.get_all_series()
async def get_event(self, series_id: int) -> EventSeries: async def get_event(self, series_id: int) -> EventSeries:
"""
Retrieves a single event series by numeric ID.
:param int series_id: The ID of the event series to retrieve
:return EventSeries: The event series object for a response body
"""
return self.event_controller.get_one_series_by_id(series_id) return self.event_controller.get_one_series_by_id(series_id)
async def create_event( async def create_event(
self, series: NewEventSeries, token: HTTPAuthorizationCredentials self, series: NewEventSeries, token: HTTPAuthorizationCredentials
) -> EventSeries: ) -> EventSeries:
"""
Creates a new event series and returns the created event series object.
:param NewEventSeries series: The new event series object
:param HTTPAuthorizationCredentials token: The OAuth token
:return EventSeries: The newly created event series object which is suitable for a response body
"""
_, sub = self.oauth_token.email_and_sub(token) _, sub = self.oauth_token.email_and_sub(token)
self.user_controller.get_user_by_sub(sub) self.user_controller.get_user_by_sub(sub)
return self.event_controller.create_series(series) return self.event_controller.create_series(series)
@@ -89,6 +122,14 @@ class MainController:
async def add_series_poster( async def add_series_poster(
self, series_id: int, poster: UploadFile, token: HTTPAuthorizationCredentials self, series_id: int, poster: UploadFile, token: HTTPAuthorizationCredentials
) -> EventSeries: ) -> EventSeries:
"""
Adds a poster to an event series and returns the updated event series object.
:param int series_id: The ID of the event series to update
:param UploadFile poster: The image file to upload
:param HTTPAuthorizationCredentials token: The OAuth token
:return EventSeries: The updated event series object which is suitable for a response body
"""
_, sub = self.oauth_token.email_and_sub(token) _, sub = self.oauth_token.email_and_sub(token)
self.user_controller.get_user_by_sub(sub) self.user_controller.get_user_by_sub(sub)
return self.event_controller.add_series_poster(series_id, poster) return self.event_controller.add_series_poster(series_id, poster)
@@ -96,6 +137,12 @@ class MainController:
async def delete_series( async def delete_series(
self, series_id: int, token: HTTPAuthorizationCredentials self, series_id: int, token: HTTPAuthorizationCredentials
) -> None: ) -> None:
"""
Deletes an event series by numeric ID.
:param int series_id: The ID of the event series to delete
:param HTTPAuthorizationCredentials token: The OAuth token
"""
_, sub = self.oauth_token.email_and_sub(token) _, sub = self.oauth_token.email_and_sub(token)
self.user_controller.get_user_by_sub(sub) self.user_controller.get_user_by_sub(sub)
self.event_controller.delete_series(series_id) self.event_controller.delete_series(series_id)
@@ -103,35 +150,63 @@ class MainController:
async def update_series( async def update_series(
self, route_id: int, series: EventSeries, token: HTTPAuthorizationCredentials self, route_id: int, series: EventSeries, token: HTTPAuthorizationCredentials
) -> EventSeries: ) -> EventSeries:
"""
Updates an event series and returns the updated event series object.
:param int route_id: The ID of the event series in the URL
:param EventSeries series: The updated event series object
:param HTTPAuthorizationCredentials token: The OAuth token
:return EventSeries: The updated event series object which is suitable for a response body
"""
_, sub = self.oauth_token.email_and_sub(token) _, sub = self.oauth_token.email_and_sub(token)
self.user_controller.get_user_by_sub(sub) self.user_controller.get_user_by_sub(sub)
return self.event_controller.update_series(route_id, series) return self.event_controller.update_series(route_id, series)
async def get_users(self) -> list[User]: async def get_users(self) -> list[User]:
"""
Retrieves all users and returns them as a list.
:return list[User]: a list of User objects for a response body
"""
return self.user_controller.get_users() return self.user_controller.get_users()
async def get_user(self, user_id: int) -> User: async def get_user(self, user_id: int) -> User:
"""
Retrieves a single user by numeric ID.
:param int user_id: The ID of the user to retrieve
:return User: The user object for a response body
"""
return self.user_controller.get_user_by_id(user_id) return self.user_controller.get_user_by_id(user_id)
async def create_user(self, token: HTTPAuthorizationCredentials) -> User: async def create_user(self, token: HTTPAuthorizationCredentials) -> User:
"""This method does NOT post a user to the database. """
Instead, it retrieves the user's information from the OAuth token, Creates a new user and returns the created user object.
updates the user's sub in the db if needed, and returns the user object. Does NOT post a user to the database (this is not supported).
Args: :param HTTPAuthorizationCredentials token: The OAuth token
token (HTTPAuthorizationCredentials): The OAuth token :return User: The newly created user object which is suitable for a response body
Returns:
User: The User object
""" """
return self.user_controller.create_user(token) return self.user_controller.create_user(token)
async def get_group(self) -> Group: async def get_group(self) -> Group:
"""
Retrieves the group object and returns it.
:return Group: The group object for a response body
"""
return self.group_controller.get_group() return self.group_controller.get_group()
async def update_group_bio( async def update_group_bio(
self, bio: str, token: HTTPAuthorizationCredentials self, bio: str, token: HTTPAuthorizationCredentials
) -> Group: ) -> Group:
"""
Updates the group's bio and returns the updated group object.
:param str bio: The new bio for the group
:param HTTPAuthorizationCredentials token: The OAuth token
:return Group: The updated group object which is suitable for a response body
"""
_, sub = self.oauth_token.email_and_sub(token) _, sub = self.oauth_token.email_and_sub(token)
self.user_controller.get_user_by_sub(sub) self.user_controller.get_user_by_sub(sub)
return self.group_controller.update_group_bio(bio) return self.group_controller.update_group_bio(bio)

View File

@@ -11,26 +11,26 @@ from app.models.event import Event, EventSeries, NewEventSeries
class EventController(BaseController): class EventController(BaseController):
""" """
Handles all event-related operations and serves as an intermediate controller between Handles all event-related operations.
the main controller and the model layer.
Inherits from BaseController, which provides logging and other generic methods. Inherits from BaseController, which provides logging and other generic methods.
Testing: pass a mocked EventQueries object to the constructor.
""" """
def __init__(self, event_queries=event_queries) -> None: def __init__(self, event_queries: EventQueries = event_queries) -> None:
"""
Initializes the EventController with an EventQueries object.
:param EventQueries event_queries: object for querying event data, defaults to event_queries
"""
super().__init__() super().__init__()
self.db: EventQueries = event_queries self.db: EventQueries = event_queries
def _all_series(self, data_rows: list[dict]) -> dict[str, EventSeries]: def _all_series(self, data_rows: list[dict]) -> dict[str, EventSeries]:
"""Creates and returns a dictionary of EventSeries objects from a list of sql rows (as dicts). """
Should only be used internally. Builds a dictionary of EventSeries objects from a list of dictionaries.
Must only be used internally.
Args: :param list[dict] data_rows: The list of sql rows as dictionaries
data_rows (list[dict]): List of dicts, each representing a row from the database. `event_id` may be null :return dict[str, EventSeries]: A dictionary of EventSeries objects keyed by series name
Returns:
dict[str, EventSeries]: A dictionary of EventSeries objects, keyed by series name
""" """
all_series: dict[str, EventSeries] = {} all_series: dict[str, EventSeries] = {}
@@ -44,13 +44,11 @@ class EventController(BaseController):
return all_series return all_series
def get_all_series(self) -> list[EventSeries]: def get_all_series(self) -> list[EventSeries]:
"""Retrieves all EventSeries objects from the database and returns them as a list. """
Retrieves all EventSeries objects and returns them as a list.
Raises: :raises HTTPException: If any error occurs (status code 500)
HTTPException: If any error occurs during the retrieval process (status code 500) :return list[EventSeries]: A list of EventSeries objects suitable for a response body
Returns:
list[EventSeries]: A list of EventSeries objects which are suitable for a response body
""" """
series_data = self.db.select_all_series() series_data = self.db.select_all_series()
@@ -64,17 +62,13 @@ class EventController(BaseController):
) )
def get_one_series_by_id(self, series_id: int) -> EventSeries: def get_one_series_by_id(self, series_id: int) -> EventSeries:
"""Builds and returns a single EventSeries object by its numeric ID. """
Retrieves a single EventSeries object by numeric ID.
Args: :param int series_id: The ID of the series to retrieve
series_id (int): The numeric id of the series to retrieve :raises HTTPException: If the series is not found (status code 404)
:raises HTTPException: If any error occurs during the instantiation process (status code 500)
Raises: :return EventSeries: The EventSeries object which is suitable for a response body
HTTPException: If the series is not found (status code 404)
HTTPException: If an error occurs (status code 500)
Returns:
EventSeries: A single EventSeries object
""" """
if not (rows := self.db.select_one_by_id(series_id)): if not (rows := self.db.select_one_by_id(series_id)):
raise HTTPException( raise HTTPException(
@@ -91,16 +85,12 @@ class EventController(BaseController):
) )
def create_series(self, series: NewEventSeries) -> EventSeries: def create_series(self, series: NewEventSeries) -> EventSeries:
"""Takes a NewEventSeries object and passes it to the database layer for insertion. """
Passes a new EventSeries object to the database for creation and returns the created object.
Args: :param NewEventSeries series: The new EventSeries object to create
series (NewEventSeries): A NewEventSeries object which does not yet have an ID :raises HTTPException: If the series name already exists (status code 400)
:return EventSeries: The created EventSeries object which is suitable for a response body
Raises:
HTTPException: If the series name already exists (status code 400)
Returns:
EventSeries: The newly created EventSeries object with an ID
""" """
try: try:
inserted_id = self.db.insert_one_series(series) inserted_id = self.db.insert_one_series(series)
@@ -114,14 +104,12 @@ class EventController(BaseController):
) )
def add_series_poster(self, series_id: int, poster: UploadFile) -> EventSeries: def add_series_poster(self, series_id: int, poster: UploadFile) -> EventSeries:
"""Adds (or updates) a poster image to a series. """
Updates the poster image for an EventSeries object and returns the updated object.
Args: :param int series_id: The numeric ID of the series
series_id (int): The numeric ID of the series to update :param UploadFile poster: The new poster image file
poster (UploadFile): The image file to upload :return EventSeries: The updated EventSeries object with the new poster image
Returns:
EventSeries: The updated EventSeries object
""" """
series = self.get_one_series_by_id(series_id) series = self.get_one_series_by_id(series_id)
series.poster_id = self._upload_poster(poster) series.poster_id = self._upload_poster(poster)
@@ -129,17 +117,12 @@ class EventController(BaseController):
return self.get_one_series_by_id(series.series_id) return self.get_one_series_by_id(series.series_id)
def _upload_poster(self, poster: UploadFile) -> str: def _upload_poster(self, poster: UploadFile) -> str:
"""Uploads a poster image to Cloudinary and returns the public ID for storage in the database. """
Should only be used internally. Uploads an image file to the cloud and returns the public ID.
Args: :param UploadFile poster: The image file to upload
poster (UploadFile): The image file to upload :raises HTTPException: If any error occurs during the upload process (status code 500)
:return str: The public ID of the uploaded image
Raises:
HTTPException: If an error occurs during the upload process (status code 500)
Returns:
str: The public ID of the uploaded image
""" """
image_file = self.verify_image(poster) image_file = self.verify_image(poster)
try: try:
@@ -152,27 +135,23 @@ class EventController(BaseController):
) )
def delete_series(self, id: int) -> None: def delete_series(self, id: int) -> None:
"""Ensures an EventSeries object exists and then deletes it from the database. """
Deletes an EventSeries object from the database.
Args: :param int id: The numeric ID of the series to delete
id (int): The numeric ID of the series to delete
""" """
series = self.get_one_series_by_id(id) series = self.get_one_series_by_id(id)
self.db.delete_one_series(series) self.db.delete_one_series(series)
def update_series(self, route_id: int, series: EventSeries) -> EventSeries: def update_series(self, route_id: int, series: EventSeries) -> EventSeries:
"""Updates an existing EventSeries object in the database. """
Updates an EventSeries object in the database and returns the updated object.
Args: :param int route_id: The numeric ID in the URL
route_id (int): The numeric ID of the series in the URL :param EventSeries series: The updated EventSeries object
series (EventSeries): The updated EventSeries object :raises HTTPException: If the ID in the URL does not match the ID in the request body (status code 400)
:raises HTTPException: If the poster ID is updated directly (status code 400)
Raises: :return EventSeries: The updated EventSeries object which is suitable for a response body
HTTPException: if the ID in the URL does not match the ID in the request body (status code 400)
HTTPException: if the poster ID is updated directly (status code 400)
Returns:
EventSeries: The updated EventSeries object with updated info
""" """
if route_id != series.series_id: if route_id != series.series_id:
raise HTTPException( raise HTTPException(

View File

@@ -8,28 +8,26 @@ from app.models.group import Group
class GroupController(BaseController): class GroupController(BaseController):
""" """
Handles all group-related operations and serves as an intermediate controller between Handles all group-related operations.
the main controller and the model layer.
Inherits from BaseController, which provides logging and other generic methods. Inherits from BaseController, which provides logging and other generic methods.
The corresponding table contains only one row.
Testing: pass a mocked GroupQueries object to the constructor.
""" """
def __init__(self, group_queries=group_queries) -> None: def __init__(self, group_queries=group_queries) -> None:
"""
Initializes the GroupController with a GroupQueries object.
:param GroupQueries group_queries: object for quering group data, defaults to group_queries
"""
super().__init__() super().__init__()
self.group_queries: GroupQueries = group_queries self.group_queries: GroupQueries = group_queries
def get_group(self) -> Group: def get_group(self) -> Group:
"""Retrieves the group from the database and returns it as a Group object. """
Instantiates a Group object and retuns it for a response body.
Raises: :raises HTTPException: If the group is not found (status code 404)
HTTPException: If the group is not found (status code 404) :raises HTTPException: If any error occurs during the instantiation process (status code 500)
HTTPException: If any error occurs during the retrieval process (status code 500) :return Group: The Group object which is suitable for a response body
Returns:
Group: A Group object which is suitable for a response body
""" """
if (data := self.group_queries.select_one_by_id()) is None: if (data := self.group_queries.select_one_by_id()) is None:
raise HTTPException( raise HTTPException(
@@ -44,16 +42,12 @@ class GroupController(BaseController):
) )
def update_group_bio(self, bio: str) -> Group: def update_group_bio(self, bio: str) -> Group:
"""Updates the group's bio in the database and returns the updated Group object. """
Updates the group's bio in the database and returns the updated Group object.
Args: :param str bio: The new bio for the group
bio (str): The new bio for the group :raises HTTPException: If any error occurs during the update process (status code 500)
:return Group: The updated Group object which is suitable for a response body
Raises:
HTTPException: If any error occurs during the update process (status code 500)
Returns:
Group: The updated Group object which is suitable for a response body
""" """
try: try:
self.group_queries.update_group_bio(bio) self.group_queries.update_group_bio(bio)

View File

@@ -1,3 +1,5 @@
from typing import Optional
from fastapi import HTTPException, UploadFile, status from fastapi import HTTPException, UploadFile, status
from icecream import ic from icecream import ic
@@ -10,25 +12,25 @@ from app.models.musician import Musician
class MusicianController(BaseController): class MusicianController(BaseController):
""" """
Handles all musician-related operations and serves as an intermediate controller between Handles all musician-related operations.
the main controller and the model layer.
Inherits from BaseController, which provides logging and other generic methods. Inherits from BaseController, which provides logging and other generic methods.
Testing: pass a mocked MusicianQueries object to the constructor.
""" """
def __init__(self, musician_queries=musician_queries) -> None: def __init__(self, musician_queries: MusicianQueries = musician_queries) -> None:
"""
Initializes the MusicianController with a MusicianQueries object.
:param MusicianQueries musician_queries: object for querying musician data, defaults to musician_queries
"""
super().__init__() super().__init__()
self.db: MusicianQueries = musician_queries self.db: MusicianQueries = musician_queries
def get_musicians(self) -> list[Musician]: def get_musicians(self) -> list[Musician]:
"""Retrieves all musicians from the database and returns them as a list of Musician objects. """
Retrieves all musicians and returns them as a list.
Raises: :raises HTTPException: If any error occurs during the retrieval process (status code 500)
HTTPException: If any error occurs during the retrieval process (status code 500) :return list[Musician]: A list of Musician objects suitable for a response body
Returns:
list[Musician]: A list of Musician objects which are suitable for a response body
""" """
data = self.db.select_all_series() data = self.db.select_all_series()
try: try:
@@ -40,17 +42,13 @@ class MusicianController(BaseController):
) )
def get_musician(self, musician_id: int) -> Musician: def get_musician(self, musician_id: int) -> Musician:
"""Retrieves a single musician from the database and returns it as a Musician object. """
Retrieves a single musician by numeric ID.
Args: :param int musician_id: The ID of the musician to retrieve
id (int): The ID of the musician to retrieve :raises HTTPException: If the musician is not found (status code 404)
:raises HTTPException: If any error occurs during the instantiation process (status code 500)
Raises: :return Musician: The musician object for a response body
HTTPException: If the musician is not found (status code 404)
HTTPException: If any error occurs during the retrieval process (status code 500)
Returns:
Musician: A Musician object which is suitable for a response body
""" """
if (data := self.db.select_one_by_id(musician_id)) is None: if (data := self.db.select_one_by_id(musician_id)) is None:
raise HTTPException( raise HTTPException(
@@ -68,20 +66,16 @@ class MusicianController(BaseController):
self, self,
musician_id: int, musician_id: int,
new_bio: str, new_bio: str,
file: UploadFile | None = None, file: Optional[UploadFile] = None,
) -> Musician: ) -> Musician:
"""Updates a musician's bio and/or headshot by conditionally calling the appropriate methods. """
Updates a musician in the database and returns the updated musician object.
Args: :param int musician_id: The ID of the musician to update
musician_id (int): The numeric ID of the musician to update :param str new_bio: The new biography for the musician
new_bio (str): The new biography for the musician :param Optional[UploadFile] file: The new headshot file, defaults to None
file (UploadFile | None, optional): The new headshot file. Defaults to None. :raises HTTPException: _description_
:return Musician: _description_
Raises:
HTTPException: If the musician is not found (status code 404)
Returns:
Musician: The updated Musician object
""" """
musician = self.get_musician(musician_id) musician = self.get_musician(musician_id)
if new_bio != musician.bio: if new_bio != musician.bio:
@@ -97,17 +91,13 @@ class MusicianController(BaseController):
def _update_musician_headshot( def _update_musician_headshot(
self, musician: Musician, headshot_id: str self, musician: Musician, headshot_id: str
) -> Musician: ) -> Musician:
"""Updates a musician's headshot in the database. """
Updates a musician's headshot in the database.
Args: :param Musician musician: The musician object to update
musician (Musician): The musician object to update :param str headshot_id: The new public ID for the headshot
headshot_id (str): The public ID of the new headshot (as determined by Cloudinary) :raises HTTPException: If any error occurs during the update process (status code 500)
:return Musician: The updated Musician object
Raises:
HTTPException: If any error occurs during the update process (status code 500)
Returns:
Musician: The updated Musician object
""" """
try: try:
self.db.update_headshot(musician, headshot_id) self.db.update_headshot(musician, headshot_id)
@@ -119,17 +109,13 @@ class MusicianController(BaseController):
return self.get_musician(musician.id) return self.get_musician(musician.id)
def _update_musician_bio(self, musician: Musician, bio: str) -> Musician: def _update_musician_bio(self, musician: Musician, bio: str) -> Musician:
"""Updates a musician's bio in the database. """
Updates a musician's biography in the database.
Args: :param Musician musician: The musician object to update
musician (Musician): The musician object to update :param str bio: The new biography for the musician
bio (str): The new biography for the musician :raises HTTPException: If any error occurs during the update process (status code 500)
:return Musician: The updated Musician object
Raises:
HTTPException: If any error occurs during the update process (status code 500)
Returns:
Musician: The updated Musician object
""" """
try: try:
self.db.update_bio(musician, bio) self.db.update_bio(musician, bio)
@@ -141,17 +127,13 @@ class MusicianController(BaseController):
return self.get_musician(musician.id) return self.get_musician(musician.id)
def _upload_headshot(self, musician: Musician, file: UploadFile) -> Musician: def _upload_headshot(self, musician: Musician, file: UploadFile) -> Musician:
"""Uploads a new headshot for a musician and updates the database with the new public ID. """
Uploads a new headshot image for a musician and returns the updated musician object.
Args: :param Musician musician: The musician object to update
musician (Musician): The musician object to update :param UploadFile file: The new headshot file
file (UploadFile): The new headshot file :raises HTTPException: If any error occurs during the upload process (status code 500)
:return Musician: The updated Musician object
Raises:
HTTPException: If the file is not an image or exceeds the maximum file size (status code 400)
Returns:
Musician: The updated Musician object
""" """
image_file = self.verify_image(file) image_file = self.verify_image(file)
data = uploader.upload(image_file) data = uploader.upload(image_file)

View File

@@ -10,26 +10,25 @@ from app.models.user import User
class UserController(BaseController): class UserController(BaseController):
""" """
Handles all user-related operations and serves as an intermediate controller between Handles all user-related operations.
the main controller and the model layer.
Inherits from BaseController, which provides logging and other generic methods. Inherits from BaseController, which provides logging and other generic methods.
Testing: pass a mocked UserQueries object to the constructor.
""" """
def __init__(self, user_queries=user_queries) -> None: def __init__(self, user_queries: UserQueries = user_queries) -> None:
"""
Initializes the UserController with a UserQueries object.
:param UserQueries user_queries: object for querying user data, defaults to user_queries
"""
super().__init__() super().__init__()
self.db: UserQueries = user_queries self.db: UserQueries = user_queries
def get_users(self) -> list[User]: def get_users(self) -> list[User]:
"""Retrieves all users from the database and returns them as a list of User objects. """
Retrieves all users and returns them as a list.
Raises: :raises HTTPException: If any error occurs during the retrieval process (status code 500)
HTTPException: If any error occurs during the retrieval process (status code 500) :return list[User]: A list of User objects suitable for a response body
Returns:
list[User]: A list of User objects which are suitable for a response body
""" """
data = self.db.select_all_series() data = self.db.select_all_series()
try: try:
@@ -41,17 +40,13 @@ class UserController(BaseController):
) )
def get_user_by_id(self, user_id: int) -> User: def get_user_by_id(self, user_id: int) -> User:
"""Retrieves a single user from the database and returns it as a User object. """
Retrieves a single user from the database and returns it as a User object.
Args: :param int user_id: The ID of the user to retrieve
user_id (int): The ID of the user to retrieve :raises HTTPException: If the user is not found (status code 404)
:raises HTTPException: If any error occurs during the retrieval process (status code 500)
Raises: :return User: A User object which is suitable for a response body
HTTPException: If the user is not found (status code 404)
HTTPException: If any error occurs during the retrieval process (status code 500)
Returns:
User: A User object which is suitable for a response body
""" """
if (data := self.db.select_one_by_id(user_id)) is None: if (data := self.db.select_one_by_id(user_id)) is None:
raise HTTPException( raise HTTPException(
@@ -66,17 +61,13 @@ class UserController(BaseController):
) )
def get_user_by_email(self, email: str) -> User: def get_user_by_email(self, email: str) -> User:
"""Retrieves a single user from the database and returns it as a User object. """
Retrieves a single user from the database and returns it as a User object.
Args: :param str email: The email of the user to retrieve
email (str): The email of the user to retrieve :raises HTTPException: If the user is not found (status code 404)
:raises HTTPException: If any error occurs during the retrieval process (status code 500)
Raises: :return User: A User object which is suitable for a response body
HTTPException: If the user is not found (status code 404)
HTTPException: If any error occurs during the retrieval process (status code 500)
Returns:
User: A User object which is suitable for a response body
""" """
if (data := self.db.select_one_by_email(email)) is None: if (data := self.db.select_one_by_email(email)) is None:
raise HTTPException( raise HTTPException(
@@ -91,17 +82,13 @@ class UserController(BaseController):
) )
def get_user_by_sub(self, sub: str) -> User: def get_user_by_sub(self, sub: str) -> User:
"""Retrieves a single user from the database and returns it as a User object. """
Retrieves a single user from the database and returns it as a User object.
Args: :param str sub: The sub of the user to retrieve
sub (str): The sub of the user to retrieve :raises HTTPException: If the user is not found (status code 404)
:raises HTTPException: If any error occurs during the retrieval process (status code 500)
Raises: :return User: A User object which is suitable for a response body
HTTPException: If the user is not found (status code 404)
HTTPException: If any error occurs during the retrieval process (status code 500)
Returns:
User: A User object which is suitable for a response body
""" """
if (data := self.db.select_one_by_sub(sub)) is None: if (data := self.db.select_one_by_sub(sub)) is None:
raise HTTPException( raise HTTPException(
@@ -116,13 +103,11 @@ class UserController(BaseController):
) )
def create_user(self, token: HTTPAuthorizationCredentials) -> User: def create_user(self, token: HTTPAuthorizationCredentials) -> User:
"""Updates a user's sub in the database and creates a new User object. """
Creates a new user in the database and returns the created User object.
Args: :param HTTPAuthorizationCredentials token: The OAuth token
token (HTTPAuthorizationCredentials): The token containing the user's email and sub :return User: The created User object which is suitable for a response body
Returns:
User: A User object which is suitable for a response body
""" """
email, sub = oauth_token.email_and_sub(token) email, sub = oauth_token.email_and_sub(token)
user: User = self.get_user_by_email(email) user: User = self.get_user_by_email(email)

View File

@@ -6,10 +6,18 @@ from pydantic import BaseModel, HttpUrl
class Poster(BaseModel): class Poster(BaseModel):
"""
Represents a poster image file for an EventSeries object.
"""
file: UploadFile file: UploadFile
class NewEvent(BaseModel): class NewEvent(BaseModel):
"""
Represents a new event object as received from the client.
"""
location: str location: str
time: datetime time: datetime
map_url: Optional[HttpUrl] = None map_url: Optional[HttpUrl] = None
@@ -17,16 +25,28 @@ class NewEvent(BaseModel):
class Event(NewEvent): class Event(NewEvent):
"""
Represents an existing event object to be returned to the client.
"""
event_id: int event_id: int
class NewEventSeries(BaseModel): class NewEventSeries(BaseModel):
"""
Represents a new event series object as received from the client.
"""
name: str name: str
description: str description: str
events: list[NewEvent] events: list[NewEvent]
class EventSeries(NewEventSeries): class EventSeries(NewEventSeries):
"""
Represents an existing event series object to be returned to the client.
"""
series_id: int series_id: int
events: list[Event] events: list[Event]
poster_id: Optional[str] = None poster_id: Optional[str] = None

View File

@@ -12,7 +12,6 @@ router = APIRouter(
@router.post("/", status_code=status.HTTP_201_CREATED) @router.post("/", status_code=status.HTTP_201_CREATED)
async def post_message(contact: Contact): async def post_message(contact: Contact):
"""Sends an email to the site owner with the provided name, email, and message."""
subject = f"New message from {contact.name}" subject = f"New message from {contact.name}"
body = f"From: {contact.email}\n\n{contact.message}" body = f"From: {contact.email}\n\n{contact.message}"
send_email(subject, body) send_email(subject, body)

View File

@@ -1,14 +1,10 @@
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pytest import pytest
from fastapi import HTTPException, status
from icecream import ic
from app.controllers.controller import MainController from app.controllers.controller import MainController
from app.models.event import Event, EventSeries, NewEventSeries from app.models.event import EventSeries, NewEventSeries
from app.models.group import Group
from app.models.musician import Musician from app.models.musician import Musician
from app.models.user import User
mock_user_controller = MagicMock() mock_user_controller = MagicMock()
mock_musician_controller = MagicMock() mock_musician_controller = MagicMock()