Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# .flake8
[flake8]
max-line-length = 98
extend-ignore = E203
exclude =
.git,
__pycache__,
.venv,
env,
venv,
alembic/versions
68 changes: 68 additions & 0 deletions .github/workflows/github-action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: CI/CD Pipeline for Blogpost

on:
push:
branches:
- '*'
pull_request:
branches:
- main

jobs:
Development-Tests:
runs-on: ubuntu-latest

services:
postgres:
image: postgres:15
env:
POSTGRES_USER: ${{ secrets.DB_USERNAME }}
POSTGRES_PASSWORD: ${{ secrets.DB_PASSWORD }}
POSTGRES_DB: ${{ secrets.TEST_DB_NAME }}
ports:
- 5432:5432
options: >-
--health-cmd="pg_isready"
--health-interval=10s
--health-timeout=5s
--health-retries=5

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest flake8

- name: Set environment variables for testing
run: |
echo "Start Postgresql database set up"
echo "db_username=${{ secrets.DB_USERNAME }}" >> $GITHUB_ENV
echo "db_password=${{ secrets.DB_PASSWORD }}" >> $GITHUB_ENV
echo "db_hostname=${{ secrets.DB_HOSTNAME }}" >> $GITHUB_ENV
echo "db_name=${{ secrets.DB_NAME }}" >> $GITHUB_ENV
echo "db_port=${{ secrets.DB_PORT }}" >> $GITHUB_ENV
echo "test_db_name=${{ secrets.TEST_DB_NAME }}" >> $GITHUB_ENV
echo "secret_key=${{ secrets.SECRET_KEY }}" >> $GITHUB_ENV
echo "algorithm=${{ secrets.ALGORITHM }}" >> $GITHUB_ENV
echo "access_token_expire_minutes=${{ secrets.ACCESS_TOKEN_EXPIRE_MINUTES }}" >> $GITHUB_ENV
echo "alembic_database_url=postgresql://${{ secrets.DB_USERNAME }}:${{ secrets.DB_PASSWORD }}@${{ secrets.DB_HOSTNAME }}:${{ secrets.DB_PORT }}/${{ secrets.TEST_DB_NAME }}" >> $GITHUB_ENV
echo "End database set up."
- name: Run linting
run: |
flake8 --verbose

- name: Run tests
run: |
pytest tests/ -v
# - name: Run development server
# run: |
# gunicorn -w 4 -k uvicorn.workers.UvicornWorker blogpost:app
2 changes: 1 addition & 1 deletion alembic.ini
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ path_separator = os
# database URL. This is consumed by the user-maintained env.py script only.
# other means of configuring database URLs may be customized within the env.py
# file.
sqlalchemy.url = driver://user:pass@localhost/dbname
sqlalchemy.url = driver://user:pass@localhost:port/dbname


[post_write_hooks]
Expand Down
13 changes: 7 additions & 6 deletions alembic/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
from database import DATABASE_URL
from models import Base
from tests.conftest import TEST_DATABASE_URL
import os

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config

# provide database URL for database name, user, password, host, and port
config.set_main_option("sqlalchemy.url", DATABASE_URL)
# Read database URL from env vars
db_url = os.getenv("ALEMBIC_DATABASE_URL")
if not db_url:
raise RuntimeError("ALEMBIC_DB_URL environment variable is not set")

# provide database URL for testing environment
#config.set_main_option("sqlalchemy.url", TEST_DATABASE_URL)
config.set_main_option("sqlalchemy.url", db_url)

# Interpret the config file for Python logging.
# This line sets up loggers basically.
Expand All @@ -28,6 +28,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
49 changes: 25 additions & 24 deletions alembic/versions/a68d3c1313e9_create_all_tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = 'a68d3c1313e9'
down_revision: Union[str, Sequence[str], None] = None
Expand All @@ -22,31 +21,33 @@ def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('users',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('email', sa.String(), nullable=False),
sa.Column('password', sa.String(), nullable=False),
sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email')
)
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('email', sa.String(), nullable=False),
sa.Column('password', sa.String(), nullable=False),
sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'),
nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email')
)
op.create_table('posts',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(), nullable=False),
sa.Column('content', sa.String(), nullable=False),
sa.Column('is_published', sa.Boolean(), server_default='TRUE', nullable=False),
sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('title')
)
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(), nullable=False),
sa.Column('content', sa.String(), nullable=False),
sa.Column('is_published', sa.Boolean(), server_default='TRUE', nullable=False),
sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'),
nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('title')
)
op.create_table('votes',
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('post_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['post_id'], ['posts.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('user_id', 'post_id')
)
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('post_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['post_id'], ['posts.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('user_id', 'post_id')
)
# ### end Alembic commands ###


Expand Down
9 changes: 2 additions & 7 deletions blogpost.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
from fastapi import FastAPI
from routers import vote, users, auth, posts
from fastapi.middleware.cors import CORSMiddleware
# import models
# from database import engine

# import models

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

app = FastAPI()

Expand All @@ -23,7 +22,3 @@
app.include_router(users.router)
app.include_router(auth.router)
app.include_router(vote.router)

@app.get("/")
def root():
return {'message': 'Hello world!'}
31 changes: 12 additions & 19 deletions database.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,25 @@
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from config import settings
# import psycopg2
# from psycopg2.extras import RealDictCursor
# import time
from sqlalchemy.orm import sessionmaker
from settings import Setting
import os

# database URL set up
DATABASE_URL = f"postgresql://{settings.db_username}:{settings.db_password}@{settings.db_hostname}/{settings.db_name}"
DATABASE_URL = (
f"postgresql://{Setting.db_username}:{Setting.db_password}"
f"@{Setting.db_hostname}:{Setting.db_port}/{Setting.db_name}"
)
os.environ["ALEMBIC_DATABASE_URL"] = DATABASE_URL
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, bind=engine, autoflush=False)

# Base class for models
Base = declarative_base()


def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()

Base = declarative_base()

# while True:
# try:
# conn = psycopg2.connect(host='localhost', database='fastapi', user='postgres', password='mehar12',
# cursor_factory=RealDictCursor)
# cursor = conn.cursor()
# print("Database connection was successful!")
# break
# except Exception as e:
# print("Failed database server connection:", e)
# time.sleep(2)
20 changes: 13 additions & 7 deletions models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ class Post(Base):
title = Column(String, nullable=False, unique=True)
content = Column(String, nullable=False)
is_published = Column(Boolean, nullable=False, server_default='TRUE')
created_at = Column(TIMESTAMP(timezone=True), nullable=False, server_default=text('now()'))
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
created_at = Column(TIMESTAMP(timezone=True), nullable=False,
server_default=text('now()'))
user_id = Column(Integer, ForeignKey("users.id",
ondelete="CASCADE"), nullable=False)

user = relationship("User")

Expand All @@ -22,11 +24,15 @@ class User(Base):
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()'))
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"), nullable=False,
primary_key=True)
post_id = Column(Integer, ForeignKey("posts.id", ondelete="CASCADE"), nullable=False,
primary_key=True)
user_id = Column(Integer,
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False, primary_key=True)
post_id = Column(Integer,
ForeignKey("posts.id", ondelete="CASCADE"),
nullable=False, primary_key=True)
20 changes: 12 additions & 8 deletions oauth2.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import jwt
from jwt.exceptions import InvalidTokenError
from datetime import datetime, timedelta, timezone
import schemas, database, models
import schemas
import database
import models
from fastapi import Depends, status, HTTPException
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session
from config import settings

from settings import Setting

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")
SECRET_KEY = settings.secret_key
ALGORITHM = settings.algorithm
ACCESS_TOKEN_EXPIRE_MINUTES = settings.access_token_expire_minutes
SECRET_KEY = Setting.secret_key
ALGORITHM = Setting.algorithm
ACCESS_TOKEN_EXPIRE_MINUTES = Setting.access_token_expire_minutes


def create_access_token(data: dict):
to_encode = data.copy()
Expand All @@ -20,6 +22,7 @@ def create_access_token(data: dict):
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt


def varify_access_token(token: str, credential_exception):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
Expand All @@ -32,10 +35,11 @@ def varify_access_token(token: str, credential_exception):
return token_data


def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(database.get_db)):
def get_current_user(token: str = Depends(oauth2_scheme),
db: Session = Depends(database.get_db)):
credential_exception = HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"})
token = varify_access_token(token, credential_exception)
user = db.query(models.User).filter(models.User.id == token.id).first()
return user
return user
19 changes: 13 additions & 6 deletions routers/auth.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
from fastapi import APIRouter, Depends, status, HTTPException
from fastapi import APIRouter, Depends
from fastapi import status, HTTPException
from sqlalchemy.orm import Session
from database import get_db
import schemas, models, utils, oauth2
import schemas
import models
import utils
import oauth2
from fastapi.security.oauth2 import OAuth2PasswordRequestForm


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


@router.post("/login", response_model=schemas.Token)
def login(user_credential: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
def login(user_credential: OAuth2PasswordRequestForm = Depends(),
db: Session = Depends(get_db)):
user = db.query(models.User).filter(models.User.email == user_credential.username).first()
if user is None:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Invalid credentials")
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN,
detail="Invalid credentials")
if not utils.varify_password(user_credential.password, user.password):
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Invalid credentials")
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN,
detail="Invalid credentials")
access_token = oauth2.create_access_token(data={'user_id': user.id})
return {'access_token': access_token, 'token_type': 'Bearer'}
Loading