initial commit
This commit is contained in:
1
server/app/__init__.py
Normal file
1
server/app/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from app.main import app
|
||||
3
server/app/admin/__init__.py
Normal file
3
server/app/admin/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from fastapi.security import HTTPBearer, OAuth2PasswordBearer
|
||||
|
||||
oauth2_http = HTTPBearer()
|
||||
17
server/app/admin/contact.py
Normal file
17
server/app/admin/contact.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from os import getenv
|
||||
|
||||
HOST = "grapefruitswebsite@gmail.com"
|
||||
|
||||
|
||||
def send_email(subject: str, body: str) -> None:
|
||||
password = getenv("APP_PASSWORD")
|
||||
email = getenv("EMAIL")
|
||||
msg = MIMEText(body)
|
||||
msg["Subject"] = subject
|
||||
msg["From"] = HOST
|
||||
smtp_server = smtplib.SMTP_SSL("smtp.gmail.com", 465)
|
||||
smtp_server.login(HOST, password) # type: ignore
|
||||
smtp_server.sendmail(HOST, [email], msg.as_string()) # type: ignore
|
||||
smtp_server.quit()
|
||||
48
server/app/admin/images.py
Normal file
48
server/app/admin/images.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# Set your Cloudinary credentials
|
||||
# ==============================
|
||||
|
||||
from pprint import pprint
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
# Import the Cloudinary libraries
|
||||
# ==============================
|
||||
import cloudinary
|
||||
import cloudinary.api
|
||||
import cloudinary.uploader
|
||||
|
||||
# Set configuration parameter: return "https" URLs by setting secure=True
|
||||
# ==============================
|
||||
cloudinary.config(secure=True)
|
||||
|
||||
uploader = cloudinary.uploader
|
||||
|
||||
|
||||
class CloudinaryException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def delete_image(public_id: str) -> None:
|
||||
result = uploader.destroy(public_id)
|
||||
|
||||
if result.get("result") != "ok":
|
||||
raise CloudinaryException("Failed to delete image")
|
||||
|
||||
|
||||
def get_image_data(public_id: str) -> dict:
|
||||
data = cloudinary.api.resource(public_id)
|
||||
return data
|
||||
|
||||
|
||||
def get_image_url(public_id: str) -> str:
|
||||
url = cloudinary.utils.cloudinary_url(public_id)[0]
|
||||
if url is None:
|
||||
raise CloudinaryException("Failed to get image URL")
|
||||
return url
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
image_id = "coco_copy_jywbxm"
|
||||
27
server/app/admin/oauth_token.py
Normal file
27
server/app/admin/oauth_token.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from os import getenv
|
||||
|
||||
from fastapi.security.http import HTTPAuthorizationCredentials
|
||||
from google.auth import jwt
|
||||
from icecream import ic
|
||||
|
||||
|
||||
def _token_claims(token: HTTPAuthorizationCredentials) -> dict:
|
||||
aud = getenv("AUDIENCE")
|
||||
credentials = token.credentials
|
||||
claims = jwt.decode(credentials, aud, verify=False)
|
||||
if not claims:
|
||||
raise ValueError("Invalid token")
|
||||
if claims.get("aud") != aud:
|
||||
raise ValueError("Invalid audience")
|
||||
if claims.get("email_verified") is not True:
|
||||
raise ValueError("Email not verified")
|
||||
if not claims.get("email"):
|
||||
raise ValueError("Email not found in token")
|
||||
if not claims.get("sub"):
|
||||
raise ValueError("Sub not found in token")
|
||||
return claims
|
||||
|
||||
|
||||
def email_and_sub(token: HTTPAuthorizationCredentials) -> tuple[str, str]:
|
||||
claims = _token_claims(token)
|
||||
return claims["email"], claims["sub"]
|
||||
3
server/app/controllers/__init__.py
Normal file
3
server/app/controllers/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from app.controllers.controller import Controller
|
||||
|
||||
controller = Controller()
|
||||
28
server/app/controllers/base_controller.py
Normal file
28
server/app/controllers/base_controller.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from fastapi import HTTPException, UploadFile, status
|
||||
|
||||
from app.db.base_queries import BaseQueries
|
||||
|
||||
ALLOWED_FILES_TYPES = ["image/jpeg", "image/png"]
|
||||
MAX_FILE_SIZE = 1000000 # 1 MB
|
||||
|
||||
|
||||
class BaseController:
|
||||
def __init__(self) -> None:
|
||||
self.db: BaseQueries = None # type: ignore
|
||||
self.ALL_FILES = ALLOWED_FILES_TYPES
|
||||
self.MAX_FILE_SIZE = MAX_FILE_SIZE
|
||||
|
||||
async def verify_image(self, file: UploadFile) -> bytes:
|
||||
print("verifying image")
|
||||
if file.content_type not in self.ALL_FILES:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"File type {file.content_type} not allowed. Allowed file types are {self.ALL_FILES}",
|
||||
)
|
||||
image_file = await file.read()
|
||||
if len(image_file) > self.MAX_FILE_SIZE:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"File size {len(image_file)} bytes exceeds maximum of {self.MAX_FILE_SIZE} bytes",
|
||||
)
|
||||
return image_file
|
||||
99
server/app/controllers/controller.py
Normal file
99
server/app/controllers/controller.py
Normal file
@@ -0,0 +1,99 @@
|
||||
from fastapi import HTTPException, UploadFile, status
|
||||
from fastapi.security import HTTPAuthorizationCredentials
|
||||
from icecream import ic
|
||||
|
||||
from app.admin import oauth_token
|
||||
from app.controllers.events import EventController
|
||||
from app.controllers.group import GroupController
|
||||
from app.controllers.musicians import MusicianController
|
||||
from app.controllers.users import UserController
|
||||
from app.models.event import EventSeries, NewEventSeries
|
||||
from app.models.group import Group
|
||||
from app.models.musician import Musician
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
class Controller:
|
||||
def __init__(self) -> None:
|
||||
self.event_controller = EventController()
|
||||
self.musician_controller = MusicianController()
|
||||
self.user_controller = UserController()
|
||||
self.group_controller = GroupController()
|
||||
|
||||
async def get_musicians(self) -> list[Musician]:
|
||||
return await self.musician_controller.get_musicians()
|
||||
|
||||
async def get_musician(self, id: int) -> Musician:
|
||||
return await self.musician_controller.get_musician(id)
|
||||
|
||||
async def update_musician(
|
||||
self,
|
||||
musician: Musician,
|
||||
url_param_id: int,
|
||||
token: HTTPAuthorizationCredentials,
|
||||
file: UploadFile | None = None,
|
||||
) -> Musician:
|
||||
|
||||
if musician.id != url_param_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="ID in URL does not match ID in request body",
|
||||
)
|
||||
_, sub = oauth_token.email_and_sub(token)
|
||||
await self.user_controller.get_user_by_sub(sub)
|
||||
return await self.musician_controller.update_musician(
|
||||
musician_id=musician.id,
|
||||
new_bio=musician.bio,
|
||||
file=file,
|
||||
)
|
||||
|
||||
async def get_events(self) -> list[EventSeries]:
|
||||
return await self.event_controller.get_all_series()
|
||||
|
||||
async def get_event(self, id: int) -> EventSeries:
|
||||
return await self.event_controller.get_one_series(id)
|
||||
|
||||
async def create_event(
|
||||
self, series: NewEventSeries, token: HTTPAuthorizationCredentials
|
||||
) -> EventSeries:
|
||||
_, sub = oauth_token.email_and_sub(token)
|
||||
await self.user_controller.get_user_by_sub(sub)
|
||||
return await self.event_controller.create_series(series)
|
||||
|
||||
async def add_series_poster(
|
||||
self, series_id: int, poster: UploadFile, token: HTTPAuthorizationCredentials
|
||||
) -> EventSeries:
|
||||
_, sub = oauth_token.email_and_sub(token)
|
||||
await self.user_controller.get_user_by_sub(sub)
|
||||
return await self.event_controller.add_series_poster(series_id, poster)
|
||||
|
||||
async def delete_series(self, id: int, token: HTTPAuthorizationCredentials) -> None:
|
||||
_, sub = oauth_token.email_and_sub(token)
|
||||
await self.user_controller.get_user_by_sub(sub)
|
||||
await self.event_controller.delete_series(id)
|
||||
|
||||
async def update_series(
|
||||
self, route_id: int, series: EventSeries, token: HTTPAuthorizationCredentials
|
||||
) -> EventSeries:
|
||||
_, sub = oauth_token.email_and_sub(token)
|
||||
await self.user_controller.get_user_by_sub(sub)
|
||||
return await self.event_controller.update_series(route_id, series)
|
||||
|
||||
async def get_users(self) -> list[User]:
|
||||
return await self.user_controller.get_users()
|
||||
|
||||
async def get_user(self, id: int) -> User:
|
||||
return await self.user_controller.get_user_by_id(id)
|
||||
|
||||
async def create_user(self, token: HTTPAuthorizationCredentials) -> User:
|
||||
return await self.user_controller.create_user(token)
|
||||
|
||||
async def get_group(self) -> Group:
|
||||
return await self.group_controller.get_group()
|
||||
|
||||
async def update_group_bio(
|
||||
self, bio: str, token: HTTPAuthorizationCredentials
|
||||
) -> Group:
|
||||
_, sub = oauth_token.email_and_sub(token)
|
||||
await self.user_controller.get_user_by_sub(sub)
|
||||
return await self.group_controller.update_group_bio(bio)
|
||||
106
server/app/controllers/events.py
Normal file
106
server/app/controllers/events.py
Normal file
@@ -0,0 +1,106 @@
|
||||
from fastapi import HTTPException, UploadFile, status
|
||||
from icecream import ic
|
||||
from mysql.connector.errors import IntegrityError
|
||||
|
||||
from app.admin.images import uploader
|
||||
from app.controllers.base_controller import BaseController
|
||||
from app.db import event_queries
|
||||
from app.db.events import EventQueries
|
||||
from app.models.event import Event, EventSeries, NewEventSeries
|
||||
|
||||
|
||||
class EventController(BaseController):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.db: EventQueries = event_queries
|
||||
|
||||
def _all_series(self, data: list[dict]) -> list[EventSeries]:
|
||||
all_series: dict[str, EventSeries] = {}
|
||||
|
||||
for event_series_row in data:
|
||||
series_name: str = event_series_row["name"]
|
||||
event = Event(**event_series_row)
|
||||
if series_name not in all_series:
|
||||
all_series[series_name] = EventSeries(**event_series_row, events=[])
|
||||
all_series[series_name].events.append(event)
|
||||
|
||||
return [series for series in all_series.values()]
|
||||
|
||||
async def get_all_series(self) -> list[EventSeries]:
|
||||
data = await self.db.get_all()
|
||||
try:
|
||||
return self._all_series(data)
|
||||
except Exception as e:
|
||||
ic(e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error retrieving event objects: {e}",
|
||||
)
|
||||
|
||||
async def get_one_series(self, id: int) -> EventSeries:
|
||||
if not (data := await self.db.get_one(id)):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Event not found"
|
||||
)
|
||||
try:
|
||||
event = EventSeries(
|
||||
**data[0], events=[Event(**e) for e in data if e.get("event_id")]
|
||||
)
|
||||
return event
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error creating event object: {e}",
|
||||
)
|
||||
|
||||
async def create_series(self, series: NewEventSeries) -> EventSeries:
|
||||
try:
|
||||
inserted_id = await self.db.insert_one_series(series)
|
||||
for new_event in series.events:
|
||||
await self.db.insert_one_event(new_event, inserted_id)
|
||||
return await self.get_one_series(inserted_id)
|
||||
except IntegrityError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Series name already exists. Each series must have a unique name.\n{e}",
|
||||
)
|
||||
|
||||
async def add_series_poster(self, series_id, poster: UploadFile) -> EventSeries:
|
||||
series = await self.get_one_series(series_id)
|
||||
series.poster_id = await self._upload_poster(poster)
|
||||
await self.db.update_series_poster(series)
|
||||
return await self.get_one_series(series.series_id)
|
||||
|
||||
async def _upload_poster(self, poster: UploadFile) -> str:
|
||||
image_file = await self.verify_image(poster)
|
||||
try:
|
||||
data = uploader.upload(image_file)
|
||||
return data.get("public_id")
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error uploading image: {e}",
|
||||
)
|
||||
|
||||
async def delete_series(self, id: int) -> None:
|
||||
series = await self.get_one_series(id)
|
||||
await self.db.delete_one_series(series)
|
||||
|
||||
async def update_series(self, route_id: int, series: EventSeries) -> EventSeries:
|
||||
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",
|
||||
)
|
||||
prev_series = await self.get_one_series(series.series_id)
|
||||
if series.poster_id != prev_series.poster_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Poster ID cannot be updated directly. Use the /poster endpoint instead.",
|
||||
)
|
||||
await self.db.delete_events_by_series(series)
|
||||
await self.db.replace_series(series)
|
||||
for event in series.events:
|
||||
await self.db.insert_one_event(event, series.series_id)
|
||||
return await self.get_one_series(series.series_id)
|
||||
35
server/app/controllers/group.py
Normal file
35
server/app/controllers/group.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from app.controllers.base_controller import BaseController
|
||||
from app.db import group_queries
|
||||
from app.db.group import GroupQueries
|
||||
from app.models.group import Group
|
||||
|
||||
|
||||
class GroupController(BaseController):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.db: GroupQueries = group_queries
|
||||
|
||||
async def get_group(self) -> Group:
|
||||
if (data := await self.db.get_one()) is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Group not found"
|
||||
)
|
||||
try:
|
||||
return Group(**data)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error creating group object: {e}",
|
||||
)
|
||||
|
||||
async def update_group_bio(self, bio: str) -> Group:
|
||||
try:
|
||||
await self.db.update_group_bio(bio)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error updating group bio: {e}",
|
||||
)
|
||||
return await self.get_group()
|
||||
89
server/app/controllers/musicians.py
Normal file
89
server/app/controllers/musicians.py
Normal file
@@ -0,0 +1,89 @@
|
||||
from fastapi import HTTPException, UploadFile, status
|
||||
from icecream import ic
|
||||
|
||||
from app.admin.images import uploader
|
||||
from app.controllers.base_controller import BaseController
|
||||
from app.db import musician_queries
|
||||
from app.db.musicians import MusicianQueries
|
||||
from app.models.musician import Musician
|
||||
|
||||
|
||||
class MusicianController(BaseController):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.db: MusicianQueries = musician_queries
|
||||
|
||||
async def get_musicians(self) -> list[Musician]:
|
||||
data = await self.db.get_all()
|
||||
try:
|
||||
return [Musician(**m) for m in data]
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error creating musician objects: {e}",
|
||||
)
|
||||
|
||||
async def get_musician(self, id: int) -> Musician:
|
||||
if (data := await self.db.get_one(id)) is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Musician not found"
|
||||
)
|
||||
try:
|
||||
return Musician(**data)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error creating musician object: {e}",
|
||||
)
|
||||
|
||||
async def update_musician(
|
||||
self,
|
||||
musician_id: int,
|
||||
new_bio: str,
|
||||
file: UploadFile | None = None,
|
||||
) -> Musician:
|
||||
musician = await self.get_musician(musician_id)
|
||||
if new_bio != musician.bio:
|
||||
return await self.update_musician_bio(musician.id, new_bio)
|
||||
if file is not None:
|
||||
return await self.upload_headshot(musician.id, file)
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Update operation not implemented. Neither the bio or headshot was updated.",
|
||||
)
|
||||
|
||||
async def update_musician_headshot(self, id: int, headshot_id: str) -> Musician:
|
||||
await self.get_musician(id)
|
||||
try:
|
||||
await self.db.update_headshot(id, headshot_id)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error updating musician headshot: {e}",
|
||||
)
|
||||
return await self.get_musician(id)
|
||||
|
||||
async def update_musician_bio(self, id: int, bio: str) -> Musician:
|
||||
await self.get_musician(id) # Check if musician exists
|
||||
try:
|
||||
await self.db.update_bio(id, bio)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error updating musician bio: {e}",
|
||||
)
|
||||
return await self.get_musician(id)
|
||||
|
||||
async def upload_headshot(self, id: int, file: UploadFile) -> Musician:
|
||||
image_file = await self.verify_image(file)
|
||||
data = uploader.upload(image_file)
|
||||
public_id = data.get("public_id")
|
||||
if public_id is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to upload image",
|
||||
)
|
||||
await self.update_musician_headshot(id, public_id)
|
||||
|
||||
return await self.get_musician(id)
|
||||
70
server/app/controllers/users.py
Normal file
70
server/app/controllers/users.py
Normal file
@@ -0,0 +1,70 @@
|
||||
from fastapi import HTTPException, status
|
||||
from fastapi.security import HTTPAuthorizationCredentials
|
||||
|
||||
from app.admin import oauth_token
|
||||
from app.controllers.base_controller import BaseController
|
||||
from app.db import user_queries
|
||||
from app.db.users import UserQueries
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
class UserController(BaseController):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.db: UserQueries = user_queries
|
||||
|
||||
async def get_users(self) -> list[User]:
|
||||
data = await self.db.get_all()
|
||||
try:
|
||||
return [User(**e) for e in data]
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error creating user objects: {e}",
|
||||
)
|
||||
|
||||
async def get_user_by_id(self, id: int) -> User:
|
||||
if (data := await self.db.get_one(id)) is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
|
||||
)
|
||||
try:
|
||||
return User(**data)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error creating user object: {e}",
|
||||
)
|
||||
|
||||
async def get_user_by_email(self, email: str) -> User:
|
||||
if (data := await self.db.get_one_by_email(email)) is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="User does not exist"
|
||||
)
|
||||
try:
|
||||
return User(**data)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error creating user object: {e}",
|
||||
)
|
||||
|
||||
async def get_user_by_sub(self, sub: str) -> User:
|
||||
if (data := await self.db.get_one_by_sub(sub)) is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
|
||||
)
|
||||
try:
|
||||
return User(**data)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error creating user object: {e}",
|
||||
)
|
||||
|
||||
async def create_user(self, token: HTTPAuthorizationCredentials) -> User:
|
||||
email, sub = oauth_token.email_and_sub(token)
|
||||
user: User = await self.get_user_by_email(email)
|
||||
if user.sub is None:
|
||||
await self.db.update_sub(user.email, sub)
|
||||
return await self.get_user_by_sub(sub)
|
||||
9
server/app/db/__init__.py
Normal file
9
server/app/db/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from .events import EventQueries
|
||||
from .group import GroupQueries
|
||||
from .musicians import MusicianQueries
|
||||
from .users import UserQueries
|
||||
|
||||
event_queries = EventQueries()
|
||||
user_queries = UserQueries()
|
||||
musician_queries = MusicianQueries()
|
||||
group_queries = GroupQueries()
|
||||
32
server/app/db/base_queries.py
Normal file
32
server/app/db/base_queries.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from typing import Callable
|
||||
|
||||
from app.db.conn import connect_db
|
||||
|
||||
|
||||
class BaseQueries:
|
||||
from icecream import ic
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.table: str = None # type: ignore
|
||||
self.connect_db: Callable = connect_db
|
||||
|
||||
async def get_all(self) -> list[dict]:
|
||||
query = f"SELECT * FROM {self.table}"
|
||||
db = connect_db()
|
||||
cursor = db.cursor(dictionary=True)
|
||||
cursor.execute(query)
|
||||
data = cursor.fetchall()
|
||||
cursor.close()
|
||||
db.close()
|
||||
return data # type: ignore
|
||||
|
||||
async def get_one(self, id: int) -> dict | None:
|
||||
query = f"SELECT * FROM {self.table} WHERE id = %s"
|
||||
db = self.connect_db()
|
||||
cursor = db.cursor(dictionary=True)
|
||||
cursor.execute(query, (id,))
|
||||
data = cursor.fetchone()
|
||||
cursor.close()
|
||||
db.close()
|
||||
|
||||
return data # type: ignore
|
||||
31
server/app/db/conn.py
Normal file
31
server/app/db/conn.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import os
|
||||
|
||||
import mysql.connector
|
||||
from dotenv import load_dotenv
|
||||
|
||||
|
||||
class DBException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def connect_db() -> mysql.connector.MySQLConnection:
|
||||
load_dotenv()
|
||||
host = os.getenv("DB_HOST")
|
||||
user = os.getenv("DB_USER")
|
||||
password = os.getenv("DB_PASSWORD")
|
||||
database = os.getenv("DB_DATABASE")
|
||||
|
||||
if None in [host, user, password, database]:
|
||||
raise DBException("Missing database credentials")
|
||||
|
||||
try:
|
||||
return mysql.connector.connect(
|
||||
host=host,
|
||||
user=user,
|
||||
password=password,
|
||||
database=database,
|
||||
auth_plugin="mysql_native_password",
|
||||
) # type: ignore
|
||||
|
||||
except mysql.connector.Error as err:
|
||||
raise DBException("Could not connect to database") from err
|
||||
140
server/app/db/events.py
Normal file
140
server/app/db/events.py
Normal file
@@ -0,0 +1,140 @@
|
||||
from asyncio import gather
|
||||
|
||||
from icecream import ic
|
||||
|
||||
from app.db.base_queries import BaseQueries
|
||||
from app.models.event import (
|
||||
EVENT_TABLE,
|
||||
SERIES_TABLE,
|
||||
Event,
|
||||
EventSeries,
|
||||
NewEvent,
|
||||
NewEventSeries,
|
||||
)
|
||||
|
||||
|
||||
class EventQueries(BaseQueries):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.table = SERIES_TABLE
|
||||
|
||||
async def get_one(self, series_id: int) -> list[dict] | None:
|
||||
query = f"""
|
||||
SELECT *
|
||||
FROM {SERIES_TABLE}
|
||||
INNER JOIN {EVENT_TABLE}
|
||||
ON {SERIES_TABLE}.series_id = {EVENT_TABLE}.series_id
|
||||
WHERE {SERIES_TABLE}.series_id = %s
|
||||
"""
|
||||
|
||||
db = self.connect_db()
|
||||
cursor = db.cursor(dictionary=True)
|
||||
cursor.execute(query, (series_id,))
|
||||
data = cursor.fetchall()
|
||||
cursor.close()
|
||||
db.close()
|
||||
return data
|
||||
|
||||
async def get_all(self) -> list[dict]:
|
||||
query = f"""
|
||||
SELECT *
|
||||
FROM {SERIES_TABLE}
|
||||
INNER JOIN {EVENT_TABLE}
|
||||
ON {SERIES_TABLE}.series_id = {EVENT_TABLE}.series_id
|
||||
"""
|
||||
|
||||
db = self.connect_db()
|
||||
cursor = db.cursor(dictionary=True)
|
||||
cursor.execute(query)
|
||||
data = cursor.fetchall()
|
||||
cursor.close()
|
||||
db.close()
|
||||
return data
|
||||
|
||||
async def insert_one_series(self, series: NewEventSeries) -> int:
|
||||
query = f"INSERT INTO {self.table} (name, description) VALUES (%s, %s)"
|
||||
db = self.connect_db()
|
||||
cursor = db.cursor()
|
||||
cursor.execute(
|
||||
query,
|
||||
(
|
||||
series.name,
|
||||
series.description,
|
||||
),
|
||||
)
|
||||
inserted_id = cursor.lastrowid
|
||||
db.commit()
|
||||
cursor.close()
|
||||
db.close()
|
||||
return inserted_id
|
||||
|
||||
async def insert_one_event(self, event: NewEvent, series_id: int) -> int:
|
||||
query = f"INSERT INTO {EVENT_TABLE} (series_id, location, time, ticket_url, map_url) VALUES (%s, %s, %s, %s, %s)"
|
||||
db = self.connect_db()
|
||||
cursor = db.cursor()
|
||||
ticket_url = str(event.ticket_url) if event.ticket_url else None
|
||||
map_url = str(event.map_url) if event.map_url else None
|
||||
cursor.execute(
|
||||
query, (series_id, event.location, event.time, ticket_url, map_url)
|
||||
)
|
||||
iserted_id = cursor.lastrowid
|
||||
db.commit()
|
||||
cursor.close()
|
||||
db.close()
|
||||
return iserted_id
|
||||
|
||||
async def delete_events_by_series(self, series: EventSeries) -> None:
|
||||
query = f"DELETE FROM {EVENT_TABLE} WHERE series_id = %s"
|
||||
db = self.connect_db()
|
||||
cursor = db.cursor()
|
||||
cursor.execute(query, (series.series_id,))
|
||||
db.commit()
|
||||
cursor.close()
|
||||
|
||||
async def delete_one_series(self, series: EventSeries) -> None:
|
||||
query = f"DELETE FROM {self.table} WHERE series_id = %s"
|
||||
db = self.connect_db()
|
||||
cursor = db.cursor()
|
||||
cursor.execute(query, (series.series_id,))
|
||||
db.commit()
|
||||
cursor.close()
|
||||
|
||||
async def update_series_poster(self, series: EventSeries) -> None:
|
||||
query = f"UPDATE {self.table} SET poster_id = %s WHERE series_id = %s"
|
||||
db = self.connect_db()
|
||||
cursor = db.cursor()
|
||||
cursor.execute(query, (series.poster_id, series.series_id))
|
||||
db.commit()
|
||||
cursor.close()
|
||||
|
||||
async def replace_event(self, event: Event) -> None:
|
||||
query = f"""
|
||||
UPDATE {EVENT_TABLE}
|
||||
SET location = %s, time = %s, ticket_url = %s, map_url = %s
|
||||
WHERE event_id = %s
|
||||
"""
|
||||
db = self.connect_db()
|
||||
cursor = db.cursor()
|
||||
ticket_url = str(event.ticket_url) if event.ticket_url else None
|
||||
map_url = str(event.map_url) if event.map_url else None
|
||||
cursor.execute(
|
||||
query, (event.location, event.time, ticket_url, map_url, event.event_id)
|
||||
)
|
||||
db.commit()
|
||||
cursor.close()
|
||||
db.close()
|
||||
|
||||
async def replace_series(self, series: EventSeries) -> None:
|
||||
query = f"""
|
||||
UPDATE {self.table}
|
||||
SET name = %s, description = %s, poster_id = %s
|
||||
WHERE series_id = %s
|
||||
"""
|
||||
db = self.connect_db()
|
||||
cursor = db.cursor()
|
||||
cursor.execute(
|
||||
query, (series.name, series.description, series.poster_id, series.series_id)
|
||||
)
|
||||
db.commit()
|
||||
cursor.close()
|
||||
db.close()
|
||||
36
server/app/db/group.py
Normal file
36
server/app/db/group.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from app.db.base_queries import BaseQueries
|
||||
from app.models.group import GROUP_TABLE
|
||||
|
||||
|
||||
class GroupQueries(BaseQueries):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.table = GROUP_TABLE
|
||||
|
||||
async def get_one(self) -> dict:
|
||||
query = f"SELECT * FROM {self.table}"
|
||||
db = self.connect_db()
|
||||
cursor = db.cursor(dictionary=True)
|
||||
cursor.execute(query)
|
||||
data = cursor.fetchone()
|
||||
cursor.close()
|
||||
db.close()
|
||||
|
||||
if not data:
|
||||
raise Exception("error retrieving group")
|
||||
|
||||
return data
|
||||
|
||||
async def get_all(self) -> None:
|
||||
raise NotImplementedError(
|
||||
"get_all method not implemented for GroupQueries. There's only one row in the table."
|
||||
)
|
||||
|
||||
async def update_group_bio(self, bio: str) -> None:
|
||||
db = self.connect_db()
|
||||
cursor = db.cursor()
|
||||
query = f"UPDATE {self.table} SET bio = %s WHERE id = 1" # only one row in the table
|
||||
cursor.execute(query, (bio,))
|
||||
db.commit()
|
||||
cursor.close()
|
||||
db.close()
|
||||
29
server/app/db/musicians.py
Normal file
29
server/app/db/musicians.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from icecream import ic
|
||||
|
||||
from app.db.base_queries import BaseQueries
|
||||
from app.db.conn import connect_db
|
||||
from app.models.musician import MUSICIAN_TABLE
|
||||
|
||||
|
||||
class MusicianQueries(BaseQueries):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.table = MUSICIAN_TABLE
|
||||
|
||||
async def update_bio(self, id: int, bio: str) -> None:
|
||||
db = connect_db()
|
||||
cursor = db.cursor()
|
||||
query = f"UPDATE {self.table} SET bio = %s WHERE id = %s"
|
||||
cursor.execute(query, (bio, id))
|
||||
db.commit()
|
||||
cursor.close()
|
||||
db.close()
|
||||
|
||||
async def update_headshot(self, id: int, headshot_id: str) -> None:
|
||||
db = connect_db()
|
||||
cursor = db.cursor()
|
||||
query = f"UPDATE {self.table} SET headshot_id = %s WHERE id = %s"
|
||||
cursor.execute(query, (headshot_id, id))
|
||||
db.commit()
|
||||
cursor.close()
|
||||
db.close()
|
||||
42
server/app/db/users.py
Normal file
42
server/app/db/users.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from app.db.base_queries import BaseQueries
|
||||
from app.models.user import USER_TABLE
|
||||
|
||||
|
||||
class UserQueries(BaseQueries):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.table = USER_TABLE
|
||||
|
||||
async def get_one_by_email(self, email: str) -> dict | None:
|
||||
query = f"SELECT * FROM {self.table} WHERE email = %s"
|
||||
db = self.connect_db()
|
||||
cursor = db.cursor(dictionary=True)
|
||||
cursor.execute(query, (email,))
|
||||
data = cursor.fetchone()
|
||||
cursor.close()
|
||||
db.close()
|
||||
|
||||
return data
|
||||
|
||||
async def get_one_by_sub(self, sub: str) -> dict | None:
|
||||
query = f"SELECT * FROM {self.table} WHERE sub = %s"
|
||||
db = self.connect_db()
|
||||
cursor = db.cursor(dictionary=True)
|
||||
cursor.execute(query, (sub,))
|
||||
data = cursor.fetchone()
|
||||
cursor.close()
|
||||
db.close()
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
return data
|
||||
|
||||
async def update_sub(self, email: str, sub: str) -> None:
|
||||
query = f"UPDATE {self.table} SET sub = %s WHERE email = %s"
|
||||
db = self.connect_db()
|
||||
cursor = db.cursor()
|
||||
cursor.execute(query, (sub, email))
|
||||
db.commit()
|
||||
cursor.close()
|
||||
db.close()
|
||||
56
server/app/main.py
Normal file
56
server/app/main.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from asyncio import gather
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from app.controllers import Controller
|
||||
from app.models.tgd import TheGrapefruitsDuo
|
||||
from app.routers.contact import router as contact_router
|
||||
from app.routers.events import router as event_router
|
||||
from app.routers.group import router as group_router
|
||||
from app.routers.musicians import router as musician_router
|
||||
from app.routers.users import router as user_router
|
||||
from app.scripts.version import get_version
|
||||
|
||||
app = FastAPI(
|
||||
title="The Grapefruits Duo API",
|
||||
description="API for The Grapefruits Duo website",
|
||||
version=get_version(),
|
||||
)
|
||||
app.include_router(musician_router)
|
||||
app.include_router(group_router)
|
||||
app.include_router(contact_router)
|
||||
app.include_router(event_router)
|
||||
app.include_router(user_router)
|
||||
|
||||
controller = Controller()
|
||||
|
||||
origins = [
|
||||
"http://localhost:3000",
|
||||
"https://thegrapefruitsduo.com",
|
||||
"https://www.thegrapefruitsduo.com",
|
||||
"https://tgd.lucasjensen.me",
|
||||
]
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
@app.get("/", tags=["root"])
|
||||
async def root() -> TheGrapefruitsDuo:
|
||||
musicians, events, group = await gather(
|
||||
controller.get_musicians(),
|
||||
controller.get_events(),
|
||||
controller.get_group(),
|
||||
)
|
||||
return TheGrapefruitsDuo(
|
||||
version=get_version(),
|
||||
group=group,
|
||||
musicians=musicians,
|
||||
events=events,
|
||||
)
|
||||
0
server/app/models/__init__.py
Normal file
0
server/app/models/__init__.py
Normal file
7
server/app/models/contact.py
Normal file
7
server/app/models/contact.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from pydantic import BaseModel, EmailStr
|
||||
|
||||
|
||||
class Contact(BaseModel):
|
||||
name: str
|
||||
email: EmailStr
|
||||
message: str
|
||||
36
server/app/models/event.py
Normal file
36
server/app/models/event.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import UploadFile
|
||||
from pydantic import BaseModel, HttpUrl
|
||||
|
||||
|
||||
class Poster(BaseModel):
|
||||
file: UploadFile
|
||||
|
||||
|
||||
class NewEvent(BaseModel):
|
||||
location: str
|
||||
time: datetime
|
||||
map_url: Optional[HttpUrl] = None
|
||||
ticket_url: Optional[HttpUrl] = None
|
||||
|
||||
|
||||
class Event(NewEvent):
|
||||
event_id: int
|
||||
|
||||
|
||||
class NewEventSeries(BaseModel):
|
||||
name: str
|
||||
description: str
|
||||
events: list[NewEvent]
|
||||
|
||||
|
||||
class EventSeries(NewEventSeries):
|
||||
series_id: int
|
||||
events: list[Event]
|
||||
poster_id: Optional[str] = None
|
||||
|
||||
|
||||
SERIES_TABLE = "series"
|
||||
EVENT_TABLE = "events"
|
||||
10
server/app/models/group.py
Normal file
10
server/app/models/group.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Group(BaseModel):
|
||||
name: str
|
||||
bio: str
|
||||
id: int | None = None
|
||||
|
||||
|
||||
GROUP_TABLE = "group_table"
|
||||
16
server/app/models/musician.py
Normal file
16
server/app/models/musician.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class NewMusician(BaseModel):
|
||||
name: str
|
||||
bio: str
|
||||
headshot_id: str # cloudinary id
|
||||
|
||||
|
||||
class Musician(NewMusician):
|
||||
id: int
|
||||
|
||||
|
||||
MUSICIAN_TABLE = "musicians"
|
||||
12
server/app/models/tgd.py
Normal file
12
server/app/models/tgd.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.models.event import EventSeries
|
||||
from app.models.group import Group
|
||||
from app.models.musician import Musician
|
||||
|
||||
|
||||
class TheGrapefruitsDuo(BaseModel):
|
||||
version: str
|
||||
group: Group
|
||||
musicians: list[Musician]
|
||||
events: list[EventSeries]
|
||||
13
server/app/models/user.py
Normal file
13
server/app/models/user.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
name: str
|
||||
email: str
|
||||
sub: Optional[str] = None
|
||||
id: int | None = None
|
||||
|
||||
|
||||
USER_TABLE = "users"
|
||||
0
server/app/routers/__init__.py
Normal file
0
server/app/routers/__init__.py
Normal file
18
server/app/routers/contact.py
Normal file
18
server/app/routers/contact.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from fastapi import APIRouter, status
|
||||
|
||||
from app.admin.contact import send_email
|
||||
from app.models.contact import Contact
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/contact",
|
||||
tags=["contact"],
|
||||
responses={404: {"description": "Not found"}},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/", status_code=status.HTTP_201_CREATED)
|
||||
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}"
|
||||
body = f"From: {contact.email}\n\n{contact.message}"
|
||||
send_email(subject, body)
|
||||
56
server/app/routers/events.py
Normal file
56
server/app/routers/events.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from fastapi import APIRouter, Depends, File, UploadFile
|
||||
from fastapi.security import HTTPAuthorizationCredentials
|
||||
from icecream import ic
|
||||
|
||||
from app.admin import oauth2_http
|
||||
from app.controllers import controller
|
||||
from app.models.event import EventSeries, NewEventSeries
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/events",
|
||||
tags=["events"],
|
||||
responses={404: {"description": "Not found"}},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def get_events() -> list[EventSeries]:
|
||||
return await controller.get_events()
|
||||
|
||||
|
||||
@router.get("/{id}")
|
||||
async def get_event(id: int) -> EventSeries:
|
||||
return await controller.get_event(id)
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def create_series(
|
||||
series: NewEventSeries,
|
||||
token: HTTPAuthorizationCredentials = Depends(oauth2_http),
|
||||
) -> EventSeries:
|
||||
return await controller.create_event(series, token)
|
||||
|
||||
|
||||
@router.delete("/{id}")
|
||||
async def delete_event(
|
||||
id: int, token: HTTPAuthorizationCredentials = Depends(oauth2_http)
|
||||
) -> None:
|
||||
await controller.delete_series(id, token)
|
||||
|
||||
|
||||
@router.post("/{id}/poster")
|
||||
async def add_series_poster(
|
||||
id: int,
|
||||
poster: UploadFile = File(...),
|
||||
token: HTTPAuthorizationCredentials = Depends(oauth2_http),
|
||||
) -> EventSeries:
|
||||
return await controller.add_series_poster(id, poster, token)
|
||||
|
||||
|
||||
@router.put("/{id}")
|
||||
async def update_event(
|
||||
id: int,
|
||||
event: EventSeries,
|
||||
token: HTTPAuthorizationCredentials = Depends(oauth2_http),
|
||||
) -> EventSeries:
|
||||
return await controller.update_series(id, event, token)
|
||||
27
server/app/routers/group.py
Normal file
27
server/app/routers/group.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from fastapi.security.http import HTTPAuthorizationCredentials
|
||||
from icecream import ic
|
||||
|
||||
from app.admin import oauth2_http
|
||||
from app.controllers import controller
|
||||
from app.models.group import Group
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/group",
|
||||
tags=["group"],
|
||||
responses={404: {"description": "Not found"}},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/", status_code=status.HTTP_200_OK)
|
||||
async def get_group() -> Group:
|
||||
return await controller.get_group()
|
||||
|
||||
|
||||
@router.patch("/")
|
||||
async def update_group(
|
||||
group: Group, token: HTTPAuthorizationCredentials = Depends(oauth2_http)
|
||||
) -> Group:
|
||||
"""Updates the group bio, but requires the entire group object to be sent in the request body.
|
||||
Requires authentication."""
|
||||
return await controller.update_group_bio(group.bio, token)
|
||||
49
server/app/routers/musicians.py
Normal file
49
server/app/routers/musicians.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from fastapi import APIRouter, Depends, UploadFile, status
|
||||
from fastapi.security import HTTPAuthorizationCredentials
|
||||
from icecream import ic
|
||||
|
||||
from app.admin import oauth2_http
|
||||
from app.controllers import controller
|
||||
from app.models.musician import Musician
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/musicians",
|
||||
tags=["musicians"],
|
||||
responses={404: {"description": "Not found"}},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/", status_code=status.HTTP_200_OK)
|
||||
async def get_musicians() -> list[Musician]:
|
||||
return await controller.get_musicians()
|
||||
|
||||
|
||||
@router.get("/{id}", status_code=status.HTTP_200_OK)
|
||||
async def get_musician(id: int) -> Musician:
|
||||
return await controller.get_musician(id)
|
||||
|
||||
|
||||
@router.patch("/{id}")
|
||||
async def update_musician(
|
||||
id: int,
|
||||
musician: Musician,
|
||||
token: HTTPAuthorizationCredentials = Depends(oauth2_http),
|
||||
) -> Musician:
|
||||
"""Updates a musician's bio, but requires the entire musician object to be sent in the request body.
|
||||
Requires authentication."""
|
||||
return await controller.update_musician(
|
||||
musician=musician, url_param_id=id, token=token
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{id}/headshot", status_code=status.HTTP_200_OK)
|
||||
async def update_musician_headshot(
|
||||
id: int,
|
||||
file: UploadFile,
|
||||
token: HTTPAuthorizationCredentials = Depends(oauth2_http),
|
||||
) -> Musician | None:
|
||||
"""Recieves a headshot image file, uploads it to cloudinary, and updates the musician's headshot url in the database"""
|
||||
musician = await controller.get_musician(id)
|
||||
return await controller.update_musician(
|
||||
musician=musician, url_param_id=id, token=token, file=file
|
||||
)
|
||||
29
server/app/routers/users.py
Normal file
29
server/app/routers/users.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from fastapi.security import HTTPAuthorizationCredentials
|
||||
|
||||
from app.admin import oauth2_http
|
||||
from app.controllers import controller
|
||||
from app.models.user import User
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/users",
|
||||
tags=["users"],
|
||||
responses={404: {"description": "Not found"}},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/", status_code=status.HTTP_200_OK)
|
||||
async def get_users() -> list[User]:
|
||||
return await controller.get_users()
|
||||
|
||||
|
||||
@router.get("/{id}", status_code=status.HTTP_200_OK)
|
||||
async def get_user(id: int) -> User:
|
||||
return await controller.get_user(id)
|
||||
|
||||
|
||||
@router.post("/", status_code=status.HTTP_200_OK)
|
||||
async def create_user(
|
||||
token: HTTPAuthorizationCredentials = Depends(oauth2_http),
|
||||
) -> User | None:
|
||||
return await controller.create_user(token)
|
||||
62
server/app/scripts/DDL.sql
Normal file
62
server/app/scripts/DDL.sql
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
This file is autogenerated by DBeaver.
|
||||
Paste updated definitions below as changes are made to the database schema.
|
||||
*/
|
||||
|
||||
-- thegrapefruitsduo.group_table definition
|
||||
|
||||
CREATE TABLE `group_table` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`bio` text NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
|
||||
-- thegrapefruitsduo.musicians definition
|
||||
|
||||
CREATE TABLE `musicians` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`bio` text NOT NULL,
|
||||
`headshot_id` varchar(255) NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
|
||||
-- thegrapefruitsduo.series definition
|
||||
|
||||
CREATE TABLE `series` (
|
||||
`series_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`description` text NOT NULL,
|
||||
`poster_id` varchar(255) DEFAULT NULL,
|
||||
PRIMARY KEY (`series_id`),
|
||||
UNIQUE KEY `name` (`name`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
|
||||
-- thegrapefruitsduo.users definition
|
||||
|
||||
CREATE TABLE `users` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`email` varchar(255) NOT NULL,
|
||||
`sub` varchar(255) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
|
||||
-- thegrapefruitsduo.events definition
|
||||
|
||||
CREATE TABLE `events` (
|
||||
`event_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`series_id` int(11) NOT NULL,
|
||||
`location` varchar(255) NOT NULL,
|
||||
`time` datetime NOT NULL,
|
||||
`ticket_url` varchar(255) DEFAULT NULL,
|
||||
`map_url` varchar(255) DEFAULT NULL,
|
||||
PRIMARY KEY (`event_id`),
|
||||
KEY `series_id` (`series_id`),
|
||||
CONSTRAINT `events_ibfk_1` FOREIGN KEY (`series_id`) REFERENCES `series` (`series_id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
0
server/app/scripts/__init__.py
Normal file
0
server/app/scripts/__init__.py
Normal file
11
server/app/scripts/run.py
Normal file
11
server/app/scripts/run.py
Normal file
@@ -0,0 +1,11 @@
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main() -> None:
|
||||
curr_dir = Path(__file__).resolve().parent.absolute()
|
||||
script = curr_dir / "run.sh"
|
||||
try:
|
||||
subprocess.run(["sh", script], check=True)
|
||||
except KeyboardInterrupt:
|
||||
return
|
||||
3
server/app/scripts/run.sh
Executable file
3
server/app/scripts/run.sh
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
uvicorn app:app --reload --port 8000
|
||||
266
server/app/scripts/seed.py
Normal file
266
server/app/scripts/seed.py
Normal file
@@ -0,0 +1,266 @@
|
||||
from datetime import datetime
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
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
|
||||
|
||||
margarite: NewMusician = NewMusician(
|
||||
name="Margarite Waddell",
|
||||
bio="French hornist Margarite Waddell holds positions with the Eugene Symphony, Sarasota Opera, Boise Philharmonic, Rogue Valley Symphony, and Newport Symphony. As a freelancer, Margarite has played with ensembles throughout the West Coast including the Oregon Symphony, Portland Opera, Santa Rosa Symphony, Marin Symphony, and Symphony San Jose. She has performed with popular artists such as The Who, Josh Groban, and Sarah Brightman. Margarite can be heard on Kamyar Mohajer’s album “Pictures of the Hidden” on Navona Records. She appeared as a soloist with the Silicon Valley Philharmonic in 2016. Margarite cares deeply about music education and has taught private lessons, sectionals, and masterclasses throughout the Bay Area, Southwestern Oregon, Eugene, and Corvallis since 2013. She also performed in the San Francisco Symphony's Adventures in Music program for the 2016-2017 season. Margarite received her bachelor’s degree from the University of Oregon, and her master’s degree from the San Francisco Conservatory of Music.",
|
||||
headshot_id="zlpkcrvbdsicgj7qtslx",
|
||||
)
|
||||
|
||||
coco: NewMusician = NewMusician(
|
||||
name="Coco Bender",
|
||||
bio="Coco Bender is a pianist residing in the Pacific Northwest. She recently performed with Cascadia Composers, recorded original film scores by Portland composer Christina Rusnak for the Pioneers: First Woman Filmmakers Project, and during the pandemic presented a series of outdoor recitals featuring music by H. Leslie Adams, William Grant Still, Bartok, and others. Coco is a founding member of the Eugene based horn and piano duo, The Grapefruits, as well as a co-artistic director and musical director of an all-women circus, Girl Circus. She has taken master classes with Inna Faliks, Tamara Stefanovich, and Dr. William Chapman Nyaho. Coco currently studies with Dr. Thomas Otten. In addition to performing regularly, she teaches a large studio of students in the Pacific Northwest, from Seattle WA to Eugene OR. Coco was the accompanist for Portland treble choir Aurora Chorus, during their 2021-2022, season under the conductorship of Kathleen Hollingsworth, Margaret Green, Betty Busch, and Joan Szymko.",
|
||||
headshot_id="coco_copy_jywbxm",
|
||||
)
|
||||
|
||||
coco_user: User = User(
|
||||
name="Coco Bender",
|
||||
email="cocobender.piano@gmail.com",
|
||||
)
|
||||
|
||||
margarite_user: User = User(
|
||||
name="Margarite Waddell",
|
||||
email="mgwaddell@gmail.com",
|
||||
)
|
||||
|
||||
lucas_user: User = User(name="Lucas Jensen", email="lucas.p.jensen10@gmail.com")
|
||||
|
||||
tgd_user: User = User(
|
||||
name="The Grapefruits Duo",
|
||||
email="thegrapefruitsduo@gmail.com",
|
||||
)
|
||||
|
||||
tgd_website: User = User(
|
||||
name="The Grapefruits Duo Website", email="grapefruitswebsite@gmail.com"
|
||||
)
|
||||
|
||||
|
||||
tgd: Group = Group(
|
||||
bio="The Grapefruits, comprising of Coco Bender, piano, and Margarite Waddell, french horn, are a contemporary classical music duo. They perform frequently through out the PNW with the goal presenting traditional classical french horn repertoire, new 20th century works, and commissioned works by PNW composers.",
|
||||
name="The Grapefruits Duo",
|
||||
)
|
||||
|
||||
|
||||
series1 = EventSeries(
|
||||
name="The Grapefruits Duo Presents: Works for Horn and Piano",
|
||||
description="Pieces by Danzi, Gomez, Gounod, Grant, and Rusnak!",
|
||||
poster_id="The_Grapefruits_Present_qhng6y",
|
||||
events=[
|
||||
Event(
|
||||
location="Medford, OR",
|
||||
time=datetime(2024, 5, 31, 19),
|
||||
event_id=0,
|
||||
),
|
||||
Event(
|
||||
location="First Presbyterian Church Newport",
|
||||
time=datetime(2024, 6, 16, 16),
|
||||
event_id=0,
|
||||
map_url="https://maps.app.goo.gl/hNfN8X5FBZLg8LDF8", # type: ignore
|
||||
),
|
||||
Event(
|
||||
location="First Church of Christ, Scientist, Eugene",
|
||||
time=datetime(2024, 6, 23, 15),
|
||||
event_id=0,
|
||||
),
|
||||
],
|
||||
series_id=0,
|
||||
)
|
||||
|
||||
# series2 = EventSeries(
|
||||
# name="The Grapefruits Duo Features: Solos for Bass Trombone",
|
||||
# description="Pieces by Ewazen, Bozza, and more!",
|
||||
# events=[
|
||||
# Event(
|
||||
# location="Eugene Family YMCA",
|
||||
# time=datetime(2024, 7, 1, 17, 30),
|
||||
# event_id=0,
|
||||
# ),
|
||||
# Event(
|
||||
# location="Tobi's Crate",
|
||||
# time=datetime(2024, 7, 2, 20),
|
||||
# event_id=0,
|
||||
# ticket_url="http://www.example.com", # type: ignore
|
||||
# ),
|
||||
# ],
|
||||
# id=0,
|
||||
# )
|
||||
|
||||
|
||||
def seed():
|
||||
confirmation = input(
|
||||
"Are you sure you want to seed the database? Data will be lost. [Y/n]: "
|
||||
)
|
||||
if confirmation.lower() not in ["y", "yes", ""]:
|
||||
print("Exiting")
|
||||
return
|
||||
print("Seeding database")
|
||||
add_musicians()
|
||||
add_users()
|
||||
add_group()
|
||||
add_events()
|
||||
|
||||
|
||||
def add_group():
|
||||
print("Adding group")
|
||||
db = connect_db()
|
||||
cursor = db.cursor()
|
||||
cursor.execute(
|
||||
f"DROP TABLE IF EXISTS {GROUP_TABLE};",
|
||||
)
|
||||
cursor.execute(
|
||||
f"""
|
||||
CREATE TABLE {GROUP_TABLE} (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
bio TEXT NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
"""
|
||||
)
|
||||
cursor.execute(
|
||||
f"INSERT INTO {GROUP_TABLE} (name, bio) VALUES (%s, %s);",
|
||||
(tgd.name, tgd.bio),
|
||||
)
|
||||
db.commit()
|
||||
cursor.close()
|
||||
|
||||
|
||||
def add_users():
|
||||
print("Adding users")
|
||||
db = connect_db()
|
||||
cursor = db.cursor()
|
||||
cursor.execute(
|
||||
f"DROP TABLE IF EXISTS {USER_TABLE};",
|
||||
)
|
||||
cursor.execute(
|
||||
f"""
|
||||
CREATE TABLE {USER_TABLE} (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
sub VARCHAR(255),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
"""
|
||||
)
|
||||
for u in [coco_user, margarite_user, lucas_user, tgd_user, tgd_website]:
|
||||
cursor.execute(
|
||||
f"INSERT INTO {USER_TABLE} (name, email, sub) VALUES (%s, %s, %s);",
|
||||
(u.name, u.email, u.sub),
|
||||
)
|
||||
|
||||
db.commit()
|
||||
cursor.close()
|
||||
|
||||
|
||||
def add_musicians():
|
||||
print("Adding musicians")
|
||||
|
||||
db = connect_db()
|
||||
cursor = db.cursor()
|
||||
cursor.execute(
|
||||
f"DROP TABLE IF EXISTS {MUSICIAN_TABLE};",
|
||||
)
|
||||
cursor.execute(
|
||||
f"""
|
||||
CREATE TABLE {MUSICIAN_TABLE} (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
bio TEXT NOT NULL,
|
||||
headshot_id VARCHAR(255) NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
"""
|
||||
)
|
||||
for m in [margarite, coco]:
|
||||
cursor.execute(
|
||||
f"INSERT INTO {MUSICIAN_TABLE} (name, bio, headshot_id) VALUES (%s, %s, %s);",
|
||||
(m.name, m.bio, m.headshot_id),
|
||||
)
|
||||
|
||||
db.commit()
|
||||
cursor.close()
|
||||
|
||||
|
||||
def add_events():
|
||||
print("Adding events")
|
||||
|
||||
db = connect_db()
|
||||
cursor = db.cursor()
|
||||
cursor.execute(
|
||||
f"DROP TABLE IF EXISTS {EVENT_TABLE};",
|
||||
)
|
||||
cursor.execute(
|
||||
f"DROP TABLE IF EXISTS {SERIES_TABLE};",
|
||||
)
|
||||
cursor.execute(
|
||||
f"""
|
||||
CREATE TABLE {SERIES_TABLE} (
|
||||
series_id INT NOT NULL AUTO_INCREMENT,
|
||||
name VARCHAR(255) NOT NULL UNIQUE,
|
||||
description TEXT NOT NULL,
|
||||
poster_id VARCHAR(255),
|
||||
PRIMARY KEY (series_id)
|
||||
);
|
||||
"""
|
||||
)
|
||||
cursor.execute(
|
||||
f"""
|
||||
CREATE TABLE {EVENT_TABLE} (
|
||||
event_id INT NOT NULL AUTO_INCREMENT,
|
||||
series_id INT NOT NULL,
|
||||
location VARCHAR(255) NOT NULL,
|
||||
time DATETIME NOT NULL,
|
||||
ticket_url VARCHAR(255),
|
||||
map_url VARCHAR(255),
|
||||
PRIMARY KEY (event_id),
|
||||
FOREIGN KEY (series_id) REFERENCES {SERIES_TABLE}(series_id) ON DELETE CASCADE
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
for series in [series1]:
|
||||
cursor.execute(
|
||||
f"INSERT INTO {SERIES_TABLE} (name, description, poster_id) VALUES (%s, %s, %s);",
|
||||
(
|
||||
series.name,
|
||||
series.description,
|
||||
series.poster_id,
|
||||
),
|
||||
)
|
||||
series_id = cursor.lastrowid
|
||||
if series_id is None:
|
||||
raise Exception("Error inserting series: could not get last row id.")
|
||||
series.series_id = series_id
|
||||
for event in series.events:
|
||||
ticket_url = str(event.ticket_url) if event.ticket_url else None
|
||||
map_url = str(event.map_url) if event.map_url else None
|
||||
cursor.execute(
|
||||
f"INSERT INTO {EVENT_TABLE} (series_id, location, time, ticket_url, map_url) VALUES (%s, %s, %s, %s, %s);",
|
||||
(
|
||||
series.series_id,
|
||||
event.location,
|
||||
event.time,
|
||||
ticket_url,
|
||||
map_url,
|
||||
),
|
||||
)
|
||||
|
||||
db.commit()
|
||||
cursor.close()
|
||||
|
||||
|
||||
def main():
|
||||
load_dotenv()
|
||||
seed()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
18
server/app/scripts/version.py
Normal file
18
server/app/scripts/version.py
Normal file
@@ -0,0 +1,18 @@
|
||||
import toml
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_version() -> str:
|
||||
abs_path = Path(__file__).resolve().parent.parent.parent / "pyproject.toml"
|
||||
try:
|
||||
with open(abs_path) as file:
|
||||
data = toml.load(file)
|
||||
return data["tool"]["poetry"]["version"]
|
||||
except Exception as e:
|
||||
return "0.0.0"
|
||||
finally:
|
||||
file.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(get_version())
|
||||
Reference in New Issue
Block a user