From 089b09afacc7c2f9fa959992160038b9895950f3 Mon Sep 17 00:00:00 2001 From: Tyrel Souza Date: Fri, 14 Oct 2022 13:52:31 -0400 Subject: [PATCH] initial commit --- .gitignore | 167 +++++++++++++++++++++++++++++++++++++++++++++++ api.py | 65 ++++++++++++++++++ app.py | 9 +++ crud.py | 64 ++++++++++++++++++ database.py | 27 ++++++++ exceptions.py | 15 +++++ models.py | 12 ++++ requirements.txt | 7 ++ schemas.py | 24 +++++++ 9 files changed, 390 insertions(+) create mode 100644 .gitignore create mode 100644 api.py create mode 100644 app.py create mode 100644 crud.py create mode 100644 database.py create mode 100644 exceptions.py create mode 100644 models.py create mode 100644 requirements.txt create mode 100644 schemas.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e09f679 --- /dev/null +++ b/.gitignore @@ -0,0 +1,167 @@ +# Created by https://www.toptal.com/developers/gitignore/api/python +# Edit at https://www.toptal.com/developers/gitignore?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# End of https://www.toptal.com/developers/gitignore/api/python + diff --git a/api.py b/api.py new file mode 100644 index 0000000..066518b --- /dev/null +++ b/api.py @@ -0,0 +1,65 @@ +from fastapi import APIRouter, Depends, HTTPException +from fastapi_utils.cbv import cbv +from sqlalchemy.orm import Session +from crud import get_all_albums, create_album, get_album_by_id, update_album, delete_album +from database import get_db +from exceptions import AlbumException +from schemas import Album, CreateAndUpdateAlbum, PaginatedAlbum + +router = APIRouter() + + +@cbv(router) +class Albums: + session: Session = Depends(get_db) + + # API to get the list of album info + @router.get("/albums", response_model=PaginatedAlbum) + def list_albums(self, limit: int = 10, offset: int = 0): + + albums_list = get_all_albums(self.session, limit, offset) + response = {"limit": limit, "offset": offset, "data": albums_list} + + return response + + # API endpoint to add a album info to the database + @router.post("/albums") + def add_album(self, album: CreateAndUpdateAlbum): + + try: + album = create_album(self.session, album) + return album + except AlbumException as ex: + raise HTTPException(**ex.__dict__) + + +# API endpoint to get info of a particular album +@router.get("/albums/{album_id}", response_model=Album) +def get_album(album_id: int, session: Session = Depends(get_db)): + + try: + album = get_album_by_id(session, album_id) + return album + except AlbumException as ex: + raise HTTPException(**ex.__dict__) + + +# API to update a existing album info +@router.put("/albums/{album_id}", response_model=Album) +def update_album(album_id: int, new_info: CreateAndUpdateAlbum, session: Session = Depends(get_db)): + + try: + album = update_album(session, album_id, new_info) + return album + except AlbumException as ex: + raise HTTPException(**ex.__dict__) + + +# API to delete a album info from the data base +@router.delete("/albums/{album_id}") +def delete_album(album_id: int, session: Session = Depends(get_db)): + + try: + return delete_album(session, album_id) + except AlbumException as ex: + raise HTTPException(**ex.__dict__) diff --git a/app.py b/app.py new file mode 100644 index 0000000..53e4b98 --- /dev/null +++ b/app.py @@ -0,0 +1,9 @@ +from fastapi import FastAPI +from pydantic import BaseModel +import uvicorn +import api + +# Initialize the app +app = FastAPI() + +app.include_router(api.router) diff --git a/crud.py b/crud.py new file mode 100644 index 0000000..b133825 --- /dev/null +++ b/crud.py @@ -0,0 +1,64 @@ +# crud.py +from typing import List +from sqlalchemy.orm import Session +from exceptions import AlbumAlreadyExistError, AlbumNotFoundError +from models import Album +from schemas import CreateAndUpdateAlbum + + +# Function to get list of album info +def get_all_albums(session: Session, limit: int, offset: int) -> List[Album]: + return session.query(Album).offset(offset).limit(limit).all() + + +# Function to get info of a particular album +def get_album_by_id(session: Session, _id: int) -> Album: + album = session.query(Album).get(_id) + + if album is None: + raise AlbumNotFoundError + + return album + + +# Function to add a new album info to the database +def create_album(session: Session, album: CreateAndUpdateAlbum) -> Album: + album_details = session.query(Album).filter(Album.title == album.title, Album.artist == album.artist).first() + if album_details is not None: + raise AlbumAlreadyExistError + + new_album = Album(**album.dict()) + session.add(new_album) + session.commit() + session.refresh(new_album) + return new_album + + +# Function to update details of the album +def update_album(session: Session, _id: int, info_update: CreateAndUpdateAlbum) -> Album: + album = get_album_by_id(session, _id) + + if album is None: + raise AlbumNotFoundError + + album.title = info_update.title + album.artist = info_update.artist + album.price = info_update.price + + session.commit() + session.refresh(album) + + return album + + +# Function to delete a album info from the db +def delete_album(session: Session, _id: int): + album = get_album_by_id(session, _id) + + if album is None: + raise AlbumNotFoundError + + session.delete(album) + session.commit() + + return diff --git a/database.py b/database.py new file mode 100644 index 0000000..d1494d9 --- /dev/null +++ b/database.py @@ -0,0 +1,27 @@ +# database.py +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + +try: + import pymysql + pymysql.install_as_MySQLdb() + import MySQLdb +except ImportError as e: + raise ImportError(f"Failed to import pymysql as MySQLdb: {e}") + +DATABASE_URL = "mysql+mysqldb://mysql:password@127.0.0.1/db" + +db_engine = create_engine(DATABASE_URL) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=db_engine) + +Base = declarative_base() + + +def get_db(): + db = None + try: + db = SessionLocal() + yield db + finally: + db.close() \ No newline at end of file diff --git a/exceptions.py b/exceptions.py new file mode 100644 index 0000000..37ad34f --- /dev/null +++ b/exceptions.py @@ -0,0 +1,15 @@ +# exceptions.py +class AlbumException(Exception): + ... + + +class AlbumNotFoundError(AlbumException): + def __init__(self): + self.status_code = 404 + self.detail = "Album Not Found" + + +class AlbumAlreadyExistError(AlbumException): + def __init__(self): + self.status_code = 409 + self.detail = "Album Already Exists" diff --git a/models.py b/models.py new file mode 100644 index 0000000..f9a7c3a --- /dev/null +++ b/models.py @@ -0,0 +1,12 @@ +from sqlalchemy.schema import Column +from sqlalchemy.types import String, Integer, Float +from database import Base + + +class Album(Base): + __tablename__ = "albums" + + id = Column(Integer, primary_key=True, index=True) + title = Column(String) + artist = Column(String) + price = Column(Float) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9ffba72 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +fastapi +uvicorn +sqlalchemy +pymysql +pydantic +fastapi_utils +mysql-connector-python \ No newline at end of file diff --git a/schemas.py b/schemas.py new file mode 100644 index 0000000..42aeb20 --- /dev/null +++ b/schemas.py @@ -0,0 +1,24 @@ +from pydantic import BaseModel +from typing import Optional, List + + +# TO support creation and update APIs +class CreateAndUpdateAlbum(BaseModel): + title: str + artist: str + price: float + + +# TO support list and get APIs +class Album(CreateAndUpdateAlbum): + id: int + + class Config: + orm_mode = True + + +# To support list cars API +class PaginatedAlbum(BaseModel): + limit: int + offset: int + data: List[Album]