Skip to content

Commit

Permalink
Refactor and restructure project files to modularity
Browse files Browse the repository at this point in the history
- Api routers v1 folder with endpoints
- configs, utils and sqlalchemy models specific folder
- Schemas pydantic validatation folder with renamed to like Interfaces
  ISchemaClassName
  • Loading branch information
dotpep committed Mar 4, 2024
1 parent a9efdc0 commit e22c3c7
Show file tree
Hide file tree
Showing 33 changed files with 257 additions and 228 deletions.
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,7 @@ Endpoints:
- [x] Conteinerize with Docker & Docker compose
- [x] Configure Nginx and Uvicorn ASGI, Gunicorn workers
- [ ] GitHub action, CI/CD Pipeline
- [ ] Use Poetry insead of pip in action steps
- [ ] Apply SOLID principles, Best practices and Common patterns for Backend/API
- [ ] Apply Clean Architecture, SOLID principles, Best practices and Common patterns for Backend/API
- [ ] Restructure project
- [ ] Continue project, add new features, ideas like logging etc.

Expand Down
8 changes: 6 additions & 2 deletions alembic/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@

from alembic import context

from app.models import Base
from app.config import settings
#from sqlmodel import SQLModel

#from app.models import *
from app.configs.database import Base
from app.configs.environment import settings

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
Expand All @@ -29,6 +32,7 @@
# target_metadata = mymodel.Base.metadata
target_metadata = Base.metadata


# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
Expand Down
5 changes: 3 additions & 2 deletions app/database.py → app/configs/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

from .config import settings
from .utils import create_database_automatically
from .environment import settings
from app.utils.database import create_database_automatically


#import psycopg2
#from psycopg2.extras import RealDictCursor
Expand Down
File renamed without changes.
11 changes: 6 additions & 5 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from .routers import post, user, auth, vote
from app.routers import v1

#models.Base.metadata.create_all(bind=engine)

Expand All @@ -18,16 +18,17 @@
)


app.include_router(post.router)
app.include_router(vote.router)
app.include_router(user.router)
app.include_router(auth.router)
app.include_router(v1.post.router)
app.include_router(v1.vote.router)
app.include_router(v1.user.router)
app.include_router(v1.auth.router)

app.get('/', tags=["Welcome Home Page"])(lambda: {
"message": "Welcome to my Social Media API powered by FastAPI, API documentation in '/docs' and `/redoc` endpoint. Successfully CI/CD pipeline deployment!",
"source_code": "https://github.com/dotpep/social-media-api",
"domain_name": "https://dotpep.xyz/"})


@app.get('/root', tags=["Welcome Home Page"], status_code=200)
def root():
return {"message": "Hello World"}
19 changes: 1 addition & 18 deletions app/models.py → app/models/post.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from sqlalchemy.orm import relationship
from sqlalchemy.sql.expression import text

from .database import Base
from app.configs.database import Base


class Post(Base):
Expand All @@ -16,20 +16,3 @@ class Post(Base):
owner_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)

owner = relationship("User")


class User(Base):
__tablename__ = "users"

id = Column(Integer, primary_key=True, nullable=False)
email = Column(String, nullable=False, unique=True)
password = Column(String, nullable=False)
created_at = Column(TIMESTAMP(timezone=True), nullable=False, server_default=text('NOW()'))


class Vote(Base):
__tablename__ = "votes"

user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), primary_key=True)
post_id = Column(Integer, ForeignKey("posts.id", ondelete="CASCADE"), primary_key=True)

13 changes: 13 additions & 0 deletions app/models/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from sqlalchemy import Column, Integer, String, TIMESTAMP
from sqlalchemy.sql.expression import text

from app.configs.database import Base


class User(Base):
__tablename__ = "users"

id = Column(Integer, primary_key=True, nullable=False)
email = Column(String, nullable=False, unique=True)
password = Column(String, nullable=False)
created_at = Column(TIMESTAMP(timezone=True), nullable=False, server_default=text('NOW()'))
10 changes: 10 additions & 0 deletions app/models/vote.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from sqlalchemy import Column, Integer, ForeignKey

from app.configs.database import Base


class Vote(Base):
__tablename__ = "votes"

user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), primary_key=True)
post_id = Column(Integer, ForeignKey("posts.id", ondelete="CASCADE"), primary_key=True)
14 changes: 9 additions & 5 deletions app/routers/auth.py → app/routers/v1/auth.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,31 @@
from fastapi import APIRouter, Depends, status, HTTPException, Response
from fastapi import APIRouter, Depends, status, HTTPException
from fastapi.security.oauth2 import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session

from .. import database, schemas, models, utils, oauth2
from app.models.user import User
from app.schemas.auth import IToken
from app.configs import database
from app.utils import auth, oauth2


router = APIRouter(
tags=["Authentication"]
)


@router.post('/login', response_model=schemas.Token)
@router.post('/login', response_model=IToken)
def user_login(
user_credentials: OAuth2PasswordRequestForm = Depends(),
db: Session = Depends(database.get_db)
):
user = db.query(models.User).filter_by(
user = db.query(User).filter_by(
email=user_credentials.username).first()

if user is None:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN,
detail=f"Invalid Credentials")

if not utils.verify(user_credentials.password, user.password):
if not auth.verify(user_credentials.password, user.password):
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN,
detail=f"Invalid Credentials")

Expand Down
75 changes: 40 additions & 35 deletions app/routers/post.py → app/routers/v1/post.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,24 @@
from typing import List, Optional
from sqlalchemy import func

from .. import models, schemas, oauth2
from ..database import get_db
from app.models.post import Post
from app.schemas.user import IUser
from app.schemas.vote import Vote
from app.schemas.post import IPost, ICreatePost, IUpdatePost, IPostVote
from app.configs import database
from app.utils import oauth2


router = APIRouter(
prefix="/posts",
tags=['Posts']
)


@router.get('/', response_model=List[schemas.PostVote])
@router.get('/', response_model=List[IPostVote])
def get_all_posts(
db: Session = Depends(get_db),
current_user: schemas.User = Depends(oauth2.get_current_user),
db: Session = Depends(database.get_db),
current_user: IUser = Depends(oauth2.get_current_user),
# query params
limit: int = 5,
skip: int = 0,
Expand All @@ -34,28 +39,28 @@ def get_all_posts(
# models.Vote, models.Vote.post_id == models.Post.id, isouter=True).group_by(models.Post.id).all()


my_post = db.query(models.Post, func.count(models.Vote.post_id).label("votes"))
my_post = db.query(Post, func.count(Vote.post_id).label("votes"))

post_join = my_post.join(
models.Vote, models.Vote.post_id == models.Post.id, isouter=True).group_by(models.Post.id)
Vote, Vote.post_id == Post.id, isouter=True).group_by(Post.id)

post_filter = post_join.filter(models.Post.title.contains(search))
post_filter = post_join.filter(Post.title.contains(search))
if owner_id is not None:
post_filter = post_filter.filter(models.Post.owner_id == int(owner_id))
post_filter = post_filter.filter(Post.owner_id == int(owner_id))
if logined_user:
post_filter = post_filter.filter(models.Post.owner_id == current_user.id)
post_filter = post_filter.filter(Post.owner_id == current_user.id)

post_paginate = post_filter.limit(limit).offset(skip)

return post_paginate.all()


@router.post('/', status_code=status.HTTP_201_CREATED, response_model=schemas.Post)
@router.post('/', status_code=status.HTTP_201_CREATED, response_model=IPost)
def create_post(
post: schemas.CreatePost,
db: Session = Depends(get_db),
post: ICreatePost,
db: Session = Depends(database.get_db),
# Constraint/Dependency to check Is User Authenticatated, Access to it if only True and user provide JWT token in body request
current_user: schemas.User = Depends(oauth2.get_current_user)
current_user: IUser = Depends(oauth2.get_current_user)
):
#cursor.execute(
# """
Expand All @@ -69,18 +74,18 @@ def create_post(
#conn.commit()

#new_post = models.Post(title=post.title, content=post.content, published=post.published)
new_post = models.Post(owner_id=current_user.id, **post.dict())
new_post = Post(owner_id=current_user.id, **post.dict())
db.add(new_post)
db.commit()
db.refresh(new_post) # Retrieve created post (RETURNING *)

return new_post


@router.get('/latest', response_model=schemas.PostVote)
@router.get('/latest', response_model=IPostVote)
def get_latest_post(
db: Session = Depends(get_db),
current_user: schemas.User = Depends(oauth2.get_current_user)
db: Session = Depends(database.get_db),
current_user: IUser = Depends(oauth2.get_current_user)
):
#cursor.execute(
# """
Expand All @@ -94,22 +99,22 @@ def get_latest_post(
#join_vote = db.query(models.Post, func.count(models.Vote.post_id).label("votes")).join(
# models.Vote, models.Vote.post_id == models.Post.id, isouter=True).group_by(models.Post.id).all()

post = db.query(models.Post, func.count(models.Vote.post_id).label("votes"))
post = db.query(Post, func.count(Vote.post_id).label("votes"))

join_vote = post.join(
models.Vote, models.Vote.post_id == models.Post.id, isouter=True).group_by(models.Post.id)
Vote, Vote.post_id == Post.id, isouter=True).group_by(Post.id)

latest_post = join_vote.order_by(
models.Post.created_at.desc())
Post.created_at.desc())

return latest_post.first()


@router.get('/{id}', response_model=schemas.PostVote)
@router.get('/{id}', response_model=IPostVote)
def get_specific_post_detail(
id: int,
db: Session = Depends(get_db),
current_user: schemas.User = Depends(oauth2.get_current_user)
db: Session = Depends(database.get_db),
current_user: IUser = Depends(oauth2.get_current_user)
):
#cursor.execute(
# """
Expand All @@ -122,12 +127,12 @@ def get_specific_post_detail(

#post = db.query(models.Post).filter(models.Post.id == id).first()

post = db.query(models.Post, func.count(models.Vote.post_id).label("votes"))
post = db.query(Post, func.count(Vote.post_id).label("votes"))

join_vote = post.join(
models.Vote, models.Vote.post_id == models.Post.id, isouter=True).group_by(models.Post.id)
Vote, Vote.post_id == Post.id, isouter=True).group_by(Post.id)

specific_post = join_vote.filter(models.Post.id == id).first()
specific_post = join_vote.filter(Post.id == id).first()

if specific_post is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
Expand All @@ -136,11 +141,11 @@ def get_specific_post_detail(
return specific_post


@router.put('/{id}', response_model=schemas.Post)
@router.put('/{id}', response_model=IPost)
def update_post(
id: int, updated_post: schemas.UpdatePost,
db: Session = Depends(get_db),
current_user: schemas.User = Depends(oauth2.get_current_user)
id: int, updated_post: IUpdatePost,
db: Session = Depends(database.get_db),
current_user: IUser = Depends(oauth2.get_current_user)
):
#cursor.execute(
# """
Expand All @@ -154,7 +159,7 @@ def update_post(
#updated_post = cursor.fetchone()
#conn.commit()

post_query = db.query(models.Post).filter_by(id=id)
post_query = db.query(Post).filter_by(id=id)
post = post_query.first()

if post is None:
Expand All @@ -175,8 +180,8 @@ def update_post(
@router.delete('/{id}', status_code=status.HTTP_204_NO_CONTENT)
def delete_post(
id: int,
db: Session = Depends(get_db),
current_user: schemas.User = Depends(oauth2.get_current_user)
db: Session = Depends(database.get_db),
current_user: IUser = Depends(oauth2.get_current_user)
):
#cursor.execute(
# """
Expand All @@ -189,7 +194,7 @@ def delete_post(
#deleted_post = cursor.fetchone()
#conn.commit()

post_query = db.query(models.Post).filter_by(id=id)
post_query = db.query(Post).filter_by(id=id)
post = post_query.first()

if post is None:
Expand Down
Loading

0 comments on commit e22c3c7

Please sign in to comment.