From 786dc603e465953e6882e4b9d335dc04b43ffb82 Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Sat, 30 Nov 2024 16:21:32 +0530 Subject: [PATCH 01/55] Basic fast api --- .gitignore | 2 ++ app/__init__.py | 0 app/main.py | 82 +++++++++++++++++++++++++++++++++++++++++++++++++ requirement.txt | 35 +++++++++++++++++++++ 4 files changed, 119 insertions(+) create mode 100644 .gitignore create mode 100644 app/__init__.py create mode 100644 app/main.py create mode 100644 requirement.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4ea05a1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +venv/ +__pycache__/ \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..e36cd5a --- /dev/null +++ b/app/main.py @@ -0,0 +1,82 @@ +from typing import Optional +from fastapi import FastAPI, Response, status, HTTPException +from pydantic import BaseModel +from random import randrange +import psycopg2 +from psycopg2.extras import RealDictCursor +import time + +app = FastAPI() + +class Post(BaseModel): + tittle: str + content: str + published: bool = True +while True: + try: + conn = psycopg2.connect(host='localhost',database='fastapi', user='postgres', password='root', cursor_factory=RealDictCursor) + cursor = conn.cursor() + print("Connected to Database") + break + except Exception as er: + print("Connection Failed") + print("Error: ", er) + time.sleep(3) + +my_post = [{"tittle":"tittle of the post", "content":"content of the post", "id":1}, + {"tittle":"tittle of the post", "content":"content of the post", "id":2} +] + + + +@app.get('/') +def root(): + return {"HELLO":"First FASTAPI Project"}\ + + +@app.get('/posts') +def get_list_of_all_post(): + cursor.execute("SELECT * from posts") + posts = cursor.fetchall() + return {"data":posts} + + +@app.post('/posts', status_code=status.HTTP_201_CREATED) +def create_post(post: Post): + print(post.model_dump()) + cursor.execute("INSERT INTO posts(tittle, content, published) VALUES (%s, %s, %s) RETURNING *",(post.tittle, post.content, post.published)) + new_post = cursor.fetchone() + conn.commit() + return {"message": new_post} + #tittle str, content str + +@app.get('/posts/{id}') +def get_post(id: int, response: Response): + print() + cursor.execute("SELECT * from posts WHERE id = %s", (str(id))) + post = cursor.fetchone() + if not post: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail= f"post with {id} not found") + return {"Message":f"This is your post with {id}", "post":post} + + + +@app.delete('/posts/delete/{id}') +def delete_post(id: int): + cursor.execute("DELETE from posts where id = %s returning *", (str(id))) + deleted_post = cursor.fetchone() + conn.commit() + if not deleted_post: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,detail= f"post with {id} not found") + return {"message": f"post with {id} deleted", "post": deleted_post} + + +@app.put('/posts/update/{id}') +def update_post(id: int, post: Post): + cursor.execute("update posts set tittle = %s, content = %s, published = %s where id = %s returning *", (str(post.tittle), post.content, post.published,str(id))) + updated_post = cursor.fetchone() + conn.commit() + + if updated_post == None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"post with {id} does not exist") + return {"data": updated_post} \ No newline at end of file diff --git a/requirement.txt b/requirement.txt new file mode 100644 index 0000000..e69a986 --- /dev/null +++ b/requirement.txt @@ -0,0 +1,35 @@ +annotated-types==0.7.0 +anyio==4.5.2 +certifi==2024.8.30 +click==8.1.7 +colorama==0.4.6 +dnspython==2.6.1 +email-validator==2.2.0 +exceptiongroup==1.2.2 +fastapi==0.115.5 +fastapi-cli==0.0.5 +h11==0.14.0 +httpcore==1.0.7 +httptools==0.6.4 +httpx==0.28.0 +idna==3.10 +jinja2==3.1.4 +markdown-it-py==3.0.0 +MarkupSafe==2.1.5 +mdurl==0.1.2 +psycopg2==2.9.1 +pydantic==2.10.2 +pydantic-core==2.27.1 +pygments==2.18.0 +python-dotenv==1.0.1 +python-multipart==0.0.18 +PyYAML==6.0.2 +rich==13.9.4 +shellingham==1.5.4 +sniffio==1.3.1 +starlette==0.41.3 +typer==0.14.0 +typing-extensions==4.12.2 +uvicorn==0.32.1 +watchfiles==0.24.0 +websockets==13.1 From 7c224c20bdba2b4ad6a3be64077956faff6a6ff2 Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Tue, 17 Dec 2024 14:04:16 +0530 Subject: [PATCH 02/55] Upost routes are Created --- app/database.py | 5 ++ app/main.py | 184 ++++++++++++++++++++++++++++++------------------ app/model.py | 67 ++++++++++++++++++ database.db | Bin 0 -> 8192 bytes 4 files changed, 187 insertions(+), 69 deletions(-) create mode 100644 app/database.py create mode 100644 app/model.py create mode 100644 database.db diff --git a/app/database.py b/app/database.py new file mode 100644 index 0000000..bbd54a5 --- /dev/null +++ b/app/database.py @@ -0,0 +1,5 @@ +from sqlmodel import create_engine + +POSTGRESS_SQL_DATABASE_URL = "postgresql://postgres:root@localhost/fastapi" + +engine = create_engine(POSTGRESS_SQL_DATABASE_URL, echo= True) diff --git a/app/main.py b/app/main.py index e36cd5a..f3c72e8 100644 --- a/app/main.py +++ b/app/main.py @@ -2,81 +2,127 @@ from fastapi import FastAPI, Response, status, HTTPException from pydantic import BaseModel from random import randrange +from contextlib import asynccontextmanager import psycopg2 from psycopg2.extras import RealDictCursor -import time +from sqlmodel import SQLModel +from .model import * -app = FastAPI() +@asynccontextmanager +async def lifespan(app: FastAPI): + app.state.db = create_database_and_tables() -class Post(BaseModel): - tittle: str - content: str - published: bool = True -while True: - try: - conn = psycopg2.connect(host='localhost',database='fastapi', user='postgres', password='root', cursor_factory=RealDictCursor) - cursor = conn.cursor() - print("Connected to Database") - break - except Exception as er: - print("Connection Failed") - print("Error: ", er) - time.sleep(3) + yield -my_post = [{"tittle":"tittle of the post", "content":"content of the post", "id":1}, - {"tittle":"tittle of the post", "content":"content of the post", "id":2} -] + app.state.db = drop_database_and_tables() +app = FastAPI(lifespan=lifespan) - -@app.get('/') +@app.get("/") def root(): - return {"HELLO":"First FASTAPI Project"}\ - - -@app.get('/posts') -def get_list_of_all_post(): - cursor.execute("SELECT * from posts") - posts = cursor.fetchall() - return {"data":posts} - - -@app.post('/posts', status_code=status.HTTP_201_CREATED) -def create_post(post: Post): - print(post.model_dump()) - cursor.execute("INSERT INTO posts(tittle, content, published) VALUES (%s, %s, %s) RETURNING *",(post.tittle, post.content, post.published)) - new_post = cursor.fetchone() - conn.commit() - return {"message": new_post} - #tittle str, content str - -@app.get('/posts/{id}') -def get_post(id: int, response: Response): - print() - cursor.execute("SELECT * from posts WHERE id = %s", (str(id))) - post = cursor.fetchone() - if not post: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail= f"post with {id} not found") - return {"Message":f"This is your post with {id}", "post":post} - - - -@app.delete('/posts/delete/{id}') -def delete_post(id: int): - cursor.execute("DELETE from posts where id = %s returning *", (str(id))) - deleted_post = cursor.fetchone() - conn.commit() - if not deleted_post: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,detail= f"post with {id} not found") - return {"message": f"post with {id} deleted", "post": deleted_post} - - -@app.put('/posts/update/{id}') -def update_post(id: int, post: Post): - cursor.execute("update posts set tittle = %s, content = %s, published = %s where id = %s returning *", (str(post.tittle), post.content, post.published,str(id))) - updated_post = cursor.fetchone() - conn.commit() + return {"Message": "Welcome to FastAPI APP"} + + + +# class Post(BaseModel): +# tittle: str +# content: str +# published: bool = True + +# while True: +# try: +# conn = psycopg2.connect(host='localhost',database='fastapi', user='postgres', password='root', cursor_factory=RealDictCursor) +# cursor = conn.cursor() +# print("Connected to Database") +# break +# except Exception as er: +# print("Connection Failed") +# print("Error: ", er) +# time.sleep(3) + +# my_post = [{"tittle":"tittle of the post", "content":"content of the post", "id":1}, +# {"tittle":"tittle of the post", "content":"content of the post", "id":2} +# ] +# @app.get('/') +# def root(): +# return "Hello User" + +# @app.post('/schema') +# def create_schema(): +# create_database_and_table() +# return {"Tables Created"} + +# @app.get('/posts') +# def get_list_of_all_post(): +# cursor.execute("SELECT * from posts") +# posts = cursor.fetchall() +# return {"data":posts} + +# @app.post('/posts', status_code=status.HTTP_201_CREATED) +# def create_post(post: Post): +# print(post.model_dump()) +# cursor.execute("INSERT INTO posts(tittle, content, published) VALUES (%s, %s, %s) RETURNING *",(post.tittle, post.content, post.published)) +# new_post = cursor.fetchone() +# conn.commit() +# return {"message": new_post} + +# @app.get('/posts/{id}') +# def get_post(id: int, response: Response): +# print() +# cursor.execute("SELECT * from posts WHERE id = %s", (id,)) +# post = cursor.fetchone() +# if not post: +# raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail= f"post with {id} not found") +# return {"Message":f"This is your post with {id}", "post":post} + +# @app.delete('/posts/delete/{id}') +# def delete_post(id: int): +# cursor.execute("DELETE from posts where id = %s returning *", (str(id))) +# deleted_post = cursor.fetchone() +# conn.commit() +# if not deleted_post: +# raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,detail= f"post with {id} not found") +# return {"message": f"post with {id} deleted", "post": deleted_post} + +# @app.put('/posts/update/{id}') +# def update_post(id: int, post: Post): +# cursor.execute("update posts set tittle = %s, content = %s, published = %s where id = %s returning *", (str(post.tittle), post.content, post.published,str(id))) +# updated_post = cursor.fetchone() +# conn.commit() - if updated_post == None: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"post with {id} does not exist") - return {"data": updated_post} \ No newline at end of file +# if updated_post == None: +# raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"post with {id} does not exist") +# return {"data": updated_post} + +# @app.get('/uposts', status_code=status.HTTP_200_OK) +# def get_list_of_all_post(): +# posts = select_all_post() +# return {"ALL UPosts":posts} + +# @app.post('/uposts', status_code=status.HTTP_201_CREATED) +# def create_post(post: UPosts): +# post = insert_data_in_table(post) +# return {"Created UPost": post.model_dump()} + +# @app.get('/uposts/{id}', status_code=status.HTTP_200_OK) +# def get_post(id: int, response: Response): +# post = get_post_by_id(id) +# if not post: +# raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"post with {id} does not exist in UPosts") +# print(list(post)) +# return {f"Post with ID {id}": post} + +# @app.put('/uposts/update/{id}', status_code=status.HTTP_200_OK) +# def update_post(id: int, upost: UPosts, response: Response): +# post = update_post_by_id(id, upost) +# if not post: +# raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"post with {id} does not exist in UPosts") +# print(list(post)) +# return {"Updated Post": post} + +# @app.delete('/uposts/delete/{id}', status_code=status.HTTP_200_OK) +# def delete_post(id: int, response: Response): +# post = delete_post_by_id(id) +# if not post: +# raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"post with {id} does not exist in UPosts") +# return {"Deleted Post": post} diff --git a/app/model.py b/app/model.py new file mode 100644 index 0000000..4d7907f --- /dev/null +++ b/app/model.py @@ -0,0 +1,67 @@ +import datetime +from typing import Optional +from sqlmodel import Field, SQLModel, Session, select +from .database import engine + +class UPosts(SQLModel, table=True): + id: Optional[int] | None = Field(default= None, primary_key = True) + tittle: str + content: str + create_dtm: Optional[datetime.datetime] = Field(default_factory=datetime.datetime.now) + +class User(SQLModel, table=True): + userid: Optional[int] | None = Field(default=None, primary_key=True) + email: str + password: str + create_dtm: Optional[datetime.datetime] = Field(default_factory=datetime.datetime.now) + + +def create_database_and_tables(): + SQLModel.metadata.create_all(engine) + return "Database Connected" + +def drop_database_and_tables(): + SQLModel.metadata.drop_all(engine) + return "Database Disconnected" + + + +def insert_data_in_table(post: UPosts): + session = Session(engine) + session.add(post) + session.commit() + session.refresh(post) + session.close() + print("Data Inserted into the UPost Table") + return post + +def select_all_post(): + with Session(engine) as session: + posts = session.exec(select(UPosts)).all() + for post in posts: + print("post tittle", post.tittle, "post id", post.id) + return posts + +def get_post_by_id(id: int): + with Session(engine) as session: + post = session.get(UPosts, id) + return post + +def update_post_by_id(id: int, upost: UPosts): + with Session(engine) as session: + postTobeUpdated = session.exec(select(UPosts).where(UPosts.id == id)).first() + if postTobeUpdated: + upost.id = id + postTobeUpdated = session.merge(upost) + session.add(postTobeUpdated) + session.commit() + session.refresh(postTobeUpdated) + return postTobeUpdated + +def delete_post_by_id(id: int): + with Session(engine) as session: + upost = session.get(UPosts, id) + session.delete(upost) + session.commit() + return upost + diff --git a/database.db b/database.db new file mode 100644 index 0000000000000000000000000000000000000000..aa0e89a3c4cd71e7790af7ba88c39ca9ec8d84f0 GIT binary patch literal 8192 zcmeI#u?oU45C-5x1hF8xh=|LL7R1FDFqWWVts1M~BvqpptU~STpigcxbZ`)N`Hvjo za^Z$=yXC@yqR%?L*(`WUicnH02EHWwLT#rUA7~te zawruAvyg(7?{~;4U!z3RS}1QMJ{xzvPj$Ba+K)~;A86;y)p#n@h9+`D?fBq2uHDVe c(vN@u1Rwwb2tWV=5P$##AOHafK;VxB-fr?TM*si- literal 0 HcmV?d00001 From e153fded05a83fa4884597fdfbf00695f572e5ac Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Tue, 17 Dec 2024 20:59:12 +0530 Subject: [PATCH 03/55] modular code for UPosts --- app/crud/__init__.py | 0 app/crud/posts_crud.py | 37 ++++++++++++++++++++++++++++++++++++ app/crud/users_crud.py | 0 app/database.py | 22 +++++++++++++++++++-- app/main.py | 42 ++++++++++------------------------------- app/model.py | 8 -------- app/routers/__init__.py | 0 app/routers/posts.py | 37 ++++++++++++++++++++++++++++++++++++ app/routers/users.py | 7 +++++++ 9 files changed, 111 insertions(+), 42 deletions(-) create mode 100644 app/crud/__init__.py create mode 100644 app/crud/posts_crud.py create mode 100644 app/crud/users_crud.py create mode 100644 app/routers/__init__.py create mode 100644 app/routers/posts.py create mode 100644 app/routers/users.py diff --git a/app/crud/__init__.py b/app/crud/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/crud/posts_crud.py b/app/crud/posts_crud.py new file mode 100644 index 0000000..98cd84b --- /dev/null +++ b/app/crud/posts_crud.py @@ -0,0 +1,37 @@ +from app.database import get_session +from app.model import UPosts +from sqlmodel import select, Session + +session = next(get_session()) + +def insert_data_in_table(post: UPosts): + session.add(post) + session.commit() + session.refresh(post) + session.close() + return post + +def select_all_post(): + posts = session.exec(select(UPosts)).all() + return posts + +def get_post_by_id(id: int): + post = session.get(UPosts, id) + return post + +def update_post_by_id(id: int, upost: UPosts): + postTobeUpdated = session.exec(select(UPosts).where(UPosts.id == id)).first() + if postTobeUpdated: + upost.id = id + postTobeUpdated = session.merge(upost) + session.add(postTobeUpdated) + session.commit() + session.refresh(postTobeUpdated) + return postTobeUpdated + +def delete_post_by_id(id: int): + upost = session.get(UPosts, id) + session.delete(upost) + session.commit() + return upost + diff --git a/app/crud/users_crud.py b/app/crud/users_crud.py new file mode 100644 index 0000000..e69de29 diff --git a/app/database.py b/app/database.py index bbd54a5..620fdfe 100644 --- a/app/database.py +++ b/app/database.py @@ -1,5 +1,23 @@ -from sqlmodel import create_engine +from sqlmodel import create_engine, SQLModel, Session POSTGRESS_SQL_DATABASE_URL = "postgresql://postgres:root@localhost/fastapi" -engine = create_engine(POSTGRESS_SQL_DATABASE_URL, echo= True) +# add echo = true if you like to log create query +engine = create_engine(POSTGRESS_SQL_DATABASE_URL) + + +def create_database_and_tables(): + SQLModel.metadata.create_all(engine) + return "Database Connected" + +def drop_database_and_tables(): + SQLModel.metadata.drop_all(engine) + return "Database Disconnected" + +def get_session() -> Session: # type: ignore + with Session(engine) as session: + try: + yield session + finally: + session.close() + diff --git a/app/main.py b/app/main.py index f3c72e8..0ba9b24 100644 --- a/app/main.py +++ b/app/main.py @@ -4,20 +4,30 @@ from random import randrange from contextlib import asynccontextmanager import psycopg2 +from app.routers import posts, users from psycopg2.extras import RealDictCursor from sqlmodel import SQLModel + +from app.database import create_database_and_tables, drop_database_and_tables from .model import * @asynccontextmanager async def lifespan(app: FastAPI): app.state.db = create_database_and_tables() + print(app.state.db) yield app.state.db = drop_database_and_tables() + print(app.state.db) app = FastAPI(lifespan=lifespan) + +app.include_router(posts.router, prefix="/uposts", tags=["Posts"]) +app.include_router(users.router, prefix="/users", tags=["Users"]) + + @app.get("/") def root(): return {"Message": "Welcome to FastAPI APP"} @@ -94,35 +104,3 @@ def root(): # raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"post with {id} does not exist") # return {"data": updated_post} -# @app.get('/uposts', status_code=status.HTTP_200_OK) -# def get_list_of_all_post(): -# posts = select_all_post() -# return {"ALL UPosts":posts} - -# @app.post('/uposts', status_code=status.HTTP_201_CREATED) -# def create_post(post: UPosts): -# post = insert_data_in_table(post) -# return {"Created UPost": post.model_dump()} - -# @app.get('/uposts/{id}', status_code=status.HTTP_200_OK) -# def get_post(id: int, response: Response): -# post = get_post_by_id(id) -# if not post: -# raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"post with {id} does not exist in UPosts") -# print(list(post)) -# return {f"Post with ID {id}": post} - -# @app.put('/uposts/update/{id}', status_code=status.HTTP_200_OK) -# def update_post(id: int, upost: UPosts, response: Response): -# post = update_post_by_id(id, upost) -# if not post: -# raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"post with {id} does not exist in UPosts") -# print(list(post)) -# return {"Updated Post": post} - -# @app.delete('/uposts/delete/{id}', status_code=status.HTTP_200_OK) -# def delete_post(id: int, response: Response): -# post = delete_post_by_id(id) -# if not post: -# raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"post with {id} does not exist in UPosts") -# return {"Deleted Post": post} diff --git a/app/model.py b/app/model.py index 4d7907f..cf52635 100644 --- a/app/model.py +++ b/app/model.py @@ -16,14 +16,6 @@ class User(SQLModel, table=True): create_dtm: Optional[datetime.datetime] = Field(default_factory=datetime.datetime.now) -def create_database_and_tables(): - SQLModel.metadata.create_all(engine) - return "Database Connected" - -def drop_database_and_tables(): - SQLModel.metadata.drop_all(engine) - return "Database Disconnected" - def insert_data_in_table(post: UPosts): diff --git a/app/routers/__init__.py b/app/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/routers/posts.py b/app/routers/posts.py new file mode 100644 index 0000000..f8b0af2 --- /dev/null +++ b/app/routers/posts.py @@ -0,0 +1,37 @@ +from fastapi import APIRouter, HTTPException, status, Response +from app.crud.posts_crud import * + +router = APIRouter() + +@router.get('/', status_code=status.HTTP_200_OK) +def get_list_of_all_post(): + posts = select_all_post() + return {"ALL UPosts":posts} + +@router.post('/newupost', status_code=status.HTTP_201_CREATED) +def create_post(post: UPosts): + post = insert_data_in_table(post) + return {"Created UPost": post.model_dump()} + +@router.get('/{id}', status_code=status.HTTP_200_OK) +def get_post(id: int, response: Response): + post = get_post_by_id(id) + if not post: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"post with {id} does not exist in UPosts") + print(list(post)) + return {f"Post with ID {id}": post} + +@router.put('/update/{id}', status_code=status.HTTP_200_OK) +def update_post(id: int, upost: UPosts, response: Response): + post = update_post_by_id(id, upost) + if not post: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"post with {id} does not exist in UPosts") + print(list(post)) + return {"Updated Post": post} + +@router.delete('/delete/{id}', status_code=status.HTTP_200_OK) +def delete_post(id: int, response: Response): + post = delete_post_by_id(id) + if not post: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"post with {id} does not exist in UPosts") + return {"Deleted Post": post} diff --git a/app/routers/users.py b/app/routers/users.py new file mode 100644 index 0000000..18d7ede --- /dev/null +++ b/app/routers/users.py @@ -0,0 +1,7 @@ +from fastapi import APIRouter + +router = APIRouter() + +@router.get("/") +def get_users(): + return {"All User fetched"} From 1caf3216b3615be4ccdb75f265d0dfbfc786d847 Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod <46452996+DMRathod@users.noreply.github.com> Date: Tue, 17 Dec 2024 21:01:33 +0530 Subject: [PATCH 04/55] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 1281f93..dc4329a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,5 @@ # python_fastapi + +Building the linkedIn Clone with FastAPI from scratch with pipeline of gitlab + Demonstrating Fastapi Usage From 97596c0190d9d6ed91f1563e0da40eef41f5e5d9 Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Thu, 19 Dec 2024 13:45:59 +0530 Subject: [PATCH 05/55] Users Endpoints added with exception --- app/crud/posts_crud.py | 11 ++++++----- app/crud/users_crud.py | 45 ++++++++++++++++++++++++++++++++++++++++++ app/database.py | 6 +++++- app/main.py | 13 ++++-------- app/model.py | 3 +-- app/routers/posts.py | 4 ++-- app/routers/users.py | 39 +++++++++++++++++++++++++++++++++--- 7 files changed, 99 insertions(+), 22 deletions(-) diff --git a/app/crud/posts_crud.py b/app/crud/posts_crud.py index 98cd84b..1e1011b 100644 --- a/app/crud/posts_crud.py +++ b/app/crud/posts_crud.py @@ -1,17 +1,17 @@ from app.database import get_session from app.model import UPosts -from sqlmodel import select, Session +from sqlmodel import select session = next(get_session()) -def insert_data_in_table(post: UPosts): +def insert_data_in_uposts_table(post: UPosts): session.add(post) session.commit() session.refresh(post) session.close() return post -def select_all_post(): +def get_all_post(): posts = session.exec(select(UPosts)).all() return posts @@ -31,7 +31,8 @@ def update_post_by_id(id: int, upost: UPosts): def delete_post_by_id(id: int): upost = session.get(UPosts, id) - session.delete(upost) - session.commit() + if upost: + session.delete(upost) + session.commit() return upost diff --git a/app/crud/users_crud.py b/app/crud/users_crud.py index e69de29..a793180 100644 --- a/app/crud/users_crud.py +++ b/app/crud/users_crud.py @@ -0,0 +1,45 @@ +from fastapi import HTTPException, status +from app.database import get_session +from app.model import Users +from sqlmodel import select + +session = next(get_session()) + +def insert_data_in_users_table(user: Users): + try: + session.add(user) + session.commit() + except Exception as e: + session.rollback() + raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE,detail=f"User NOT Created, Please Check Request {str(e.args)}") + session.refresh(user) + return user + +def get_all_user(): + posts = session.exec(select(Users)).all() + return posts + +def get_post_by_id(id: int): + user = session.get(Users, id) + if user: + return user + else: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"User with {id} not found") + +def update_user_by_id(id: int, user: Users): + usertobeupdated = session.exec(select(Users).where(Users.userid == id)).first() + if usertobeupdated: + user.userid = id + usertobeupdated = session.merge(user) + session.add(usertobeupdated) + session.commit() + session.refresh(usertobeupdated) + return usertobeupdated + +def delete_user_by_id(id: int): + user = session.get(Users, id) + if user: + session.delete(user) + session.commit() + return user + diff --git a/app/database.py b/app/database.py index 620fdfe..875f9dc 100644 --- a/app/database.py +++ b/app/database.py @@ -10,9 +10,13 @@ def create_database_and_tables(): SQLModel.metadata.create_all(engine) return "Database Connected" +def close_connection(): + engine.dispose() + return "Database Disconnected" + def drop_database_and_tables(): SQLModel.metadata.drop_all(engine) - return "Database Disconnected" + return "Database Droped" def get_session() -> Session: # type: ignore with Session(engine) as session: diff --git a/app/main.py b/app/main.py index 0ba9b24..d5afb23 100644 --- a/app/main.py +++ b/app/main.py @@ -1,14 +1,9 @@ -from typing import Optional -from fastapi import FastAPI, Response, status, HTTPException -from pydantic import BaseModel -from random import randrange +from fastapi import FastAPI from contextlib import asynccontextmanager -import psycopg2 from app.routers import posts, users -from psycopg2.extras import RealDictCursor -from sqlmodel import SQLModel -from app.database import create_database_and_tables, drop_database_and_tables + +from app.database import create_database_and_tables, close_connection from .model import * @asynccontextmanager @@ -18,7 +13,7 @@ async def lifespan(app: FastAPI): yield - app.state.db = drop_database_and_tables() + app.state.db = close_connection() print(app.state.db) app = FastAPI(lifespan=lifespan) diff --git a/app/model.py b/app/model.py index cf52635..8902793 100644 --- a/app/model.py +++ b/app/model.py @@ -9,7 +9,7 @@ class UPosts(SQLModel, table=True): content: str create_dtm: Optional[datetime.datetime] = Field(default_factory=datetime.datetime.now) -class User(SQLModel, table=True): +class Users(SQLModel, table=True): userid: Optional[int] | None = Field(default=None, primary_key=True) email: str password: str @@ -17,7 +17,6 @@ class User(SQLModel, table=True): - def insert_data_in_table(post: UPosts): session = Session(engine) session.add(post) diff --git a/app/routers/posts.py b/app/routers/posts.py index f8b0af2..cbc7634 100644 --- a/app/routers/posts.py +++ b/app/routers/posts.py @@ -5,12 +5,12 @@ @router.get('/', status_code=status.HTTP_200_OK) def get_list_of_all_post(): - posts = select_all_post() + posts = get_all_post() return {"ALL UPosts":posts} @router.post('/newupost', status_code=status.HTTP_201_CREATED) def create_post(post: UPosts): - post = insert_data_in_table(post) + post = insert_data_in_uposts_table(post) return {"Created UPost": post.model_dump()} @router.get('/{id}', status_code=status.HTTP_200_OK) diff --git a/app/routers/users.py b/app/routers/users.py index 18d7ede..991ce6b 100644 --- a/app/routers/users.py +++ b/app/routers/users.py @@ -1,7 +1,40 @@ -from fastapi import APIRouter +from fastapi import APIRouter, status, HTTPException, Response +from app.crud.users_crud import * router = APIRouter() -@router.get("/") +@router.get("/", status_code=status.HTTP_201_CREATED) def get_users(): - return {"All User fetched"} + users = get_all_user() + return {"Users": users} + +@router.post('/adduser', status_code=status.HTTP_201_CREATED) +def create_user(user: Users): + try: + user = insert_data_in_users_table(user) + return {"Created User": user.model_dump()} + except Exception as e: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,detail=f"User NOT Created, Please Check Request {str(e.args)}") + +@router.get('/{id}', status_code=status.HTTP_200_OK) +def get_user(id: int, response: Response): + user = get_post_by_id(id) + if not user: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"user with {id} does not exist in Users") + return {f"User with ID {id}": user} + +@router.put('/update/{id}', status_code=status.HTTP_200_OK) +def update_post(id: int, user: Users, response: Response): + user = update_user_by_id(id, user) + if not user: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"user with {id} does not exist in Users") + return {"Updated User": user} + + +@router.delete('/delete/{id}', status_code=status.HTTP_200_OK) +def delete_post(id: int, response: Response): + user = delete_user_by_id(id) + if not user: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"user with {id} does not exist in Users") + return {"Deleted user": user} + From 741da9f4871c5900808259a63ba42c4dbb0abd21 Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Thu, 19 Dec 2024 13:53:55 +0530 Subject: [PATCH 06/55] updated model.py --- app/model.py | 44 +------------------------------------------- app/routers/posts.py | 2 +- 2 files changed, 2 insertions(+), 44 deletions(-) diff --git a/app/model.py b/app/model.py index 8902793..9a3260a 100644 --- a/app/model.py +++ b/app/model.py @@ -13,46 +13,4 @@ class Users(SQLModel, table=True): userid: Optional[int] | None = Field(default=None, primary_key=True) email: str password: str - create_dtm: Optional[datetime.datetime] = Field(default_factory=datetime.datetime.now) - - - -def insert_data_in_table(post: UPosts): - session = Session(engine) - session.add(post) - session.commit() - session.refresh(post) - session.close() - print("Data Inserted into the UPost Table") - return post - -def select_all_post(): - with Session(engine) as session: - posts = session.exec(select(UPosts)).all() - for post in posts: - print("post tittle", post.tittle, "post id", post.id) - return posts - -def get_post_by_id(id: int): - with Session(engine) as session: - post = session.get(UPosts, id) - return post - -def update_post_by_id(id: int, upost: UPosts): - with Session(engine) as session: - postTobeUpdated = session.exec(select(UPosts).where(UPosts.id == id)).first() - if postTobeUpdated: - upost.id = id - postTobeUpdated = session.merge(upost) - session.add(postTobeUpdated) - session.commit() - session.refresh(postTobeUpdated) - return postTobeUpdated - -def delete_post_by_id(id: int): - with Session(engine) as session: - upost = session.get(UPosts, id) - session.delete(upost) - session.commit() - return upost - + create_dtm: Optional[datetime.datetime] = Field(default_factory=datetime.datetime.now) \ No newline at end of file diff --git a/app/routers/posts.py b/app/routers/posts.py index cbc7634..a2b9df1 100644 --- a/app/routers/posts.py +++ b/app/routers/posts.py @@ -6,7 +6,7 @@ @router.get('/', status_code=status.HTTP_200_OK) def get_list_of_all_post(): posts = get_all_post() - return {"ALL UPosts":posts} + return {"UPosts":posts} @router.post('/newupost', status_code=status.HTTP_201_CREATED) def create_post(post: UPosts): From 8620568bbe3bccce38cc48726c6a3f4b71a45f2d Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Thu, 19 Dec 2024 17:55:19 +0530 Subject: [PATCH 07/55] added validators for password and emailID --- app/crud/users_crud.py | 5 +++++ app/main.py | 5 +++-- app/model.py | 24 ++++++++++++++++++++++-- app/routers/users.py | 1 - 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/app/crud/users_crud.py b/app/crud/users_crud.py index a793180..5afcbab 100644 --- a/app/crud/users_crud.py +++ b/app/crud/users_crud.py @@ -1,12 +1,17 @@ from fastapi import HTTPException, status +from pydantic import ValidationError from app.database import get_session from app.model import Users from sqlmodel import select session = next(get_session()) + def insert_data_in_users_table(user: Users): try: + user_dict = {**user.model_dump()} + Users.validate_email_domain(user.email) + user.password = Users.password_hash(user.password) session.add(user) session.commit() except Exception as e: diff --git a/app/main.py b/app/main.py index d5afb23..ac7889a 100644 --- a/app/main.py +++ b/app/main.py @@ -3,7 +3,7 @@ from app.routers import posts, users -from app.database import create_database_and_tables, close_connection +from app.database import create_database_and_tables, close_connection, drop_database_and_tables from .model import * @asynccontextmanager @@ -13,7 +13,8 @@ async def lifespan(app: FastAPI): yield - app.state.db = close_connection() + # app.state.db = close_connection() + app.state.db = drop_database_and_tables() print(app.state.db) app = FastAPI(lifespan=lifespan) diff --git a/app/model.py b/app/model.py index 9a3260a..0a3ca04 100644 --- a/app/model.py +++ b/app/model.py @@ -1,4 +1,6 @@ import datetime +import bcrypt +from pydantic import EmailStr, field_validator from typing import Optional from sqlmodel import Field, SQLModel, Session, select from .database import engine @@ -11,6 +13,24 @@ class UPosts(SQLModel, table=True): class Users(SQLModel, table=True): userid: Optional[int] | None = Field(default=None, primary_key=True) - email: str + email: EmailStr = Field(sa_column_kwargs={"unique": True}) password: str - create_dtm: Optional[datetime.datetime] = Field(default_factory=datetime.datetime.now) \ No newline at end of file + create_dtm: Optional[datetime.datetime] = Field(default_factory=datetime.datetime.now) + + @field_validator("email", mode="before") + def validate_email_domain(cls, value): + allowed_domains = ["gmail.com", "yahoo.com"] + domain = value.split('@')[-1] + if domain not in allowed_domains: + raise ValueError(f"Invalid email domain: {domain}. Allowed domains: {', '.join(allowed_domains)}") + return value + + @field_validator("password", mode = "before") + def password_hash(cls, password): + if password: + hashedpass = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()) + return hashedpass.decode('utf-8') + return password + + def verify_password(self, password: str)->bool: + return bcrypt.checkpw(password.encode('utf-8'), self.password.encode('utf-8')) \ No newline at end of file diff --git a/app/routers/users.py b/app/routers/users.py index 991ce6b..50c9499 100644 --- a/app/routers/users.py +++ b/app/routers/users.py @@ -37,4 +37,3 @@ def delete_post(id: int, response: Response): if not user: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"user with {id} does not exist in Users") return {"Deleted user": user} - From f0f11735f76fb70e9236b6f7770a16b7b546a64c Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Tue, 24 Dec 2024 23:23:22 +0530 Subject: [PATCH 08/55] udpate the code with the response model for API --- app/crud/users_crud.py | 10 +++++----- app/main.py | 2 +- app/model.py | 14 +++++++++++++- app/routers/posts.py | 24 +++++++++++++----------- app/routers/users.py | 27 ++++++++++++++------------- 5 files changed, 46 insertions(+), 31 deletions(-) diff --git a/app/crud/users_crud.py b/app/crud/users_crud.py index 5afcbab..2b9e9b2 100644 --- a/app/crud/users_crud.py +++ b/app/crud/users_crud.py @@ -1,13 +1,13 @@ from fastapi import HTTPException, status from pydantic import ValidationError from app.database import get_session -from app.model import Users +from app.model import Users from sqlmodel import select session = next(get_session()) -def insert_data_in_users_table(user: Users): +def insert_data_in_users_table(user: Users)->Users: try: user_dict = {**user.model_dump()} Users.validate_email_domain(user.email) @@ -20,9 +20,9 @@ def insert_data_in_users_table(user: Users): session.refresh(user) return user -def get_all_user(): - posts = session.exec(select(Users)).all() - return posts +def get_all_user()->Users: + users = session.exec(select(Users)).all() + return users def get_post_by_id(id: int): user = session.get(Users, id) diff --git a/app/main.py b/app/main.py index ac7889a..e1b3f96 100644 --- a/app/main.py +++ b/app/main.py @@ -3,7 +3,7 @@ from app.routers import posts, users -from app.database import create_database_and_tables, close_connection, drop_database_and_tables +from app.database import create_database_and_tables, drop_database_and_tables from .model import * @asynccontextmanager diff --git a/app/model.py b/app/model.py index 0a3ca04..d3cb295 100644 --- a/app/model.py +++ b/app/model.py @@ -33,4 +33,16 @@ def password_hash(cls, password): return password def verify_password(self, password: str)->bool: - return bcrypt.checkpw(password.encode('utf-8'), self.password.encode('utf-8')) \ No newline at end of file + return bcrypt.checkpw(password.encode('utf-8'), self.password.encode('utf-8')) + +class UPostOut(SQLModel): + id: int + tittle: str + content: str + create_dtm: datetime.datetime + +class UserOut(SQLModel): + userid: int + email: EmailStr + create_dtm: datetime.datetime + diff --git a/app/routers/posts.py b/app/routers/posts.py index a2b9df1..ccdf777 100644 --- a/app/routers/posts.py +++ b/app/routers/posts.py @@ -1,37 +1,39 @@ +from typing import List from fastapi import APIRouter, HTTPException, status, Response from app.crud.posts_crud import * +from app.model import UPostOut router = APIRouter() -@router.get('/', status_code=status.HTTP_200_OK) -def get_list_of_all_post(): +@router.get('/', status_code=status.HTTP_200_OK, response_model=List[UPostOut]) +def get_list_of_all_post()->List[UPostOut]: posts = get_all_post() - return {"UPosts":posts} + return posts -@router.post('/newupost', status_code=status.HTTP_201_CREATED) +@router.post('/newupost', status_code=status.HTTP_201_CREATED, response_model=UPostOut) def create_post(post: UPosts): post = insert_data_in_uposts_table(post) - return {"Created UPost": post.model_dump()} + return post -@router.get('/{id}', status_code=status.HTTP_200_OK) +@router.get('/{id}', status_code=status.HTTP_200_OK, response_model=UPostOut) def get_post(id: int, response: Response): post = get_post_by_id(id) if not post: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"post with {id} does not exist in UPosts") print(list(post)) - return {f"Post with ID {id}": post} + return post -@router.put('/update/{id}', status_code=status.HTTP_200_OK) +@router.put('/update/{id}', status_code=status.HTTP_200_OK, response_model=UPostOut) def update_post(id: int, upost: UPosts, response: Response): post = update_post_by_id(id, upost) if not post: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"post with {id} does not exist in UPosts") print(list(post)) - return {"Updated Post": post} + return post -@router.delete('/delete/{id}', status_code=status.HTTP_200_OK) +@router.delete('/delete/{id}', status_code=status.HTTP_200_OK, response_model=UPostOut) def delete_post(id: int, response: Response): post = delete_post_by_id(id) if not post: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"post with {id} does not exist in UPosts") - return {"Deleted Post": post} + return post diff --git a/app/routers/users.py b/app/routers/users.py index 50c9499..6853f28 100644 --- a/app/routers/users.py +++ b/app/routers/users.py @@ -1,39 +1,40 @@ +from typing import List from fastapi import APIRouter, status, HTTPException, Response from app.crud.users_crud import * - +from app.model import UserOut router = APIRouter() -@router.get("/", status_code=status.HTTP_201_CREATED) -def get_users(): +@router.get("/", status_code=status.HTTP_201_CREATED, response_model=List[UserOut]) +def get_users()->List[UserOut]: users = get_all_user() - return {"Users": users} + return users -@router.post('/adduser', status_code=status.HTTP_201_CREATED) -def create_user(user: Users): +@router.post('/adduser', status_code=status.HTTP_201_CREATED, response_model=UserOut) +def create_user(user: Users)->UserOut: try: user = insert_data_in_users_table(user) - return {"Created User": user.model_dump()} + return user except Exception as e: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,detail=f"User NOT Created, Please Check Request {str(e.args)}") -@router.get('/{id}', status_code=status.HTTP_200_OK) +@router.get('/{id}', status_code=status.HTTP_200_OK, response_model=UserOut) def get_user(id: int, response: Response): user = get_post_by_id(id) if not user: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"user with {id} does not exist in Users") - return {f"User with ID {id}": user} + return user -@router.put('/update/{id}', status_code=status.HTTP_200_OK) +@router.put('/update/{id}', status_code=status.HTTP_200_OK, response_model=UserOut) def update_post(id: int, user: Users, response: Response): user = update_user_by_id(id, user) if not user: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"user with {id} does not exist in Users") - return {"Updated User": user} + return user -@router.delete('/delete/{id}', status_code=status.HTTP_200_OK) +@router.delete('/delete/{id}', status_code=status.HTTP_200_OK, response_model=UserOut) def delete_post(id: int, response: Response): user = delete_user_by_id(id) if not user: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"user with {id} does not exist in Users") - return {"Deleted user": user} + return user From 69848063592b130a237f69caf47a5a991dfc85a7 Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Thu, 26 Dec 2024 00:48:58 +0530 Subject: [PATCH 09/55] updated code with Oauth2 toekn secure end points --- app/crud/users_crud.py | 9 ++++++++- app/main.py | 3 ++- app/model.py | 10 ++++++++++ app/oauth2.py | 44 ++++++++++++++++++++++++++++++++++++++++++ app/routers/auth.py | 20 +++++++++++++++++++ app/routers/posts.py | 25 ++++++++++++------------ app/routers/users.py | 30 +++++++++++++++++----------- app/util.py | 9 +++++++++ 8 files changed, 125 insertions(+), 25 deletions(-) create mode 100644 app/oauth2.py create mode 100644 app/routers/auth.py create mode 100644 app/util.py diff --git a/app/crud/users_crud.py b/app/crud/users_crud.py index 2b9e9b2..c682c02 100644 --- a/app/crud/users_crud.py +++ b/app/crud/users_crud.py @@ -24,13 +24,20 @@ def get_all_user()->Users: users = session.exec(select(Users)).all() return users -def get_post_by_id(id: int): +def get_user_by_id(id: int): user = session.get(Users, id) if user: return user else: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"User with {id} not found") +def get_user_by_email(email: str): + user = session.exec(select(Users).where(Users.email == email)).first() + if user: + return user + else: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"User Not found in the System") + def update_user_by_id(id: int, user: Users): usertobeupdated = session.exec(select(Users).where(Users.userid == id)).first() if usertobeupdated: diff --git a/app/main.py b/app/main.py index e1b3f96..1acd156 100644 --- a/app/main.py +++ b/app/main.py @@ -1,6 +1,6 @@ from fastapi import FastAPI from contextlib import asynccontextmanager -from app.routers import posts, users +from app.routers import posts, users, auth from app.database import create_database_and_tables, drop_database_and_tables @@ -22,6 +22,7 @@ async def lifespan(app: FastAPI): app.include_router(posts.router, prefix="/uposts", tags=["Posts"]) app.include_router(users.router, prefix="/users", tags=["Users"]) +app.include_router(auth.router, prefix="/auth", tags=["Authentication"]) @app.get("/") diff --git a/app/model.py b/app/model.py index d3cb295..6d8f753 100644 --- a/app/model.py +++ b/app/model.py @@ -46,3 +46,13 @@ class UserOut(SQLModel): email: EmailStr create_dtm: datetime.datetime +class UserLogin(SQLModel): + email: EmailStr + password: str + +class Token(SQLModel): + access_token: str + token_type: str + +class TokenData(SQLModel): + id: Optional[int] = None diff --git a/app/oauth2.py b/app/oauth2.py new file mode 100644 index 0000000..6cc2b6a --- /dev/null +++ b/app/oauth2.py @@ -0,0 +1,44 @@ +import jwt +from datetime import datetime, timedelta +from app.model import TokenData +from fastapi import status, Depends, HTTPException +from fastapi.security import OAuth2PasswordBearer + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl='login') + + +SECRET_KEY = "" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + + +def create_jwt_token(data: dict): + encoded_data = data.copy() + expire = datetime.now() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + encoded_data.update({"exp": expire}) + token =jwt.encode(encoded_data, SECRET_KEY, ALGORITHM) + return token + +def verify_jwt_token(token: str, credentials_exception): + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + id : str = payload.get("userid") + if id is None: + raise credentials_exception + token_data = TokenData(id=id) + + except jwt.InvalidTokenError: + raise credentials_exception + + except jwt.ExpiredSignatureError: + raise credentials_exception + return token_data + +def get_current_user(token: str = Depends(oauth2_scheme)): + credentials_exception = HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=f"Could not validate Credentials", headers={"WWW-Authenticate":"Bearer"}) + return verify_jwt_token(token, credentials_exception) + + + + + diff --git a/app/routers/auth.py b/app/routers/auth.py new file mode 100644 index 0000000..1494759 --- /dev/null +++ b/app/routers/auth.py @@ -0,0 +1,20 @@ +from fastapi import APIRouter, HTTPException, status, Depends +from fastapi.security.oauth2 import OAuth2PasswordRequestForm +from app.model import UserLogin, Token +from app.crud.users_crud import get_user_by_email +from app.util import * +from app.oauth2 import create_jwt_token + +router = APIRouter() + +@router.post("/login", status_code=status.HTTP_200_OK, response_model=Token) +def login(user_credentials: OAuth2PasswordRequestForm = Depends()): + user = get_user_by_email(user_credentials.username) + if not user: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN,detail=f"User Not Found") + access_token = create_jwt_token(data = {"userid": user.userid}) + if not verify(user_credentials.password, user.password): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=f"Invalid Password or Email Id") + return {"access_token": access_token, "token_type": "bearer"} + + diff --git a/app/routers/posts.py b/app/routers/posts.py index ccdf777..419595e 100644 --- a/app/routers/posts.py +++ b/app/routers/posts.py @@ -1,30 +1,31 @@ from typing import List -from fastapi import APIRouter, HTTPException, status, Response +from fastapi import APIRouter, HTTPException, status, Response, Depends from app.crud.posts_crud import * from app.model import UPostOut +from app.oauth2 import get_current_user router = APIRouter() -@router.get('/', status_code=status.HTTP_200_OK, response_model=List[UPostOut]) -def get_list_of_all_post()->List[UPostOut]: +@router.get('/', status_code=status.HTTP_200_OK, response_model=List[UPostOut],) +def get_list_of_post(userid: int = Depends(get_current_user))->List[UPostOut]: posts = get_all_post() return posts -@router.post('/newupost', status_code=status.HTTP_201_CREATED, response_model=UPostOut) -def create_post(post: UPosts): - post = insert_data_in_uposts_table(post) - return post - -@router.get('/{id}', status_code=status.HTTP_200_OK, response_model=UPostOut) -def get_post(id: int, response: Response): +@router.get('/get/{id}', status_code=status.HTTP_200_OK, response_model=UPostOut) +def get_post(id: int, response: Response, userid: int = Depends(get_current_user)): post = get_post_by_id(id) if not post: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"post with {id} does not exist in UPosts") print(list(post)) return post +@router.post('/add', status_code=status.HTTP_201_CREATED, response_model=UPostOut) +def create_post(post: UPosts, userid: int = Depends(get_current_user)): + post = insert_data_in_uposts_table(post) + return post + @router.put('/update/{id}', status_code=status.HTTP_200_OK, response_model=UPostOut) -def update_post(id: int, upost: UPosts, response: Response): +def update_post(id: int, upost: UPosts, response: Response, userid: int = Depends(get_current_user)): post = update_post_by_id(id, upost) if not post: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"post with {id} does not exist in UPosts") @@ -32,7 +33,7 @@ def update_post(id: int, upost: UPosts, response: Response): return post @router.delete('/delete/{id}', status_code=status.HTTP_200_OK, response_model=UPostOut) -def delete_post(id: int, response: Response): +def delete_post(id: int, response: Response, userid: int = Depends(get_current_user)): post = delete_post_by_id(id) if not post: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"post with {id} does not exist in UPosts") diff --git a/app/routers/users.py b/app/routers/users.py index 6853f28..828a684 100644 --- a/app/routers/users.py +++ b/app/routers/users.py @@ -5,11 +5,26 @@ router = APIRouter() @router.get("/", status_code=status.HTTP_201_CREATED, response_model=List[UserOut]) -def get_users()->List[UserOut]: +def get_list_of_users()->List[UserOut]: users = get_all_user() return users -@router.post('/adduser', status_code=status.HTTP_201_CREATED, response_model=UserOut) +@router.get('/get/by-id/{id}', status_code=status.HTTP_200_OK, response_model=UserOut) +def get_user_by_id(id: int, response: Response): + user = get_user_by_id(id) + if not user: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"user with {id} does not exist in Users") + return user + +@router.get('/get/by-email/{email}', status_code=status.HTTP_200_OK, response_model=Users) +def get_user_by_email(email: str, response: Response): + user = get_user_by_email(email) + if not user: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"user with {email} does not exist in Users") + return user + + +@router.post('/add', status_code=status.HTTP_201_CREATED, response_model=UserOut) def create_user(user: Users)->UserOut: try: user = insert_data_in_users_table(user) @@ -17,15 +32,8 @@ def create_user(user: Users)->UserOut: except Exception as e: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,detail=f"User NOT Created, Please Check Request {str(e.args)}") -@router.get('/{id}', status_code=status.HTTP_200_OK, response_model=UserOut) -def get_user(id: int, response: Response): - user = get_post_by_id(id) - if not user: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"user with {id} does not exist in Users") - return user - @router.put('/update/{id}', status_code=status.HTTP_200_OK, response_model=UserOut) -def update_post(id: int, user: Users, response: Response): +def update_user(id: int, user: Users, response: Response): user = update_user_by_id(id, user) if not user: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"user with {id} does not exist in Users") @@ -33,7 +41,7 @@ def update_post(id: int, user: Users, response: Response): @router.delete('/delete/{id}', status_code=status.HTTP_200_OK, response_model=UserOut) -def delete_post(id: int, response: Response): +def delete_user(id: int, response: Response): user = delete_user_by_id(id) if not user: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"user with {id} does not exist in Users") diff --git a/app/util.py b/app/util.py new file mode 100644 index 0000000..228cfa4 --- /dev/null +++ b/app/util.py @@ -0,0 +1,9 @@ +from passlib.context import CryptContext +pass_context = CryptContext(schemes=["bcrypt"], deprecated = "auto") + + +def hash(password: str): + return pass_context.hash(password) + +def verify(row_password, hashed_password): + return pass_context.verify(row_password, hashed_password) \ No newline at end of file From 4c1b6e132c19dc920e1085a29f6245c1e0e52b9a Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Sat, 28 Dec 2024 21:28:30 +0530 Subject: [PATCH 10/55] token expiration working --- .gitignore | 3 ++- app/model.py | 2 +- app/oauth2.py | 18 ++++++++++-------- app/routers/auth.py | 2 +- app/routers/posts.py | 10 ++++++---- app/routers/users.py | 1 + 6 files changed, 21 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index 4ea05a1..c5bd6df 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ venv/ -__pycache__/ \ No newline at end of file +__pycache__/ +.env \ No newline at end of file diff --git a/app/model.py b/app/model.py index 6d8f753..702aa00 100644 --- a/app/model.py +++ b/app/model.py @@ -55,4 +55,4 @@ class Token(SQLModel): token_type: str class TokenData(SQLModel): - id: Optional[int] = None + email: Optional[EmailStr] = None diff --git a/app/oauth2.py b/app/oauth2.py index 6cc2b6a..3a52608 100644 --- a/app/oauth2.py +++ b/app/oauth2.py @@ -1,31 +1,32 @@ import jwt -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from app.model import TokenData from fastapi import status, Depends, HTTPException from fastapi.security import OAuth2PasswordBearer +from app.crud.users_crud import * oauth2_scheme = OAuth2PasswordBearer(tokenUrl='login') -SECRET_KEY = "" +SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" ALGORITHM = "HS256" -ACCESS_TOKEN_EXPIRE_MINUTES = 30 +ACCESS_TOKEN_EXPIRE_MINUTES = 1 def create_jwt_token(data: dict): encoded_data = data.copy() - expire = datetime.now() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) - encoded_data.update({"exp": expire}) + expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + encoded_data.update({"exp": expire.timestamp()}) token =jwt.encode(encoded_data, SECRET_KEY, ALGORITHM) return token def verify_jwt_token(token: str, credentials_exception): try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) - id : str = payload.get("userid") - if id is None: + email : str = payload.get("email") + if email is None: raise credentials_exception - token_data = TokenData(id=id) + token_data = TokenData(email=email) except jwt.InvalidTokenError: raise credentials_exception @@ -36,6 +37,7 @@ def verify_jwt_token(token: str, credentials_exception): def get_current_user(token: str = Depends(oauth2_scheme)): credentials_exception = HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=f"Could not validate Credentials", headers={"WWW-Authenticate":"Bearer"}) + token_data = verify_jwt_token(token, credentials_exception) return verify_jwt_token(token, credentials_exception) diff --git a/app/routers/auth.py b/app/routers/auth.py index 1494759..172944c 100644 --- a/app/routers/auth.py +++ b/app/routers/auth.py @@ -12,7 +12,7 @@ def login(user_credentials: OAuth2PasswordRequestForm = Depends()): user = get_user_by_email(user_credentials.username) if not user: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN,detail=f"User Not Found") - access_token = create_jwt_token(data = {"userid": user.userid}) + access_token = create_jwt_token(data = {"email": user.email}) if not verify(user_credentials.password, user.password): raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=f"Invalid Password or Email Id") return {"access_token": access_token, "token_type": "bearer"} diff --git a/app/routers/posts.py b/app/routers/posts.py index 419595e..4431cb2 100644 --- a/app/routers/posts.py +++ b/app/routers/posts.py @@ -12,7 +12,8 @@ def get_list_of_post(userid: int = Depends(get_current_user))->List[UPostOut]: return posts @router.get('/get/{id}', status_code=status.HTTP_200_OK, response_model=UPostOut) -def get_post(id: int, response: Response, userid: int = Depends(get_current_user)): +def get_post(id: int, response: Response, user_email: str = Depends(get_current_user)): + print("user email", user_email) post = get_post_by_id(id) if not post: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"post with {id} does not exist in UPosts") @@ -20,12 +21,13 @@ def get_post(id: int, response: Response, userid: int = Depends(get_current_user return post @router.post('/add', status_code=status.HTTP_201_CREATED, response_model=UPostOut) -def create_post(post: UPosts, userid: int = Depends(get_current_user)): +def create_post(post: UPosts, user_email: str = Depends(get_current_user)): + print(user_email) post = insert_data_in_uposts_table(post) return post @router.put('/update/{id}', status_code=status.HTTP_200_OK, response_model=UPostOut) -def update_post(id: int, upost: UPosts, response: Response, userid: int = Depends(get_current_user)): +def update_post(id: int, upost: UPosts, response: Response, user_email: str = Depends(get_current_user)): post = update_post_by_id(id, upost) if not post: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"post with {id} does not exist in UPosts") @@ -33,7 +35,7 @@ def update_post(id: int, upost: UPosts, response: Response, userid: int = Depend return post @router.delete('/delete/{id}', status_code=status.HTTP_200_OK, response_model=UPostOut) -def delete_post(id: int, response: Response, userid: int = Depends(get_current_user)): +def delete_post(id: int, response: Response, user_email: str = Depends(get_current_user)): post = delete_post_by_id(id) if not post: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"post with {id} does not exist in UPosts") diff --git a/app/routers/users.py b/app/routers/users.py index 828a684..523da16 100644 --- a/app/routers/users.py +++ b/app/routers/users.py @@ -2,6 +2,7 @@ from fastapi import APIRouter, status, HTTPException, Response from app.crud.users_crud import * from app.model import UserOut + router = APIRouter() @router.get("/", status_code=status.HTTP_201_CREATED, response_model=List[UserOut]) From c3ec402f438e2939d90390858200a384e96795c8 Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Wed, 1 Jan 2025 22:50:01 +0530 Subject: [PATCH 11/55] env variables are set: --- app/config.py | 19 +++++++++++++++++++ app/crud/posts_crud.py | 5 ++--- app/database.py | 3 ++- app/model.py | 29 +++++++++++++++++++---------- app/oauth2.py | 13 +++++++++---- app/routers/auth.py | 2 +- app/routers/posts.py | 29 ++++++++++++++++------------- app/routers/users.py | 2 -- 8 files changed, 68 insertions(+), 34 deletions(-) create mode 100644 app/config.py diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..fa11f80 --- /dev/null +++ b/app/config.py @@ -0,0 +1,19 @@ +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + database_hostname: str + database_port: str + database_password: str + database_name: str + database_username: str + secret_key: str + algorithm: str + access_token_expire_minutes: int + + class Config: + env_file = ".env" + + + +settings = Settings() diff --git a/app/crud/posts_crud.py b/app/crud/posts_crud.py index 1e1011b..94051b0 100644 --- a/app/crud/posts_crud.py +++ b/app/crud/posts_crud.py @@ -8,11 +8,10 @@ def insert_data_in_uposts_table(post: UPosts): session.add(post) session.commit() session.refresh(post) - session.close() return post -def get_all_post(): - posts = session.exec(select(UPosts)).all() +def get_all_post(limit, skip, search): + posts = session.exec(select(UPosts).filter(UPosts.tittle.contains(search)).limit(limit).offset(skip)).all() return posts def get_post_by_id(id: int): diff --git a/app/database.py b/app/database.py index 875f9dc..117d8b9 100644 --- a/app/database.py +++ b/app/database.py @@ -1,6 +1,7 @@ from sqlmodel import create_engine, SQLModel, Session +from .config import settings -POSTGRESS_SQL_DATABASE_URL = "postgresql://postgres:root@localhost/fastapi" +POSTGRESS_SQL_DATABASE_URL = f"postgresql://{settings.database_username}:{settings.database_password}@{settings.database_hostname}/{settings.database_name}" # add echo = true if you like to log create query engine = create_engine(POSTGRESS_SQL_DATABASE_URL) diff --git a/app/model.py b/app/model.py index 702aa00..9349885 100644 --- a/app/model.py +++ b/app/model.py @@ -1,21 +1,19 @@ import datetime import bcrypt from pydantic import EmailStr, field_validator -from typing import Optional -from sqlmodel import Field, SQLModel, Session, select +from typing import List, Optional +from sqlalchemy import ForeignKey +from sqlmodel import Field, SQLModel, Relationship from .database import engine -class UPosts(SQLModel, table=True): - id: Optional[int] | None = Field(default= None, primary_key = True) - tittle: str - content: str - create_dtm: Optional[datetime.datetime] = Field(default_factory=datetime.datetime.now) class Users(SQLModel, table=True): userid: Optional[int] | None = Field(default=None, primary_key=True) email: EmailStr = Field(sa_column_kwargs={"unique": True}) password: str create_dtm: Optional[datetime.datetime] = Field(default_factory=datetime.datetime.now) + posts: list["UPosts"] = Relationship(back_populates="owner") + @field_validator("email", mode="before") def validate_email_domain(cls, value): @@ -35,17 +33,27 @@ def password_hash(cls, password): def verify_password(self, password: str)->bool: return bcrypt.checkpw(password.encode('utf-8'), self.password.encode('utf-8')) -class UPostOut(SQLModel): - id: int +class UPosts(SQLModel, table=True): + id: Optional[int] | None = Field(default= None, primary_key = True) tittle: str content: str - create_dtm: datetime.datetime + userid: int = Field(foreign_key="users.userid", nullable=False) + create_dtm: Optional[datetime.datetime] = Field(default_factory=datetime.datetime.now) + owner: Optional[Users] = Relationship(back_populates="posts") class UserOut(SQLModel): userid: int email: EmailStr create_dtm: datetime.datetime +class UPostOut(SQLModel): + id: int + tittle: str + content: str + userid: int + create_dtm: datetime.datetime + owner: Optional[UserOut] + class UserLogin(SQLModel): email: EmailStr password: str @@ -56,3 +64,4 @@ class Token(SQLModel): class TokenData(SQLModel): email: Optional[EmailStr] = None + id: Optional[int] = None diff --git a/app/oauth2.py b/app/oauth2.py index 3a52608..5ac1709 100644 --- a/app/oauth2.py +++ b/app/oauth2.py @@ -4,13 +4,15 @@ from fastapi import status, Depends, HTTPException from fastapi.security import OAuth2PasswordBearer from app.crud.users_crud import * +from .config import settings + oauth2_scheme = OAuth2PasswordBearer(tokenUrl='login') -SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" -ALGORITHM = "HS256" -ACCESS_TOKEN_EXPIRE_MINUTES = 1 +SECRET_KEY = f"{settings.secret_key}" +ALGORITHM = f"{settings.algorithm}" +ACCESS_TOKEN_EXPIRE_MINUTES = settings.access_token_expire_minutes def create_jwt_token(data: dict): @@ -24,9 +26,12 @@ def verify_jwt_token(token: str, credentials_exception): try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) email : str = payload.get("email") + id: int = payload.get("userid") if email is None: raise credentials_exception - token_data = TokenData(email=email) + if id is None: + raise credentials_exception + token_data = TokenData(email=email,id=id) except jwt.InvalidTokenError: raise credentials_exception diff --git a/app/routers/auth.py b/app/routers/auth.py index 172944c..e80ca92 100644 --- a/app/routers/auth.py +++ b/app/routers/auth.py @@ -12,7 +12,7 @@ def login(user_credentials: OAuth2PasswordRequestForm = Depends()): user = get_user_by_email(user_credentials.username) if not user: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN,detail=f"User Not Found") - access_token = create_jwt_token(data = {"email": user.email}) + access_token = create_jwt_token(data = {"email": user.email, "userid": user.userid}) if not verify(user_credentials.password, user.password): raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=f"Invalid Password or Email Id") return {"access_token": access_token, "token_type": "bearer"} diff --git a/app/routers/posts.py b/app/routers/posts.py index 4431cb2..a2a2af4 100644 --- a/app/routers/posts.py +++ b/app/routers/posts.py @@ -1,42 +1,45 @@ -from typing import List +from typing import List, Optional from fastapi import APIRouter, HTTPException, status, Response, Depends from app.crud.posts_crud import * -from app.model import UPostOut +from app.model import UPostOut, TokenData from app.oauth2 import get_current_user router = APIRouter() @router.get('/', status_code=status.HTTP_200_OK, response_model=List[UPostOut],) -def get_list_of_post(userid: int = Depends(get_current_user))->List[UPostOut]: - posts = get_all_post() +def get_list_of_post(current_user:TokenData = Depends(get_current_user), limit: int = 5, skip: int = 0, search: Optional[str] = "")->List[UPostOut]: + posts = get_all_post(limit, skip, search) return posts - + @router.get('/get/{id}', status_code=status.HTTP_200_OK, response_model=UPostOut) -def get_post(id: int, response: Response, user_email: str = Depends(get_current_user)): - print("user email", user_email) +def get_post(id: int, response: Response, current_user:TokenData = Depends(get_current_user)): post = get_post_by_id(id) if not post: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"post with {id} does not exist in UPosts") - print(list(post)) + if current_user.id != post.userid: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=f"User not Authorized to get the post") return post @router.post('/add', status_code=status.HTTP_201_CREATED, response_model=UPostOut) -def create_post(post: UPosts, user_email: str = Depends(get_current_user)): - print(user_email) +def create_post(post: UPosts, current_user:TokenData = Depends(get_current_user)): + post.userid = current_user.id post = insert_data_in_uposts_table(post) return post @router.put('/update/{id}', status_code=status.HTTP_200_OK, response_model=UPostOut) -def update_post(id: int, upost: UPosts, response: Response, user_email: str = Depends(get_current_user)): +def update_post(id: int, upost: UPosts, response: Response, current_user:TokenData = Depends(get_current_user)): post = update_post_by_id(id, upost) if not post: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"post with {id} does not exist in UPosts") - print(list(post)) + if current_user.id != post.userid: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=f"User not Authorized to get the post") return post @router.delete('/delete/{id}', status_code=status.HTTP_200_OK, response_model=UPostOut) -def delete_post(id: int, response: Response, user_email: str = Depends(get_current_user)): +def delete_post(id: int, response: Response, current_user:TokenData = Depends(get_current_user)): post = delete_post_by_id(id) if not post: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"post with {id} does not exist in UPosts") + if current_user.id != post.userid: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=f"User not Authorized to get the post") return post diff --git a/app/routers/users.py b/app/routers/users.py index 523da16..7d94cb1 100644 --- a/app/routers/users.py +++ b/app/routers/users.py @@ -24,7 +24,6 @@ def get_user_by_email(email: str, response: Response): raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"user with {email} does not exist in Users") return user - @router.post('/add', status_code=status.HTTP_201_CREATED, response_model=UserOut) def create_user(user: Users)->UserOut: try: @@ -40,7 +39,6 @@ def update_user(id: int, user: Users, response: Response): raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"user with {id} does not exist in Users") return user - @router.delete('/delete/{id}', status_code=status.HTTP_200_OK, response_model=UserOut) def delete_user(id: int, response: Response): user = delete_user_by_id(id) From 913e18760d26e2cbe0b407fe56f38f673b2a22ad Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Wed, 1 Jan 2025 23:42:49 +0530 Subject: [PATCH 12/55] updated with on delete cascade for post and user --- app/model.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/model.py b/app/model.py index 9349885..b9d8edc 100644 --- a/app/model.py +++ b/app/model.py @@ -12,7 +12,7 @@ class Users(SQLModel, table=True): email: EmailStr = Field(sa_column_kwargs={"unique": True}) password: str create_dtm: Optional[datetime.datetime] = Field(default_factory=datetime.datetime.now) - posts: list["UPosts"] = Relationship(back_populates="owner") + posts: list["UPosts"] = Relationship(back_populates="owner", sa_relationship_kwargs={"cascade":"all, delete-orphan"}) @field_validator("email", mode="before") @@ -65,3 +65,4 @@ class Token(SQLModel): class TokenData(SQLModel): email: Optional[EmailStr] = None id: Optional[int] = None + From d1d0598f20cd14449de56a191cf8533c4eee717f Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Thu, 2 Jan 2025 13:02:37 +0530 Subject: [PATCH 13/55] upvote and downvote for post implemented with login credentials --- app/crud/votes_crud.py | 25 +++++++++++++++++++++++++ app/main.py | 4 +++- app/model.py | 19 +++++++++++++++++-- app/routers/votes.py | 21 +++++++++++++++++++++ 4 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 app/crud/votes_crud.py create mode 100644 app/routers/votes.py diff --git a/app/crud/votes_crud.py b/app/crud/votes_crud.py new file mode 100644 index 0000000..a51056b --- /dev/null +++ b/app/crud/votes_crud.py @@ -0,0 +1,25 @@ +from fastapi import HTTPException, status +from pydantic import ValidationError +from app.database import get_session +from app.model import Vote, Votes +from sqlmodel import select + +session = next(get_session()) + +def is_vote_exist(vote: Vote, userid): + vote_found = session.exec(select(Votes).filter(Votes.post_id == vote.post_id, Votes.user_id == userid)).first() + return vote_found + +def add_vote(vote: Vote, userid): + vote = Votes(post_id=vote.post_id, user_id=userid) + session.add(vote) + session.commit() + return {"msg":"Added Vote"} + +def delete_vote(vote: Vote, userid): + vote = session.exec(select(Votes).filter(Votes.post_id == vote.post_id, Votes.user_id == userid)).first() + session.delete(vote) + session.commit() + return {"msg":"Deleted Vote"} + + diff --git a/app/main.py b/app/main.py index 1acd156..ee47a90 100644 --- a/app/main.py +++ b/app/main.py @@ -1,6 +1,6 @@ from fastapi import FastAPI from contextlib import asynccontextmanager -from app.routers import posts, users, auth +from app.routers import posts, users, auth, votes from app.database import create_database_and_tables, drop_database_and_tables @@ -23,6 +23,8 @@ async def lifespan(app: FastAPI): app.include_router(posts.router, prefix="/uposts", tags=["Posts"]) app.include_router(users.router, prefix="/users", tags=["Users"]) app.include_router(auth.router, prefix="/auth", tags=["Authentication"]) +app.include_router(votes.router, prefix="/vote", tags=["Votes"]) + @app.get("/") diff --git a/app/model.py b/app/model.py index b9d8edc..7163582 100644 --- a/app/model.py +++ b/app/model.py @@ -5,7 +5,7 @@ from sqlalchemy import ForeignKey from sqlmodel import Field, SQLModel, Relationship from .database import engine - +from enum import Enum class Users(SQLModel, table=True): userid: Optional[int] | None = Field(default=None, primary_key=True) @@ -13,6 +13,7 @@ class Users(SQLModel, table=True): password: str create_dtm: Optional[datetime.datetime] = Field(default_factory=datetime.datetime.now) posts: list["UPosts"] = Relationship(back_populates="owner", sa_relationship_kwargs={"cascade":"all, delete-orphan"}) + votes: list["Votes"] = Relationship(back_populates="user", sa_relationship_kwargs={"cascade":"all, delete-orphan"}) @field_validator("email", mode="before") @@ -40,6 +41,7 @@ class UPosts(SQLModel, table=True): userid: int = Field(foreign_key="users.userid", nullable=False) create_dtm: Optional[datetime.datetime] = Field(default_factory=datetime.datetime.now) owner: Optional[Users] = Relationship(back_populates="posts") + votes: list["Votes"] = Relationship(back_populates="post", sa_relationship_kwargs={"cascade":"all, delete-orphan"}) class UserOut(SQLModel): userid: int @@ -65,4 +67,17 @@ class Token(SQLModel): class TokenData(SQLModel): email: Optional[EmailStr] = None id: Optional[int] = None - + +class Votes(SQLModel, table = True): + user_id: Optional[int] = Field(default= None, primary_key = True, foreign_key="users.userid") + post_id: Optional[int] = Field(default= None, primary_key = True, foreign_key="uposts.id") + user: Optional[Users] = Relationship(back_populates="votes") + post: Optional[UPosts] = Relationship(back_populates="votes") + +class VoteDirection(str,Enum): + DOWNVOTE = "DOWNVOTE" # type: ignore + UPVOTE = "UPVOTE" # type: ignore + +class Vote(SQLModel): + post_id: int + dir: VoteDirection \ No newline at end of file diff --git a/app/routers/votes.py b/app/routers/votes.py new file mode 100644 index 0000000..efd6ebd --- /dev/null +++ b/app/routers/votes.py @@ -0,0 +1,21 @@ +from typing import List +from fastapi import APIRouter, Depends, status, HTTPException, Response +from app.crud.votes_crud import * +from app.model import Vote, TokenData +from app.oauth2 import get_current_user + +router = APIRouter() + +@router.post("/", status_code=status.HTTP_201_CREATED) +def vote(vote: Vote, current_user:TokenData = Depends(get_current_user)): + vote_found = is_vote_exist(vote, current_user.id) + print(vote.dir.value == "UPVOTE") + if(vote.dir.value == "UPVOTE"): + if vote_found: + raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=f"User {current_user.id} has Already voted on Post with id {vote.post_id}") + add_vote(vote, current_user.id) + else: + if not vote_found: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Us~er {current_user.id} has not voted on Post with id {vote.post_id}") + delete_vote(vote, current_user.id) + From 84ba1864bd5263f763cfc68dc3ab0b581d345c39 Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Thu, 2 Jan 2025 15:51:42 +0530 Subject: [PATCH 14/55] fetched post details with vote count --- app/crud/posts_crud.py | 9 +++++++-- app/model.py | 6 +++++- app/routers/posts.py | 11 ++++++++--- app/routers/votes.py | 5 ++--- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/app/crud/posts_crud.py b/app/crud/posts_crud.py index 94051b0..5d9ee06 100644 --- a/app/crud/posts_crud.py +++ b/app/crud/posts_crud.py @@ -1,6 +1,6 @@ from app.database import get_session -from app.model import UPosts -from sqlmodel import select +from app.model import UPosts, Votes +from sqlmodel import select, func session = next(get_session()) @@ -14,6 +14,11 @@ def get_all_post(limit, skip, search): posts = session.exec(select(UPosts).filter(UPosts.tittle.contains(search)).limit(limit).offset(skip)).all() return posts +def get_all_post_with_count(limit, skip, search): + posts = session.exec(select(UPosts, func.count(Votes.post_id).label("vote_count")).join(Votes, UPosts.id == Votes.post_id, isouter=True).group_by(UPosts.id).filter(UPosts.tittle.contains(search)).limit(limit).offset(skip)).all() + posts_with_counts = [{"UPosts": post, "vote": vote_count} for post, vote_count in posts] + return posts_with_counts + def get_post_by_id(id: int): post = session.get(UPosts, id) return post diff --git a/app/model.py b/app/model.py index 7163582..ad4cff4 100644 --- a/app/model.py +++ b/app/model.py @@ -80,4 +80,8 @@ class VoteDirection(str,Enum): class Vote(SQLModel): post_id: int - dir: VoteDirection \ No newline at end of file + dir: VoteDirection + +class UPostswithCount(SQLModel): + UPosts: UPosts + vote: int diff --git a/app/routers/posts.py b/app/routers/posts.py index a2a2af4..7a48134 100644 --- a/app/routers/posts.py +++ b/app/routers/posts.py @@ -1,16 +1,21 @@ from typing import List, Optional from fastapi import APIRouter, HTTPException, status, Response, Depends from app.crud.posts_crud import * -from app.model import UPostOut, TokenData +from app.model import UPostOut, TokenData, UPostswithCount from app.oauth2 import get_current_user router = APIRouter() -@router.get('/', status_code=status.HTTP_200_OK, response_model=List[UPostOut],) +@router.get('/', status_code=status.HTTP_200_OK, response_model=List[UPostOut]) def get_list_of_post(current_user:TokenData = Depends(get_current_user), limit: int = 5, skip: int = 0, search: Optional[str] = "")->List[UPostOut]: posts = get_all_post(limit, skip, search) return posts - + +@router.get('/withcount', status_code=status.HTTP_200_OK, response_model=List[UPostswithCount]) +def get_list_of_post_with_count(current_user:TokenData = Depends(get_current_user), limit: int = 5, skip: int = 0, search: Optional[str] = "")->List[UPostswithCount]: + posts = get_all_post_with_count(limit, skip, search) + return posts + @router.get('/get/{id}', status_code=status.HTTP_200_OK, response_model=UPostOut) def get_post(id: int, response: Response, current_user:TokenData = Depends(get_current_user)): post = get_post_by_id(id) diff --git a/app/routers/votes.py b/app/routers/votes.py index efd6ebd..aebf250 100644 --- a/app/routers/votes.py +++ b/app/routers/votes.py @@ -9,13 +9,12 @@ @router.post("/", status_code=status.HTTP_201_CREATED) def vote(vote: Vote, current_user:TokenData = Depends(get_current_user)): vote_found = is_vote_exist(vote, current_user.id) - print(vote.dir.value == "UPVOTE") if(vote.dir.value == "UPVOTE"): if vote_found: raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=f"User {current_user.id} has Already voted on Post with id {vote.post_id}") - add_vote(vote, current_user.id) + return add_vote(vote, current_user.id) else: if not vote_found: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Us~er {current_user.id} has not voted on Post with id {vote.post_id}") - delete_vote(vote, current_user.id) + return delete_vote(vote, current_user.id) From 42b13d8f49fdc3b4c0559f0e289ccb44704891dd Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Tue, 7 Jan 2025 13:00:11 +0530 Subject: [PATCH 15/55] updated with alembic --- alembic.ini | 117 ++++++++++++++++++ app/config.py | 1 + app/main.py | 11 ++ app/model.py | 1 + migrations/README | 1 + migrations/env.py | 91 ++++++++++++++ migrations/script.py.mako | 26 ++++ .../versions/6b78e5356a6f_test_migrations.py | 55 ++++++++ .../9538b4ec1aaf_adding_phone_column.py | 30 +++++ requirement.txt | 35 ------ requirements.txt | Bin 0 -> 1676 bytes 11 files changed, 333 insertions(+), 35 deletions(-) create mode 100644 alembic.ini create mode 100644 migrations/README create mode 100644 migrations/env.py create mode 100644 migrations/script.py.mako create mode 100644 migrations/versions/6b78e5356a6f_test_migrations.py create mode 100644 migrations/versions/9538b4ec1aaf_adding_phone_column.py delete mode 100644 requirement.txt create mode 100644 requirements.txt diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000..421e3fe --- /dev/null +++ b/alembic.ini @@ -0,0 +1,117 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +# Use forward slashes (/) also on windows to provide an os agnostic path +script_location = migrations + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python>=3.9 or backports.zoneinfo library. +# Any required deps can installed by adding `alembic[tz]` to the pip requirements +# string value is passed to ZoneInfo() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to migrations/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +# version_path_separator = newline +version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# new in Alembic version 1.10 +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = driver://user:pass@localhost/dbname + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# lint with attempts to fix using "ruff" - use the exec runner, execute a binary +# hooks = ruff +# ruff.type = exec +# ruff.executable = %(here)s/.venv/bin/ruff +# ruff.options = --fix REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARNING +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARNING +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/app/config.py b/app/config.py index fa11f80..1286979 100644 --- a/app/config.py +++ b/app/config.py @@ -6,6 +6,7 @@ class Settings(BaseSettings): database_port: str database_password: str database_name: str + database_name1: str database_username: str secret_key: str algorithm: str diff --git a/app/main.py b/app/main.py index ee47a90..189ab67 100644 --- a/app/main.py +++ b/app/main.py @@ -1,6 +1,7 @@ from fastapi import FastAPI from contextlib import asynccontextmanager from app.routers import posts, users, auth, votes +from fastapi.middleware.cors import CORSMiddleware from app.database import create_database_and_tables, drop_database_and_tables @@ -19,6 +20,16 @@ async def lifespan(app: FastAPI): app = FastAPI(lifespan=lifespan) +origins = ["*"] + +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + app.include_router(posts.router, prefix="/uposts", tags=["Posts"]) app.include_router(users.router, prefix="/users", tags=["Users"]) diff --git a/app/model.py b/app/model.py index ad4cff4..61af23c 100644 --- a/app/model.py +++ b/app/model.py @@ -11,6 +11,7 @@ class Users(SQLModel, table=True): userid: Optional[int] | None = Field(default=None, primary_key=True) email: EmailStr = Field(sa_column_kwargs={"unique": True}) password: str + # phone: str create_dtm: Optional[datetime.datetime] = Field(default_factory=datetime.datetime.now) posts: list["UPosts"] = Relationship(back_populates="owner", sa_relationship_kwargs={"cascade":"all, delete-orphan"}) votes: list["Votes"] = Relationship(back_populates="user", sa_relationship_kwargs={"cascade":"all, delete-orphan"}) diff --git a/migrations/README b/migrations/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..af21243 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,91 @@ +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool +from alembic import context +from sqlmodel import SQLModel +from app.model import * +from app.config import settings + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config +config.set_main_option("sqlalchemy.url",f'postgresql+psycopg2://{settings.database_username}:{settings.database_password}@{settings.database_hostname}/{settings.database_name1}') + + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = SQLModel.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") +# ... etc. + +def process_revision_directives(context, revision, directives): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + else: + script.imports.add("import sqlmodel") + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + process_revision_directives=process_revision_directives + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata, + process_revision_directives=process_revision_directives + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..fbc4b07 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,26 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/6b78e5356a6f_test_migrations.py b/migrations/versions/6b78e5356a6f_test_migrations.py new file mode 100644 index 0000000..c4aace1 --- /dev/null +++ b/migrations/versions/6b78e5356a6f_test_migrations.py @@ -0,0 +1,55 @@ +"""Test Migrations + +Revision ID: 6b78e5356a6f +Revises: +Create Date: 2025-01-07 12:06:00.908088 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import sqlmodel + +# revision identifiers, used by Alembic. +revision: str = '6b78e5356a6f' +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('users', + sa.Column('userid', sa.Integer(), nullable=False), + sa.Column('email', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('password', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('create_dtm', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('userid'), + sa.UniqueConstraint('email') + ) + op.create_table('uposts', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('tittle', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('content', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('userid', sa.Integer(), nullable=False), + sa.Column('create_dtm', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['userid'], ['users.userid'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('votes', + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('post_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['post_id'], ['uposts.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['users.userid'], ), + sa.PrimaryKeyConstraint('user_id', 'post_id') + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('votes') + op.drop_table('uposts') + op.drop_table('users') + # ### end Alembic commands ### diff --git a/migrations/versions/9538b4ec1aaf_adding_phone_column.py b/migrations/versions/9538b4ec1aaf_adding_phone_column.py new file mode 100644 index 0000000..bd5c010 --- /dev/null +++ b/migrations/versions/9538b4ec1aaf_adding_phone_column.py @@ -0,0 +1,30 @@ +"""adding phone column + +Revision ID: 9538b4ec1aaf +Revises: 6b78e5356a6f +Create Date: 2025-01-07 12:12:05.595441 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import sqlmodel + +# revision identifiers, used by Alembic. +revision: str = '9538b4ec1aaf' +down_revision: Union[str, None] = '6b78e5356a6f' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('users', sa.Column('phone', sqlmodel.sql.sqltypes.AutoString(), nullable=False)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('users', 'phone') + # ### end Alembic commands ### diff --git a/requirement.txt b/requirement.txt deleted file mode 100644 index e69a986..0000000 --- a/requirement.txt +++ /dev/null @@ -1,35 +0,0 @@ -annotated-types==0.7.0 -anyio==4.5.2 -certifi==2024.8.30 -click==8.1.7 -colorama==0.4.6 -dnspython==2.6.1 -email-validator==2.2.0 -exceptiongroup==1.2.2 -fastapi==0.115.5 -fastapi-cli==0.0.5 -h11==0.14.0 -httpcore==1.0.7 -httptools==0.6.4 -httpx==0.28.0 -idna==3.10 -jinja2==3.1.4 -markdown-it-py==3.0.0 -MarkupSafe==2.1.5 -mdurl==0.1.2 -psycopg2==2.9.1 -pydantic==2.10.2 -pydantic-core==2.27.1 -pygments==2.18.0 -python-dotenv==1.0.1 -python-multipart==0.0.18 -PyYAML==6.0.2 -rich==13.9.4 -shellingham==1.5.4 -sniffio==1.3.1 -starlette==0.41.3 -typer==0.14.0 -typing-extensions==4.12.2 -uvicorn==0.32.1 -watchfiles==0.24.0 -websockets==13.1 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..140abcf556cfa920bfa33233b6afbaad73ed60fe GIT binary patch literal 1676 zcmY+EL2uen5QO(!sXv7(Hl#@oJ>(j-Qma&|dWwuOkb(`xrof+X`^~OjY-AZ5@4VUB z*?sTd-@>+5S#5KBwbDLuW%hx4#_x^2w|7=p!?m-Hw+`&WCOj{;=e_k6H(>9s@E6#2 z9uv<;?wotix0bb;^Ule4aGuVQ(;k(3w1oWt_SKf$(v_npcU|`&r@gw8ctV77DTxdv zwW+F&uh%Le?t1)pXj%&;Wc4-iou(|`M zJQY_%yn<5*IUb&=-nBEQmE#FmgYVE#dR3LL)jF|ix`>Cd}E_Yi(}o zNjM|l8Y&%h#rG$wY3vt~d=yKZU--TuqgF|gD!TV<>dS%Hg6Ii`2|OLY>SxLk4yktW z2(3Om^S;ERF)I2|!*&F>^_W&GXTL4A~4Yh0<{RB7KOTmvG`p6mIG z3ZLy8xgX&vov}ty@?A=%oQkThwclWBrgY(Xg-O-q{cOb|USK#ok50Hahhi#p`ei;FF>gmYqB+w!8RuVUkC~JH6aI8wqfTKd45oBG V#4bMZeNJQta>IL?not;y{RcOR@ZJCb literal 0 HcmV?d00001 From 72ceaf722b321110ce61c7fb182afdb598933bf7 Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Tue, 7 Jan 2025 18:36:14 +0530 Subject: [PATCH 16/55] added Procfile for heroku --- Procfile | 1 + 1 file changed, 1 insertion(+) create mode 100644 Procfile diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..8592602 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: uvicorn app.main:app --host=0.0.0.0 --port=${POST:-5000} \ No newline at end of file From 00d6fba1252c2cadb2503421b8e22a734e144d47 Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Tue, 7 Jan 2025 18:41:56 +0530 Subject: [PATCH 17/55] updated code for heroku --- app/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/main.py b/app/main.py index 189ab67..4b6d85c 100644 --- a/app/main.py +++ b/app/main.py @@ -40,7 +40,7 @@ async def lifespan(app: FastAPI): @app.get("/") def root(): - return {"Message": "Welcome to FastAPI APP"} + return {"Message": "Welcome to FastAPI APP My First Web heroku APP"} From 91af19d6e7cf803f1830295ea9a981009e8e1a83 Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Tue, 7 Jan 2025 18:51:31 +0530 Subject: [PATCH 18/55] removed requirement file --- requirements.txt | Bin 1676 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 140abcf556cfa920bfa33233b6afbaad73ed60fe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1676 zcmY+EL2uen5QO(!sXv7(Hl#@oJ>(j-Qma&|dWwuOkb(`xrof+X`^~OjY-AZ5@4VUB z*?sTd-@>+5S#5KBwbDLuW%hx4#_x^2w|7=p!?m-Hw+`&WCOj{;=e_k6H(>9s@E6#2 z9uv<;?wotix0bb;^Ule4aGuVQ(;k(3w1oWt_SKf$(v_npcU|`&r@gw8ctV77DTxdv zwW+F&uh%Le?t1)pXj%&;Wc4-iou(|`M zJQY_%yn<5*IUb&=-nBEQmE#FmgYVE#dR3LL)jF|ix`>Cd}E_Yi(}o zNjM|l8Y&%h#rG$wY3vt~d=yKZU--TuqgF|gD!TV<>dS%Hg6Ii`2|OLY>SxLk4yktW z2(3Om^S;ERF)I2|!*&F>^_W&GXTL4A~4Yh0<{RB7KOTmvG`p6mIG z3ZLy8xgX&vov}ty@?A=%oQkThwclWBrgY(Xg-O-q{cOb|USK#ok50Hahhi#p`ei;FF>gmYqB+w!8RuVUkC~JH6aI8wqfTKd45oBG V#4bMZeNJQta>IL?not;y{RcOR@ZJCb From fb5106e5a834f4daa69441bf783a99dfeedd3445 Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Tue, 7 Jan 2025 18:59:33 +0530 Subject: [PATCH 19/55] updated req file --- requirements.txt | Bin 0 -> 1676 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..140abcf556cfa920bfa33233b6afbaad73ed60fe GIT binary patch literal 1676 zcmY+EL2uen5QO(!sXv7(Hl#@oJ>(j-Qma&|dWwuOkb(`xrof+X`^~OjY-AZ5@4VUB z*?sTd-@>+5S#5KBwbDLuW%hx4#_x^2w|7=p!?m-Hw+`&WCOj{;=e_k6H(>9s@E6#2 z9uv<;?wotix0bb;^Ule4aGuVQ(;k(3w1oWt_SKf$(v_npcU|`&r@gw8ctV77DTxdv zwW+F&uh%Le?t1)pXj%&;Wc4-iou(|`M zJQY_%yn<5*IUb&=-nBEQmE#FmgYVE#dR3LL)jF|ix`>Cd}E_Yi(}o zNjM|l8Y&%h#rG$wY3vt~d=yKZU--TuqgF|gD!TV<>dS%Hg6Ii`2|OLY>SxLk4yktW z2(3Om^S;ERF)I2|!*&F>^_W&GXTL4A~4Yh0<{RB7KOTmvG`p6mIG z3ZLy8xgX&vov}ty@?A=%oQkThwclWBrgY(Xg-O-q{cOb|USK#ok50Hahhi#p`ei;FF>gmYqB+w!8RuVUkC~JH6aI8wqfTKd45oBG V#4bMZeNJQta>IL?not;y{RcOR@ZJCb literal 0 HcmV?d00001 From 747af43e61f00e08f4531b35db8de1808e252945 Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Tue, 7 Jan 2025 19:36:33 +0530 Subject: [PATCH 20/55] updated the code --- Procfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Procfile b/Procfile index 8592602..087e818 100644 --- a/Procfile +++ b/Procfile @@ -1 +1,2 @@ -web: uvicorn app.main:app --host=0.0.0.0 --port=${POST:-5000} \ No newline at end of file + +web: uvicorn app.main:app --host=0.0.0.0 --port=${PORT:-5000} \ No newline at end of file From 7a15d1578ad872bb5bb9d4fc7e25c48793d4b25b Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Sat, 11 Jan 2025 13:09:13 +0530 Subject: [PATCH 21/55] udpated database_string --- app/config.py | 1 + app/database.py | 6 ++++-- migrations/env.py | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/config.py b/app/config.py index 1286979..2a041d5 100644 --- a/app/config.py +++ b/app/config.py @@ -11,6 +11,7 @@ class Settings(BaseSettings): secret_key: str algorithm: str access_token_expire_minutes: int + database_string: str class Config: env_file = ".env" diff --git a/app/database.py b/app/database.py index 117d8b9..d51a86d 100644 --- a/app/database.py +++ b/app/database.py @@ -1,13 +1,15 @@ from sqlmodel import create_engine, SQLModel, Session from .config import settings -POSTGRESS_SQL_DATABASE_URL = f"postgresql://{settings.database_username}:{settings.database_password}@{settings.database_hostname}/{settings.database_name}" +POSTGRESS_SQL_DATABASE_URL = f"postgresql://{settings.database_username}:{settings.database_password}@{settings.database_hostname}/{settings.database_name1}" # add echo = true if you like to log create query -engine = create_engine(POSTGRESS_SQL_DATABASE_URL) +# engine = create_engine(POSTGRESS_SQL_DATABASE_URL, echo = True) +engine = create_engine(f"{settings.database_string}", echo = True) def create_database_and_tables(): + print(POSTGRESS_SQL_DATABASE_URL) SQLModel.metadata.create_all(engine) return "Database Connected" diff --git a/migrations/env.py b/migrations/env.py index af21243..d32e861 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -10,7 +10,8 @@ # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config -config.set_main_option("sqlalchemy.url",f'postgresql+psycopg2://{settings.database_username}:{settings.database_password}@{settings.database_hostname}/{settings.database_name1}') +# config.set_main_option("sqlalchemy.url",f'postgresql+psycopg2://{settings.database_username}:{settings.database_password}@{settings.database_hostname}/{settings.database_name1}') +config.set_main_option("sqlalchemy.url",f'{settings.database_string}') # Interpret the config file for Python logging. From 492ac8a5f6b6588da2d1d5fc115e3dd88a9cf35d Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Sat, 11 Jan 2025 13:51:05 +0530 Subject: [PATCH 22/55] udpated port --- app/main.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/main.py b/app/main.py index 4b6d85c..00a2e8a 100644 --- a/app/main.py +++ b/app/main.py @@ -2,7 +2,7 @@ from contextlib import asynccontextmanager from app.routers import posts, users, auth, votes from fastapi.middleware.cors import CORSMiddleware - +import uvicorn from app.database import create_database_and_tables, drop_database_and_tables from .model import * @@ -43,6 +43,9 @@ def root(): return {"Message": "Welcome to FastAPI APP My First Web heroku APP"} +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0",port=8800) + # class Post(BaseModel): # tittle: str From f23ef608c1adc5ebc4975487c54b0f11fb30c404 Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Sat, 11 Jan 2025 14:14:20 +0530 Subject: [PATCH 23/55] udpated port --- app/main.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/main.py b/app/main.py index 00a2e8a..22c0470 100644 --- a/app/main.py +++ b/app/main.py @@ -3,7 +3,7 @@ from app.routers import posts, users, auth, votes from fastapi.middleware.cors import CORSMiddleware import uvicorn - +from config import settings from app.database import create_database_and_tables, drop_database_and_tables from .model import * @@ -40,11 +40,12 @@ async def lifespan(app: FastAPI): @app.get("/") def root(): - return {"Message": "Welcome to FastAPI APP My First Web heroku APP"} + return {"Message": "Welcome to FastAPI, My First Web APP"} if __name__ == "__main__": - uvicorn.run(app, host="0.0.0.0",port=8800) + port = settings.database_port + uvicorn.run(app, host="0.0.0.0", port=port) # class Post(BaseModel): From c77c4525d2abd348929fc173ce02fcfcf269b162 Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Sat, 11 Jan 2025 14:19:47 +0530 Subject: [PATCH 24/55] updated port --- app/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/main.py b/app/main.py index 22c0470..d8fe7e1 100644 --- a/app/main.py +++ b/app/main.py @@ -3,7 +3,7 @@ from app.routers import posts, users, auth, votes from fastapi.middleware.cors import CORSMiddleware import uvicorn -from config import settings +from app.config import settings from app.database import create_database_and_tables, drop_database_and_tables from .model import * From 84d89a2f78b0f32c24ee9daadaeba1e7f0700961 Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Sun, 12 Jan 2025 16:47:30 +0530 Subject: [PATCH 25/55] updated the code for better login functionality --- app/crud/posts_crud.py | 11 +++-- app/crud/users_crud.py | 2 +- app/database.py | 2 +- app/model.py | 12 ++++- app/oauth2.py | 2 +- app/routers/auth.py | 2 + app/routers/posts.py | 39 +++++++++++---- app/routers/users.py | 49 +++++++++++++------ .../versions/6b78e5356a6f_test_migrations.py | 2 +- 9 files changed, 86 insertions(+), 35 deletions(-) diff --git a/app/crud/posts_crud.py b/app/crud/posts_crud.py index 5d9ee06..375a31d 100644 --- a/app/crud/posts_crud.py +++ b/app/crud/posts_crud.py @@ -1,6 +1,7 @@ from app.database import get_session -from app.model import UPosts, Votes -from sqlmodel import select, func +from app.model import UPosts, Votes, Users +from sqlmodel import select, func +from sqlalchemy.orm import joinedload session = next(get_session()) @@ -11,11 +12,11 @@ def insert_data_in_uposts_table(post: UPosts): return post def get_all_post(limit, skip, search): - posts = session.exec(select(UPosts).filter(UPosts.tittle.contains(search)).limit(limit).offset(skip)).all() + posts = session.exec(select(UPosts).filter(UPosts.title.contains(search)).limit(limit).offset(skip)).all() return posts def get_all_post_with_count(limit, skip, search): - posts = session.exec(select(UPosts, func.count(Votes.post_id).label("vote_count")).join(Votes, UPosts.id == Votes.post_id, isouter=True).group_by(UPosts.id).filter(UPosts.tittle.contains(search)).limit(limit).offset(skip)).all() + posts = session.exec(select(UPosts, func.count(Votes.post_id).label("vote_count")).join(Votes, UPosts.id == Votes.post_id, isouter=True).group_by(UPosts.id).filter(UPosts.title.contains(search)).limit(limit).offset(skip)).all() posts_with_counts = [{"UPosts": post, "vote": vote_count} for post, vote_count in posts] return posts_with_counts @@ -34,7 +35,7 @@ def update_post_by_id(id: int, upost: UPosts): return postTobeUpdated def delete_post_by_id(id: int): - upost = session.get(UPosts, id) + upost = session.exec(select(UPosts).join(Users).where(UPosts.id == id).options(joinedload(UPosts.owner))).first() if upost: session.delete(upost) session.commit() diff --git a/app/crud/users_crud.py b/app/crud/users_crud.py index c682c02..d9a553f 100644 --- a/app/crud/users_crud.py +++ b/app/crud/users_crud.py @@ -16,7 +16,7 @@ def insert_data_in_users_table(user: Users)->Users: session.commit() except Exception as e: session.rollback() - raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE,detail=f"User NOT Created, Please Check Request {str(e.args)}") + raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE,detail=f"Failed to Create User : Validation") session.refresh(user) return user diff --git a/app/database.py b/app/database.py index d51a86d..a816c22 100644 --- a/app/database.py +++ b/app/database.py @@ -4,7 +4,7 @@ POSTGRESS_SQL_DATABASE_URL = f"postgresql://{settings.database_username}:{settings.database_password}@{settings.database_hostname}/{settings.database_name1}" # add echo = true if you like to log create query -# engine = create_engine(POSTGRESS_SQL_DATABASE_URL, echo = True) +# engine = create_engine(POSTGRESS_SQL_DATABASE_URL) engine = create_engine(f"{settings.database_string}", echo = True) diff --git a/app/model.py b/app/model.py index 61af23c..00db9df 100644 --- a/app/model.py +++ b/app/model.py @@ -37,7 +37,7 @@ def verify_password(self, password: str)->bool: class UPosts(SQLModel, table=True): id: Optional[int] | None = Field(default= None, primary_key = True) - tittle: str + title: str content: str userid: int = Field(foreign_key="users.userid", nullable=False) create_dtm: Optional[datetime.datetime] = Field(default_factory=datetime.datetime.now) @@ -49,14 +49,22 @@ class UserOut(SQLModel): email: EmailStr create_dtm: datetime.datetime +class UserIn(SQLModel): + email: EmailStr + password: str + class UPostOut(SQLModel): id: int - tittle: str + title: str content: str userid: int create_dtm: datetime.datetime owner: Optional[UserOut] +class UPostCreate(SQLModel): + title: str + content: str + class UserLogin(SQLModel): email: EmailStr password: str diff --git a/app/oauth2.py b/app/oauth2.py index 5ac1709..fce2f29 100644 --- a/app/oauth2.py +++ b/app/oauth2.py @@ -7,7 +7,7 @@ from .config import settings -oauth2_scheme = OAuth2PasswordBearer(tokenUrl='login') +oauth2_scheme = OAuth2PasswordBearer(tokenUrl='auth/login') SECRET_KEY = f"{settings.secret_key}" diff --git a/app/routers/auth.py b/app/routers/auth.py index e80ca92..ac0ddcd 100644 --- a/app/routers/auth.py +++ b/app/routers/auth.py @@ -9,6 +9,8 @@ @router.post("/login", status_code=status.HTTP_200_OK, response_model=Token) def login(user_credentials: OAuth2PasswordRequestForm = Depends()): + print(f"Username: {user_credentials.username}") + print(f"Password: {user_credentials.password}") user = get_user_by_email(user_credentials.username) if not user: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN,detail=f"User Not Found") diff --git a/app/routers/posts.py b/app/routers/posts.py index 7a48134..84bf094 100644 --- a/app/routers/posts.py +++ b/app/routers/posts.py @@ -1,22 +1,31 @@ from typing import List, Optional from fastapi import APIRouter, HTTPException, status, Response, Depends from app.crud.posts_crud import * -from app.model import UPostOut, TokenData, UPostswithCount +from app.model import UPostOut, TokenData, UPostswithCount, UPostCreate from app.oauth2 import get_current_user router = APIRouter() -@router.get('/', status_code=status.HTTP_200_OK, response_model=List[UPostOut]) +@router.get('/', status_code=status.HTTP_200_OK, response_model=List[UPostOut], responses={ + 404: {"description": "Posts not found"}, + 500: {"description": "Internal server error"} + }) def get_list_of_post(current_user:TokenData = Depends(get_current_user), limit: int = 5, skip: int = 0, search: Optional[str] = "")->List[UPostOut]: posts = get_all_post(limit, skip, search) return posts -@router.get('/withcount', status_code=status.HTTP_200_OK, response_model=List[UPostswithCount]) +@router.get('/withcount', status_code=status.HTTP_200_OK, response_model=List[UPostswithCount], responses={ + 404: {"description": "Posts not found"}, + 500: {"description": "Internal server error"} + }) def get_list_of_post_with_count(current_user:TokenData = Depends(get_current_user), limit: int = 5, skip: int = 0, search: Optional[str] = "")->List[UPostswithCount]: posts = get_all_post_with_count(limit, skip, search) return posts -@router.get('/get/{id}', status_code=status.HTTP_200_OK, response_model=UPostOut) +@router.get('/get/{id}', status_code=status.HTTP_200_OK, response_model=UPostOut, responses={ + 404: {"description": "Post not found"}, + 500: {"description": "Internal server error"} + }) def get_post(id: int, response: Response, current_user:TokenData = Depends(get_current_user)): post = get_post_by_id(id) if not post: @@ -25,22 +34,32 @@ def get_post(id: int, response: Response, current_user:TokenData = Depends(get_c raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=f"User not Authorized to get the post") return post -@router.post('/add', status_code=status.HTTP_201_CREATED, response_model=UPostOut) -def create_post(post: UPosts, current_user:TokenData = Depends(get_current_user)): +@router.post('/add', status_code=status.HTTP_201_CREATED, response_model=UPostOut, responses={ + 500: {"description": "Internal server error"} + }) +def create_post(post: UPostCreate, current_user:TokenData = Depends(get_current_user)): + post = UPosts(title=post.title, content=post.content) post.userid = current_user.id post = insert_data_in_uposts_table(post) return post -@router.put('/update/{id}', status_code=status.HTTP_200_OK, response_model=UPostOut) -def update_post(id: int, upost: UPosts, response: Response, current_user:TokenData = Depends(get_current_user)): - post = update_post_by_id(id, upost) +@router.put('/update/{id}', status_code=status.HTTP_200_OK, response_model=UPostOut, responses={ + 404: {"description": "Post not found"}, + 500: {"description": "Internal server error"} + }) +def update_post(id: int, post: UPostCreate, response: Response, current_user:TokenData = Depends(get_current_user)): + post = UPosts(title=post.title, content=post.content) + post = update_post_by_id(id, post) if not post: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"post with {id} does not exist in UPosts") if current_user.id != post.userid: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=f"User not Authorized to get the post") return post -@router.delete('/delete/{id}', status_code=status.HTTP_200_OK, response_model=UPostOut) +@router.delete('/delete/{id}', status_code=status.HTTP_200_OK, response_model=UPostOut, responses={ + 404: {"description": "Post not found"}, + 500: {"description": "Internal server error"} + }) def delete_post(id: int, response: Response, current_user:TokenData = Depends(get_current_user)): post = delete_post_by_id(id) if not post: diff --git a/app/routers/users.py b/app/routers/users.py index 7d94cb1..0b6946f 100644 --- a/app/routers/users.py +++ b/app/routers/users.py @@ -1,45 +1,66 @@ from typing import List from fastapi import APIRouter, status, HTTPException, Response from app.crud.users_crud import * -from app.model import UserOut +from app.model import UserOut, UserIn router = APIRouter() -@router.get("/", status_code=status.HTTP_201_CREATED, response_model=List[UserOut]) +@router.get("/", status_code=status.HTTP_201_CREATED, response_model=List[UserOut], responses={ + 404: {"description": "User not found"}, + 500: {"description": "Internal server error"} + }) def get_list_of_users()->List[UserOut]: users = get_all_user() return users -@router.get('/get/by-id/{id}', status_code=status.HTTP_200_OK, response_model=UserOut) -def get_user_by_id(id: int, response: Response): +@router.get('/get/by-id/{id}', status_code=status.HTTP_200_OK, response_model=UserOut, responses={ + 404: {"description": "User not found"}, + 500: {"description": "Internal server error"} + }) +def get_user_with_id(id: int, response: Response): user = get_user_by_id(id) if not user: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"user with {id} does not exist in Users") return user -@router.get('/get/by-email/{email}', status_code=status.HTTP_200_OK, response_model=Users) -def get_user_by_email(email: str, response: Response): +@router.get('/get/by-email/{email}', status_code=status.HTTP_200_OK, response_model=UserOut, responses={ + 404: {"description": "User not found"}, + 500: {"description": "Internal server error"} + }) +def get_user_with_email(email: str, response: Response): user = get_user_by_email(email) if not user: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"user with {email} does not exist in Users") return user -@router.post('/add', status_code=status.HTTP_201_CREATED, response_model=UserOut) -def create_user(user: Users)->UserOut: - try: +@router.post('/add', status_code=status.HTTP_201_CREATED, response_model=UserOut, responses={ + 500: {"description": "Internal server error"} + }) +def create_user(user: UserIn)->UserOut: + try: + user = Users(email=user.email, password=user.password) user = insert_data_in_users_table(user) - return user except Exception as e: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,detail=f"User NOT Created, Please Check Request {str(e.args)}") + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,detail=f"Error {str(e.args)}") + except HTTPException as e: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,detail=f"Error {str(e.detail)}") + return user -@router.put('/update/{id}', status_code=status.HTTP_200_OK, response_model=UserOut) -def update_user(id: int, user: Users, response: Response): +@router.put('/update/{id}', status_code=status.HTTP_200_OK, response_model=UserOut, responses={ + 404: {"description": "User not found"}, + 500: {"description": "Internal server error"} + }) +def update_user(id: int, user: UserIn, response: Response): + user = Users(email=user.email, password=user.password) user = update_user_by_id(id, user) if not user: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"user with {id} does not exist in Users") return user -@router.delete('/delete/{id}', status_code=status.HTTP_200_OK, response_model=UserOut) +@router.delete('/delete/{id}', status_code=status.HTTP_200_OK, response_model=UserOut, responses={ + 404: {"description": "User not found"}, + 500: {"description": "Internal server error"} + }) def delete_user(id: int, response: Response): user = delete_user_by_id(id) if not user: diff --git a/migrations/versions/6b78e5356a6f_test_migrations.py b/migrations/versions/6b78e5356a6f_test_migrations.py index c4aace1..bc4d7c1 100644 --- a/migrations/versions/6b78e5356a6f_test_migrations.py +++ b/migrations/versions/6b78e5356a6f_test_migrations.py @@ -30,7 +30,7 @@ def upgrade() -> None: ) op.create_table('uposts', sa.Column('id', sa.Integer(), nullable=False), - sa.Column('tittle', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('title', sqlmodel.sql.sqltypes.AutoString(), nullable=False), sa.Column('content', sqlmodel.sql.sqltypes.AutoString(), nullable=False), sa.Column('userid', sa.Integer(), nullable=False), sa.Column('create_dtm', sa.DateTime(), nullable=True), From f92629209fb23a63ab48c5ffb907246d6e3eb70a Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Sun, 12 Jan 2025 17:12:08 +0530 Subject: [PATCH 26/55] updated all routes --- app/main.py | 8 ++++---- app/routers/auth.py | 2 -- app/routers/posts.py | 10 +++++----- app/routers/users.py | 10 +++++----- 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/app/main.py b/app/main.py index d8fe7e1..4ebe84e 100644 --- a/app/main.py +++ b/app/main.py @@ -31,10 +31,10 @@ async def lifespan(app: FastAPI): ) -app.include_router(posts.router, prefix="/uposts", tags=["Posts"]) -app.include_router(users.router, prefix="/users", tags=["Users"]) -app.include_router(auth.router, prefix="/auth", tags=["Authentication"]) -app.include_router(votes.router, prefix="/vote", tags=["Votes"]) +app.include_router(posts.router, prefix="/uposts", tags=["post"]) +app.include_router(users.router, prefix="/users", tags=["user"]) +app.include_router(auth.router, prefix="/auth", tags=["authentication"]) +app.include_router(votes.router, prefix="/vote", tags=["votes"]) diff --git a/app/routers/auth.py b/app/routers/auth.py index ac0ddcd..e80ca92 100644 --- a/app/routers/auth.py +++ b/app/routers/auth.py @@ -9,8 +9,6 @@ @router.post("/login", status_code=status.HTTP_200_OK, response_model=Token) def login(user_credentials: OAuth2PasswordRequestForm = Depends()): - print(f"Username: {user_credentials.username}") - print(f"Password: {user_credentials.password}") user = get_user_by_email(user_credentials.username) if not user: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN,detail=f"User Not Found") diff --git a/app/routers/posts.py b/app/routers/posts.py index 84bf094..2f1f6f2 100644 --- a/app/routers/posts.py +++ b/app/routers/posts.py @@ -14,7 +14,7 @@ def get_list_of_post(current_user:TokenData = Depends(get_current_user), limit: posts = get_all_post(limit, skip, search) return posts -@router.get('/withcount', status_code=status.HTTP_200_OK, response_model=List[UPostswithCount], responses={ +@router.get('/count', status_code=status.HTTP_200_OK, response_model=List[UPostswithCount], responses={ 404: {"description": "Posts not found"}, 500: {"description": "Internal server error"} }) @@ -22,7 +22,7 @@ def get_list_of_post_with_count(current_user:TokenData = Depends(get_current_use posts = get_all_post_with_count(limit, skip, search) return posts -@router.get('/get/{id}', status_code=status.HTTP_200_OK, response_model=UPostOut, responses={ +@router.get('/{id}', status_code=status.HTTP_200_OK, response_model=UPostOut, responses={ 404: {"description": "Post not found"}, 500: {"description": "Internal server error"} }) @@ -34,7 +34,7 @@ def get_post(id: int, response: Response, current_user:TokenData = Depends(get_c raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=f"User not Authorized to get the post") return post -@router.post('/add', status_code=status.HTTP_201_CREATED, response_model=UPostOut, responses={ +@router.post('/', status_code=status.HTTP_201_CREATED, response_model=UPostOut, responses={ 500: {"description": "Internal server error"} }) def create_post(post: UPostCreate, current_user:TokenData = Depends(get_current_user)): @@ -43,7 +43,7 @@ def create_post(post: UPostCreate, current_user:TokenData = Depends(get_current_ post = insert_data_in_uposts_table(post) return post -@router.put('/update/{id}', status_code=status.HTTP_200_OK, response_model=UPostOut, responses={ +@router.put('/{id}', status_code=status.HTTP_200_OK, response_model=UPostOut, responses={ 404: {"description": "Post not found"}, 500: {"description": "Internal server error"} }) @@ -56,7 +56,7 @@ def update_post(id: int, post: UPostCreate, response: Response, current_user:Tok raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=f"User not Authorized to get the post") return post -@router.delete('/delete/{id}', status_code=status.HTTP_200_OK, response_model=UPostOut, responses={ +@router.delete('/{id}', status_code=status.HTTP_200_OK, response_model=UPostOut, responses={ 404: {"description": "Post not found"}, 500: {"description": "Internal server error"} }) diff --git a/app/routers/users.py b/app/routers/users.py index 0b6946f..3498b7e 100644 --- a/app/routers/users.py +++ b/app/routers/users.py @@ -13,7 +13,7 @@ def get_list_of_users()->List[UserOut]: users = get_all_user() return users -@router.get('/get/by-id/{id}', status_code=status.HTTP_200_OK, response_model=UserOut, responses={ +@router.get('/by-id/{id}', status_code=status.HTTP_200_OK, response_model=UserOut, responses={ 404: {"description": "User not found"}, 500: {"description": "Internal server error"} }) @@ -23,7 +23,7 @@ def get_user_with_id(id: int, response: Response): raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"user with {id} does not exist in Users") return user -@router.get('/get/by-email/{email}', status_code=status.HTTP_200_OK, response_model=UserOut, responses={ +@router.get('/by-email/{email}', status_code=status.HTTP_200_OK, response_model=UserOut, responses={ 404: {"description": "User not found"}, 500: {"description": "Internal server error"} }) @@ -33,7 +33,7 @@ def get_user_with_email(email: str, response: Response): raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"user with {email} does not exist in Users") return user -@router.post('/add', status_code=status.HTTP_201_CREATED, response_model=UserOut, responses={ +@router.post('/', status_code=status.HTTP_201_CREATED, response_model=UserOut, responses={ 500: {"description": "Internal server error"} }) def create_user(user: UserIn)->UserOut: @@ -46,7 +46,7 @@ def create_user(user: UserIn)->UserOut: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,detail=f"Error {str(e.detail)}") return user -@router.put('/update/{id}', status_code=status.HTTP_200_OK, response_model=UserOut, responses={ +@router.put('/{id}', status_code=status.HTTP_200_OK, response_model=UserOut, responses={ 404: {"description": "User not found"}, 500: {"description": "Internal server error"} }) @@ -57,7 +57,7 @@ def update_user(id: int, user: UserIn, response: Response): raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"user with {id} does not exist in Users") return user -@router.delete('/delete/{id}', status_code=status.HTTP_200_OK, response_model=UserOut, responses={ +@router.delete('/{id}', status_code=status.HTTP_200_OK, response_model=UserOut, responses={ 404: {"description": "User not found"}, 500: {"description": "Internal server error"} }) From dd435176ee07ae03b1e1634a660ffa15bc6d35f2 Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Sun, 12 Jan 2025 17:18:08 +0530 Subject: [PATCH 27/55] updated with route tag: --- app/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/main.py b/app/main.py index 4ebe84e..2b359b1 100644 --- a/app/main.py +++ b/app/main.py @@ -31,8 +31,8 @@ async def lifespan(app: FastAPI): ) -app.include_router(posts.router, prefix="/uposts", tags=["post"]) -app.include_router(users.router, prefix="/users", tags=["user"]) +app.include_router(posts.router, prefix="/uposts", tags=["uposts"]) +app.include_router(users.router, prefix="/users", tags=["users"]) app.include_router(auth.router, prefix="/auth", tags=["authentication"]) app.include_router(votes.router, prefix="/vote", tags=["votes"]) From f333640f1fb7ed73fee5a26fab9646855b498ddd Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Fri, 17 Jan 2025 17:18:09 +0530 Subject: [PATCH 28/55] added testcases and github action workflow --- .coverage | Bin 0 -> 53248 bytes .github/workflows/github-actions-demo.yml | 16 +++ Procfile | 2 - app/config.py | 1 + app/crud/posts_crud.py | 12 +- app/crud/users_crud.py | 15 +-- app/crud/votes_crud.py | 12 +- app/database.py | 23 +++- app/main.py | 1 + app/routers/auth.py | 6 +- app/routers/posts.py | 27 +++-- app/routers/users.py | 34 +++--- app/routers/votes.py | 12 +- tests/__init__.py | 0 tests/conftest.py | 36 ++++++ tests/test_app.py | 140 ++++++++++++++++++++++ 16 files changed, 274 insertions(+), 63 deletions(-) create mode 100644 .coverage create mode 100644 .github/workflows/github-actions-demo.yml delete mode 100644 Procfile create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_app.py diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..61b2261d5de63f150ef002cea9f1e946b452f27a GIT binary patch literal 53248 zcmeI4Ta4UR8OQDM%y@4084`shEFpd&n^pIkN(o7%rVFG})j}u)UWhXB%y@TAcE%Ii zvlpTyY>SA5#0xw@1LBQ08U(ylBz=IFa+9hG!b=E36;xbOaS0M^+46nI_RdU_jl5E- zw&~xRIT!nU=X~dPzH`oeGh=`HsVCjgv1UELWrxx!+>|JeyP$bex>|v-7Yrh0uB%W0T2LzcQ=96 zrz-l;&YkMh=R&)_;Do;2aQrAVKKiMnGsllw$7eqB zX59tHa@$R}VTW#e&I;$9q;xlMn!M4`Ioj&D;DnUtRLyOYROrl66&>Gg+5U=k+F2Qs z1>)91XE}^cP$iB#*X9z|-dKFp@|{`7ciIgnh_u-2Hb+aN&s-?!L)*8j7aiFr+i%dT zJkT3D(d)|Nvs6*T^P8-F-M8C~c_$dN?2R5aJkoPHjC$*Pt;x`{7TmV%W;<|0*K1qO zveW2>PV=rE9Ajw7LGb!}Lv@|v4xNf5O%b#8ZC zbwd96ZmYwCLlRNLp|_djX0o9dcQ*Cd++{3#{T&$21UZjrB-L%ZXS&YD;E!27$uZfD zwl`*RC&&UmRL&0_+^#BQmCkZ-W`RC$)$DHQ$$X6lzcx)libpGs^Z^q}M)EGpXmY9L*g@A%7Lt1{l8Hob zl}zSHe1*C*uIGnlhBxa@T&^}9Ckm^2Rv+52LtTw#1g}f{&JA>e&-A9iZ5u&)^*eSr zZ{4XopC-jea`~adJ2q3CRj5sKn1AMT8L9Hcosxcw7JYte11j^Gp2|-+E8~my0<9(; z+x2P6(i$A?02UW)+Em!Cd)-jlFK4BB^4`#kQPuNqbQCXa(_b0s|aA{lRZ7VpG zoV7;g?SNOIV8p6>-hyMdqrHJ79uKHZH?X7*ckW6@dUXUVT*utvh}0?BTzHv3PLnQ~ zUzBLI>~=HWuXr1dYNfTGCnvWPEg~dnjnJeFXo}ueXHsc=l~=5!{)rAT;aXdEGn(!^ zD19|9%{zBg7wtwLvqL9zTaLv=q%zX5mh8Z4_zqp9y>xU$yO$a8Xw{}UWG}hEALUke ztNPGG52>r&crr%oOf8^Iy=5nA$Jxw)k=%B%XD`QS!KF_-{dU6a`fzYIYJnHGXawsK zg``ycS*JA04!$bZ6nfzR0T2KI5C8!X009sH0T2KI5C8!Xc<=~lYDU%B|IdiuE8->v zI6wddKmY_l00ck)1V8`;KmY_l00iEV1S%PAmq`BMF;!Ky;hph+01iy-pO~I1QLi%M zRYkliZoMNr2&F*)1V8`;KmY_l00ck)1V8`;KmY_b2voFPYVtKerlbuQ<8J}@_y20; zn~L~_xFk-A5%Z?`1M`CUaq|PlpN;2?MdJ~}to)h^-~a&-009sH0T2KI5C8!X0D(7+ zz`?4ftQ(^TeVTIzf$-bQAaip())o zqCVYLPdAxI(j+)xYDy!GU^jFZsQi9IQ-U<*lZNQIAM>nWWs3^Zvupf-1P_+C0!2zC zP^L&}tHK3!8zr)h#84N$iOhZ*Fz z!l2^?Vb3sfDY;9Vs@2@K8`f&n=7DTV!DOIivm|&()0DGmI+zIdH9C<=X^k!0Z8J51V8`; zKmY_l00ck)1V8`;9%up@{awJ|-~UzdxjP+aZH7k_*VCDSv>)mVT zpTBXgQl|X-%U9o6dhXhVBjpn1-d9?G<@NKqFKjDPW>0bb%0F+t_`=eKrKKC!)k1;J zZ7clajmOG57uEMSuKe!oWI4~7{K*?v)?U-|Im(#1jL1@KW!J-{rP|ufYv&B!1TfUT2v&EvBk@x?LuPI_p{7d{z{88KxzZEZupNXG{AB!J}@6k5_-x1%S z0ysbb1V8`;KmY_l00ck)1V8`;KmY{pBLQ~l)oN8nLdeK8Wn>sKs#IiDmd?LgD#={2 zD5F9_M!GJed|pPmoQ$$r8EKk~GMOxW0U&?>SE~0>eN+VkAOHd&00JNY0w4eaAOHd& z00JQJwj;p5|HJ=(+YJj80s#;J0T2KI5C8!X009sH0T2Lz`${0X|Nk=o|Nr0O@AM3S zKZ!qx>vRvm%i>q!Me$Sdf_R?p1Gs!&8%1Rx00JNY0w4eaAOHd&00JNY0w4ea_avZI o>7o6a;K1a-;Gn`mnS&AsMGgua=p5uZ$Z?S6K;t080YCr$|7Fr8wEzGB literal 0 HcmV?d00001 diff --git a/.github/workflows/github-actions-demo.yml b/.github/workflows/github-actions-demo.yml new file mode 100644 index 0000000..39a57f7 --- /dev/null +++ b/.github/workflows/github-actions-demo.yml @@ -0,0 +1,16 @@ +name: python_social_media_backend +on: + push: + branches: + - "dev_fastapi" + pull_request: + branches: + - "dev_fastapi" +job: + fastapi_job: + runs-on: windows-latest + steps: + - name: pulling git repo + uses: actions/checkout@v4 + - name: Hi to the User + run: echo "Hello User" \ No newline at end of file diff --git a/Procfile b/Procfile deleted file mode 100644 index 087e818..0000000 --- a/Procfile +++ /dev/null @@ -1,2 +0,0 @@ - -web: uvicorn app.main:app --host=0.0.0.0 --port=${PORT:-5000} \ No newline at end of file diff --git a/app/config.py b/app/config.py index 2a041d5..1830bfc 100644 --- a/app/config.py +++ b/app/config.py @@ -7,6 +7,7 @@ class Settings(BaseSettings): database_password: str database_name: str database_name1: str + test_database_name: str database_username: str secret_key: str algorithm: str diff --git a/app/crud/posts_crud.py b/app/crud/posts_crud.py index 375a31d..75dc664 100644 --- a/app/crud/posts_crud.py +++ b/app/crud/posts_crud.py @@ -5,26 +5,26 @@ session = next(get_session()) -def insert_data_in_uposts_table(post: UPosts): +def insert_data_in_uposts_table(post: UPosts, session): session.add(post) session.commit() session.refresh(post) return post -def get_all_post(limit, skip, search): +def get_all_post(limit, skip, search, session): posts = session.exec(select(UPosts).filter(UPosts.title.contains(search)).limit(limit).offset(skip)).all() return posts -def get_all_post_with_count(limit, skip, search): +def get_all_post_with_count(limit, skip, search, session): posts = session.exec(select(UPosts, func.count(Votes.post_id).label("vote_count")).join(Votes, UPosts.id == Votes.post_id, isouter=True).group_by(UPosts.id).filter(UPosts.title.contains(search)).limit(limit).offset(skip)).all() posts_with_counts = [{"UPosts": post, "vote": vote_count} for post, vote_count in posts] return posts_with_counts -def get_post_by_id(id: int): +def get_post_by_id(id: int, session): post = session.get(UPosts, id) return post -def update_post_by_id(id: int, upost: UPosts): +def update_post_by_id(id: int, upost: UPosts, session): postTobeUpdated = session.exec(select(UPosts).where(UPosts.id == id)).first() if postTobeUpdated: upost.id = id @@ -34,7 +34,7 @@ def update_post_by_id(id: int, upost: UPosts): session.refresh(postTobeUpdated) return postTobeUpdated -def delete_post_by_id(id: int): +def delete_post_by_id(id: int, session): upost = session.exec(select(UPosts).join(Users).where(UPosts.id == id).options(joinedload(UPosts.owner))).first() if upost: session.delete(upost) diff --git a/app/crud/users_crud.py b/app/crud/users_crud.py index d9a553f..cc99190 100644 --- a/app/crud/users_crud.py +++ b/app/crud/users_crud.py @@ -4,10 +4,7 @@ from app.model import Users from sqlmodel import select -session = next(get_session()) - - -def insert_data_in_users_table(user: Users)->Users: +def insert_data_in_users_table(user: Users, session)->Users: try: user_dict = {**user.model_dump()} Users.validate_email_domain(user.email) @@ -20,25 +17,25 @@ def insert_data_in_users_table(user: Users)->Users: session.refresh(user) return user -def get_all_user()->Users: +def get_all_user(session)->Users: users = session.exec(select(Users)).all() return users -def get_user_by_id(id: int): +def get_user_by_id(id: int, session): user = session.get(Users, id) if user: return user else: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"User with {id} not found") -def get_user_by_email(email: str): +def get_user_by_email(email: str, session): user = session.exec(select(Users).where(Users.email == email)).first() if user: return user else: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"User Not found in the System") -def update_user_by_id(id: int, user: Users): +def update_user_by_id(id: int, user: Users, session): usertobeupdated = session.exec(select(Users).where(Users.userid == id)).first() if usertobeupdated: user.userid = id @@ -48,7 +45,7 @@ def update_user_by_id(id: int, user: Users): session.refresh(usertobeupdated) return usertobeupdated -def delete_user_by_id(id: int): +def delete_user_by_id(id: int, session): user = session.get(Users, id) if user: session.delete(user) diff --git a/app/crud/votes_crud.py b/app/crud/votes_crud.py index a51056b..80122d7 100644 --- a/app/crud/votes_crud.py +++ b/app/crud/votes_crud.py @@ -4,22 +4,20 @@ from app.model import Vote, Votes from sqlmodel import select -session = next(get_session()) - -def is_vote_exist(vote: Vote, userid): +def is_vote_exist(vote: Vote, userid, session): vote_found = session.exec(select(Votes).filter(Votes.post_id == vote.post_id, Votes.user_id == userid)).first() return vote_found -def add_vote(vote: Vote, userid): +def add_vote(vote: Vote, userid, session): vote = Votes(post_id=vote.post_id, user_id=userid) session.add(vote) session.commit() - return {"msg":"Added Vote"} + return {"Message":"Added Vote"} -def delete_vote(vote: Vote, userid): +def delete_vote(vote: Vote, userid, session): vote = session.exec(select(Votes).filter(Votes.post_id == vote.post_id, Votes.user_id == userid)).first() session.delete(vote) session.commit() - return {"msg":"Deleted Vote"} + return {"Message":"Deleted Vote"} diff --git a/app/database.py b/app/database.py index a816c22..92c2503 100644 --- a/app/database.py +++ b/app/database.py @@ -2,22 +2,38 @@ from .config import settings POSTGRESS_SQL_DATABASE_URL = f"postgresql://{settings.database_username}:{settings.database_password}@{settings.database_hostname}/{settings.database_name1}" +TEST_POSTGRESS_SQL_DATABASE_URL = f"postgresql://{settings.database_username}:{settings.database_password}@{settings.database_hostname}/{settings.test_database_name}" # add echo = true if you like to log create query -# engine = create_engine(POSTGRESS_SQL_DATABASE_URL) -engine = create_engine(f"{settings.database_string}", echo = True) +SQL_DATABASE_URL = POSTGRESS_SQL_DATABASE_URL +engine = create_engine(SQL_DATABASE_URL) +# engine = create_engine(f"{settings.database_string}", echo = True) +def set_test_database(): + print("Setting Up test databse") + global engine + # engine = create_engine(f"{settings.test_database_string}", echo = True) + engine = create_engine(TEST_POSTGRESS_SQL_DATABASE_URL) +def get_override_session(): + with Session(engine) as session: + try: + yield session + finally: + session.close() + def create_database_and_tables(): - print(POSTGRESS_SQL_DATABASE_URL) SQLModel.metadata.create_all(engine) + print("database Created") return "Database Connected" def close_connection(): + print("Database Closed") engine.dispose() return "Database Disconnected" def drop_database_and_tables(): + print("Database Dropped") SQLModel.metadata.drop_all(engine) return "Database Droped" @@ -27,4 +43,3 @@ def get_session() -> Session: # type: ignore yield session finally: session.close() - diff --git a/app/main.py b/app/main.py index 2b359b1..ec9919d 100644 --- a/app/main.py +++ b/app/main.py @@ -18,6 +18,7 @@ async def lifespan(app: FastAPI): app.state.db = drop_database_and_tables() print(app.state.db) + app = FastAPI(lifespan=lifespan) origins = ["*"] diff --git a/app/routers/auth.py b/app/routers/auth.py index e80ca92..94d4609 100644 --- a/app/routers/auth.py +++ b/app/routers/auth.py @@ -4,12 +4,14 @@ from app.crud.users_crud import get_user_by_email from app.util import * from app.oauth2 import create_jwt_token +from app.database import get_session +from sqlmodel import Session router = APIRouter() @router.post("/login", status_code=status.HTTP_200_OK, response_model=Token) -def login(user_credentials: OAuth2PasswordRequestForm = Depends()): - user = get_user_by_email(user_credentials.username) +def login(user_credentials: OAuth2PasswordRequestForm = Depends(), session: Session = Depends(get_session) ): + user = get_user_by_email(user_credentials.username, session) if not user: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN,detail=f"User Not Found") access_token = create_jwt_token(data = {"email": user.email, "userid": user.userid}) diff --git a/app/routers/posts.py b/app/routers/posts.py index 2f1f6f2..d221d71 100644 --- a/app/routers/posts.py +++ b/app/routers/posts.py @@ -3,6 +3,9 @@ from app.crud.posts_crud import * from app.model import UPostOut, TokenData, UPostswithCount, UPostCreate from app.oauth2 import get_current_user +from sqlmodel import Session +from app.database import get_session + router = APIRouter() @@ -10,24 +13,24 @@ 404: {"description": "Posts not found"}, 500: {"description": "Internal server error"} }) -def get_list_of_post(current_user:TokenData = Depends(get_current_user), limit: int = 5, skip: int = 0, search: Optional[str] = "")->List[UPostOut]: - posts = get_all_post(limit, skip, search) +def get_list_of_post(current_user:TokenData = Depends(get_current_user), session: Session = Depends(get_session), limit: int = 5, skip: int = 0, search: Optional[str] = "")->List[UPostOut]: + posts = get_all_post(limit, skip, search, session) return posts @router.get('/count', status_code=status.HTTP_200_OK, response_model=List[UPostswithCount], responses={ 404: {"description": "Posts not found"}, 500: {"description": "Internal server error"} }) -def get_list_of_post_with_count(current_user:TokenData = Depends(get_current_user), limit: int = 5, skip: int = 0, search: Optional[str] = "")->List[UPostswithCount]: - posts = get_all_post_with_count(limit, skip, search) +def get_list_of_post_with_count(current_user:TokenData = Depends(get_current_user),session: Session = Depends(get_session) , limit: int = 5, skip: int = 0, search: Optional[str] = "")->List[UPostswithCount]: + posts = get_all_post_with_count(limit, skip, search, session) return posts @router.get('/{id}', status_code=status.HTTP_200_OK, response_model=UPostOut, responses={ 404: {"description": "Post not found"}, 500: {"description": "Internal server error"} }) -def get_post(id: int, response: Response, current_user:TokenData = Depends(get_current_user)): - post = get_post_by_id(id) +def get_post(id: int, response: Response, current_user:TokenData = Depends(get_current_user), session: Session = Depends(get_session)): + post = get_post_by_id(id, session) if not post: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"post with {id} does not exist in UPosts") if current_user.id != post.userid: @@ -37,19 +40,19 @@ def get_post(id: int, response: Response, current_user:TokenData = Depends(get_c @router.post('/', status_code=status.HTTP_201_CREATED, response_model=UPostOut, responses={ 500: {"description": "Internal server error"} }) -def create_post(post: UPostCreate, current_user:TokenData = Depends(get_current_user)): +def create_post(post: UPostCreate, current_user:TokenData = Depends(get_current_user), session: Session = Depends(get_session)): post = UPosts(title=post.title, content=post.content) post.userid = current_user.id - post = insert_data_in_uposts_table(post) + post = insert_data_in_uposts_table(post, session) return post @router.put('/{id}', status_code=status.HTTP_200_OK, response_model=UPostOut, responses={ 404: {"description": "Post not found"}, 500: {"description": "Internal server error"} }) -def update_post(id: int, post: UPostCreate, response: Response, current_user:TokenData = Depends(get_current_user)): +def update_post(id: int, post: UPostCreate, response: Response, current_user:TokenData = Depends(get_current_user), session: Session = Depends(get_session)): post = UPosts(title=post.title, content=post.content) - post = update_post_by_id(id, post) + post = update_post_by_id(id, post, session) if not post: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"post with {id} does not exist in UPosts") if current_user.id != post.userid: @@ -60,8 +63,8 @@ def update_post(id: int, post: UPostCreate, response: Response, current_user:Tok 404: {"description": "Post not found"}, 500: {"description": "Internal server error"} }) -def delete_post(id: int, response: Response, current_user:TokenData = Depends(get_current_user)): - post = delete_post_by_id(id) +def delete_post(id: int, response: Response, current_user:TokenData = Depends(get_current_user), session: Session = Depends(get_session)): + post = delete_post_by_id(id, session) if not post: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"post with {id} does not exist in UPosts") if current_user.id != post.userid: diff --git a/app/routers/users.py b/app/routers/users.py index 3498b7e..61b846f 100644 --- a/app/routers/users.py +++ b/app/routers/users.py @@ -1,7 +1,9 @@ from typing import List -from fastapi import APIRouter, status, HTTPException, Response +from fastapi import APIRouter, status, HTTPException, Response, Depends from app.crud.users_crud import * from app.model import UserOut, UserIn +from app.database import get_session +from sqlmodel import Session router = APIRouter() @@ -9,16 +11,16 @@ 404: {"description": "User not found"}, 500: {"description": "Internal server error"} }) -def get_list_of_users()->List[UserOut]: - users = get_all_user() +def get_list_of_users(session: Session = Depends(get_session))->List[UserOut]: + users = get_all_user(session) return users @router.get('/by-id/{id}', status_code=status.HTTP_200_OK, response_model=UserOut, responses={ 404: {"description": "User not found"}, 500: {"description": "Internal server error"} }) -def get_user_with_id(id: int, response: Response): - user = get_user_by_id(id) +def get_user_with_id(id: int, response: Response, session: Session = Depends(get_session)): + user = get_user_by_id(id, session) if not user: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"user with {id} does not exist in Users") return user @@ -27,8 +29,8 @@ def get_user_with_id(id: int, response: Response): 404: {"description": "User not found"}, 500: {"description": "Internal server error"} }) -def get_user_with_email(email: str, response: Response): - user = get_user_by_email(email) +def get_user_with_email(email: str, response: Response, session: Session = Depends(get_session)): + user = get_user_by_email(email, session) if not user: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"user with {email} does not exist in Users") return user @@ -36,23 +38,23 @@ def get_user_with_email(email: str, response: Response): @router.post('/', status_code=status.HTTP_201_CREATED, response_model=UserOut, responses={ 500: {"description": "Internal server error"} }) -def create_user(user: UserIn)->UserOut: +def create_user(user: UserIn, session: Session = Depends(get_session))->UserOut: try: user = Users(email=user.email, password=user.password) - user = insert_data_in_users_table(user) - except Exception as e: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,detail=f"Error {str(e.args)}") + user = insert_data_in_users_table(user, session) except HTTPException as e: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,detail=f"Error {str(e.detail)}") + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,detail=f"Validation Error {e}") + except Exception as e: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,detail=f"Error {str(e.args)}") return user @router.put('/{id}', status_code=status.HTTP_200_OK, response_model=UserOut, responses={ 404: {"description": "User not found"}, 500: {"description": "Internal server error"} }) -def update_user(id: int, user: UserIn, response: Response): +def update_user(id: int, user: UserIn, response: Response, session: Session = Depends(get_session)): user = Users(email=user.email, password=user.password) - user = update_user_by_id(id, user) + user = update_user_by_id(id, user, session) if not user: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"user with {id} does not exist in Users") return user @@ -61,8 +63,8 @@ def update_user(id: int, user: UserIn, response: Response): 404: {"description": "User not found"}, 500: {"description": "Internal server error"} }) -def delete_user(id: int, response: Response): - user = delete_user_by_id(id) +def delete_user(id: int, response: Response, session: Session = Depends(get_session)): + user = delete_user_by_id(id, session) if not user: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"user with {id} does not exist in Users") return user diff --git a/app/routers/votes.py b/app/routers/votes.py index aebf250..65992c3 100644 --- a/app/routers/votes.py +++ b/app/routers/votes.py @@ -3,18 +3,20 @@ from app.crud.votes_crud import * from app.model import Vote, TokenData from app.oauth2 import get_current_user +from sqlmodel import Session +from app.database import get_session router = APIRouter() @router.post("/", status_code=status.HTTP_201_CREATED) -def vote(vote: Vote, current_user:TokenData = Depends(get_current_user)): - vote_found = is_vote_exist(vote, current_user.id) +def vote(vote: Vote, current_user:TokenData = Depends(get_current_user), session: Session = Depends(get_session)): + vote_found = is_vote_exist(vote, current_user.id, session) if(vote.dir.value == "UPVOTE"): if vote_found: raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=f"User {current_user.id} has Already voted on Post with id {vote.post_id}") - return add_vote(vote, current_user.id) + return add_vote(vote, current_user.id, session) else: if not vote_found: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Us~er {current_user.id} has not voted on Post with id {vote.post_id}") - return delete_vote(vote, current_user.id) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"User {current_user.id} has not voted on Post with id {vote.post_id}") + return delete_vote(vote, current_user.id, session) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..7ee799c --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,36 @@ +import pytest +from fastapi.testclient import TestClient +from app.main import app +from app.model import UserIn +from app.database import create_database_and_tables, drop_database_and_tables, get_session, close_connection, get_override_session, set_test_database +from app.oauth2 import create_jwt_token + +@pytest.fixture(scope="module") +def session(): + set_test_database() + drop_database_and_tables() + create_database_and_tables() + try: + yield get_override_session() + finally: + close_connection() + +@pytest.fixture(scope="module") +def client(session): + app.dependency_overrides[get_session] = get_override_session + yield TestClient(app) + +@pytest.fixture(scope="module") +def create_user(client): + user_data = UserIn(email="user@gmail.com", password="user") + response = client.post("/users/", json=user_data.model_dump()) + assert response.status_code == 201, f"Failed to create user: {response.json()}" + +@pytest.fixture(scope="module") +def create_valid_login_token(): + token = create_jwt_token(data={"email":"user1@yahoo.com", "userid": 2}) + return token + + + + diff --git a/tests/test_app.py b/tests/test_app.py new file mode 100644 index 0000000..d5c5079 --- /dev/null +++ b/tests/test_app.py @@ -0,0 +1,140 @@ +from app.model import UserOut, UserIn, Token, UPostCreate, UPostOut, Vote, UPostswithCount + +def test_root(client): + response = client.get("/") + assert response.status_code == 200 + assert response.json() == {"Message": "Welcome to FastAPI, My First Web APP"} + +def test_login_user(client, create_user): + response = client.post("/auth/login", data={"username": "user@gmail.com", "password": "user"}) + login_response = Token(**response.json()) + assert response.status_code == 200, f"Login Failed: {response.json()}" + +def test_create_user(client): + response = client.post("/users/",json=UserIn(email="user1@yahoo.com", password="user1").model_dump()) + UserOut(**response.json()) + assert response.status_code == 201 + +def test_get_user_by_id(client): + response = client.get("/users/by-id/1") + user = UserOut(**response.json()) + assert user.userid == 1 + assert user.email == "user@gmail.com" + +def test_get_user_not_exist_by_id(client): + response = client.get("/users/by-id/9999999999") + assert response.status_code == 404 + +def test_get_user_by_email(client): + response = client.get("/users/by-email/user@gmail.com") + user = UserOut(**response.json()) + assert user.userid == 1 + assert user.email == "user@gmail.com" + +def test_get_user_not_exist_by_email(client): + response = client.get("/users/by-email/user546745@gmail.com") + assert response.status_code == 404 + +def test_create_duplicate_user(client): + response = client.post("/users/", json=UserIn(email="user@gmail.com", password="test1").model_dump()) + assert response.json() == {'detail': 'Validation Error 406: Failed to Create User : Validation'} + assert response.status_code == 400 + +def test_update_user(client): + response = client.put("/users/1",json=UserIn(email="updateduser@yahoo.com", password="updateduser").model_dump()) + UserOut(**response.json()) + assert response.status_code == 200 + +def test_update_non_existing_user(client): + response = client.put("/users/9999999999",json=UserIn(email="updateduser@yahoo.com", password="updateduser").model_dump()) + assert response.status_code == 404 + +def test_delete_user(client): + response = client.delete("/users/1") + UserOut(**response.json()) + assert response.status_code == 200 + +def test_delete_non_existing_user(client): + response = client.delete("/users/9999999999") + assert response.status_code == 404 + +def test_list_of_users(client): + client.post("/users/",json=UserIn(email="test100@yahoo.com", password="test").model_dump()) + client.post("/users/",json=UserIn(email="test101@yahoo.com", password="test").model_dump()) + client.post("/users/",json=UserIn(email="test102@yahoo.com", password="test").model_dump()) + client.post("/users/",json=UserIn(email="test103@yahoo.com", password="test").model_dump()) + response = client.get("/users/") + users = [UserOut(**userdata) for userdata in response.json()] + assert len(users) == 5 + +def test_create_post(client, create_valid_login_token): + response = client.post("/uposts/", json=UPostCreate(title="my test post 1", content="my test content 1").model_dump(), headers={"Authorization": f"Bearer {create_valid_login_token}"} ) + UPostOut(**response.json()) + assert response.status_code == 201 + +def test_get_post(client, create_valid_login_token): + response = client.get("/uposts/1", headers={"Authorization": f"Bearer {create_valid_login_token}"} ) + UPostOut(**response.json()) + assert response.status_code == 200 + +def test_get_non_existing_post(client, create_valid_login_token): + response = client.get("/uposts/9999999999", headers={"Authorization": f"Bearer {create_valid_login_token}"} ) + assert response.status_code == 404 + +def test_update_post(client, create_valid_login_token): + response = client.put("/uposts/1", json=UPostCreate(title="my test updated post 1", content="my test udpated content 1").model_dump(), headers={"Authorization": f"Bearer {create_valid_login_token}"} ) + UPostOut(**response.json()) + assert response.status_code == 200 + +def test_update_non_existing_post(client, create_valid_login_token): + response = client.put("/uposts/9999999999", json=UPostCreate(title="my test updated post 1", content="my test udpated content 1").model_dump(), headers={"Authorization": f"Bearer {create_valid_login_token}"} ) + assert response.status_code == 404 + +def test_delete_post(client, create_valid_login_token): + response = client.delete("/uposts/1", headers={"Authorization": f"Bearer {create_valid_login_token}"} ) + assert response.status_code == 200 + +def test_delete_non_existing_post(client, create_valid_login_token): + response = client.delete("/uposts/9999999999", headers={"Authorization": f"Bearer {create_valid_login_token}"} ) + assert response.status_code == 404 + +def test_list_of_posts(client, create_valid_login_token): + response = client.post("/uposts/", json=UPostCreate(title="my test post 1", content="my test content 1").model_dump(), headers={"Authorization": f"Bearer {create_valid_login_token}"} ) + response = client.post("/uposts/", json=UPostCreate(title="my test post 2", content="my test content 2").model_dump(), headers={"Authorization": f"Bearer {create_valid_login_token}"} ) + response = client.post("/uposts/", json=UPostCreate(title="my test post 3", content="my test content 3").model_dump(), headers={"Authorization": f"Bearer {create_valid_login_token}"} ) + response = client.post("/uposts/", json=UPostCreate(title="my test post 4", content="my test content 4").model_dump(), headers={"Authorization": f"Bearer {create_valid_login_token}"} ) + response = client.post("/uposts/", json=UPostCreate(title="my test post 5", content="my test content 5").model_dump(), headers={"Authorization": f"Bearer {create_valid_login_token}"} ) + response = client.get("/uposts/", headers={"Authorization": f"Bearer {create_valid_login_token}"}) + post_response = response.json() + posts = [UPostOut(**post) for post in post_response] + assert len(posts) == 5 + +def test_upvote(client, create_valid_login_token): + response = client.post("/vote/", json= Vote(post_id=5, dir="UPVOTE").model_dump(), headers={"Authorization": f"Bearer {create_valid_login_token}"}) + assert response.json() == {"Message":"Added Vote"} + assert response.status_code == 201 + +def test_upvote_already_provided(client, create_valid_login_token): + response = client.post("/vote/", json= Vote(post_id=5, dir="UPVOTE").model_dump(), headers={"Authorization": f"Bearer {create_valid_login_token}"}) + assert response.json() == {'detail': 'User 2 has Already voted on Post with id 5'} + assert response.status_code == 409 + +def test_downvote(client, create_valid_login_token): + response = client.post("/vote/", json= Vote(post_id=5, dir="DOWNVOTE").model_dump(), headers={"Authorization": f"Bearer {create_valid_login_token}"}) + assert response.json() == {"Message":"Deleted Vote"} + assert response.status_code == 201 + +def test_downvote_with_no_upvote(client, create_valid_login_token): + response = client.post("/vote/", json= Vote(post_id=4, dir="DOWNVOTE").model_dump(), headers={"Authorization": f"Bearer {create_valid_login_token}"}) + assert response.json() == {'detail': 'User 2 has not voted on Post with id 4'} + assert response.status_code == 404 + +def test_list_of_posts_with_count(client, create_valid_login_token): + response = client.get("/uposts/count", headers={"Authorization": f"Bearer {create_valid_login_token}"} ) + posts = [UPostswithCount(**post) for post in response.json()] + assert len(posts) == 5 + + + + + From 0f8ae3f9d7a6a48f89af1486c5bdc2fabe28b490 Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Fri, 17 Jan 2025 17:19:58 +0530 Subject: [PATCH 29/55] fixing minor --- .github/workflows/github-actions-demo.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github-actions-demo.yml b/.github/workflows/github-actions-demo.yml index 39a57f7..9e1ca29 100644 --- a/.github/workflows/github-actions-demo.yml +++ b/.github/workflows/github-actions-demo.yml @@ -6,7 +6,7 @@ on: pull_request: branches: - "dev_fastapi" -job: +jobs: fastapi_job: runs-on: windows-latest steps: From 3e47769eccefb2b5f2eb2b3d695c626f7d76e6aa Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Fri, 17 Jan 2025 17:31:34 +0530 Subject: [PATCH 30/55] added more steps to workflow --- .github/workflows/github-actions-demo.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/github-actions-demo.yml b/.github/workflows/github-actions-demo.yml index 9e1ca29..2a52b89 100644 --- a/.github/workflows/github-actions-demo.yml +++ b/.github/workflows/github-actions-demo.yml @@ -10,7 +10,15 @@ jobs: fastapi_job: runs-on: windows-latest steps: + - name: Hello User + run: Hello, ${{github.actor}} - name: pulling git repo uses: actions/checkout@v4 - - name: Hi to the User - run: echo "Hello User" \ No newline at end of file + - name: install python version 3.10.0 + uses: actions/setup-python@v5 + with: + python-version: '3.10.0' + - name: update pip + run: python -m pip install --upgrade pip + - name: installing dependencies + run: pip install -r requirements.txt \ No newline at end of file From 2016e847a9978bd13b8b25fd3f591f588c8721dd Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Fri, 17 Jan 2025 17:32:54 +0530 Subject: [PATCH 31/55] fixing minor in steps --- .github/workflows/github-actions-demo.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github-actions-demo.yml b/.github/workflows/github-actions-demo.yml index 2a52b89..724c1b3 100644 --- a/.github/workflows/github-actions-demo.yml +++ b/.github/workflows/github-actions-demo.yml @@ -11,7 +11,7 @@ jobs: runs-on: windows-latest steps: - name: Hello User - run: Hello, ${{github.actor}} + run: Hello ${{github.actor}} - name: pulling git repo uses: actions/checkout@v4 - name: install python version 3.10.0 From 9dee3f2856c284149049635840453007acd2f63e Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Fri, 17 Jan 2025 17:34:24 +0530 Subject: [PATCH 32/55] fixing minor in steps --- .github/workflows/github-actions-demo.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github-actions-demo.yml b/.github/workflows/github-actions-demo.yml index 724c1b3..8558a54 100644 --- a/.github/workflows/github-actions-demo.yml +++ b/.github/workflows/github-actions-demo.yml @@ -11,7 +11,7 @@ jobs: runs-on: windows-latest steps: - name: Hello User - run: Hello ${{github.actor}} + run-name: Hello ${{github.actor}} - name: pulling git repo uses: actions/checkout@v4 - name: install python version 3.10.0 From c8943fb26e796dcb7bd1288059df57bd2728348c Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Fri, 17 Jan 2025 17:35:51 +0530 Subject: [PATCH 33/55] fixing minor in steps --- .github/workflows/github-actions-demo.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github-actions-demo.yml b/.github/workflows/github-actions-demo.yml index 8558a54..1f893f6 100644 --- a/.github/workflows/github-actions-demo.yml +++ b/.github/workflows/github-actions-demo.yml @@ -11,7 +11,7 @@ jobs: runs-on: windows-latest steps: - name: Hello User - run-name: Hello ${{github.actor}} + run: echo Hello ${{github.actor}} - name: pulling git repo uses: actions/checkout@v4 - name: install python version 3.10.0 From 96117a7f91f3d4d5dc4b449488cac0cee64aa470 Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Fri, 17 Jan 2025 17:41:58 +0530 Subject: [PATCH 34/55] adding tests with pytest --- .github/workflows/github-actions-demo.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/github-actions-demo.yml b/.github/workflows/github-actions-demo.yml index 1f893f6..1e9adfb 100644 --- a/.github/workflows/github-actions-demo.yml +++ b/.github/workflows/github-actions-demo.yml @@ -21,4 +21,9 @@ jobs: - name: update pip run: python -m pip install --upgrade pip - name: installing dependencies - run: pip install -r requirements.txt \ No newline at end of file + run: pip install -r requirements.txt + - name: test wih pytest + run: | + pip install pytest + pytest + pytest --cov=app \ No newline at end of file From 565519fdd13b398f9edc15b38fb5df3b50938603 Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Fri, 17 Jan 2025 18:07:43 +0530 Subject: [PATCH 35/55] added env variables --- .github/workflows/github-actions-demo.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/github-actions-demo.yml b/.github/workflows/github-actions-demo.yml index 1e9adfb..5c25777 100644 --- a/.github/workflows/github-actions-demo.yml +++ b/.github/workflows/github-actions-demo.yml @@ -8,6 +8,19 @@ on: - "dev_fastapi" jobs: fastapi_job: + environment: + name: testing + env: + DATABASE_HOSTNAME={{secrets.DATABASE_HOSTNAME}} + DATABASE_PORT={{secrets.DATABASE_PORT}} + DATABASE_PASSWORD={{secrets.DATABASE_PASSWORD}} + DATABASE_NAME={{secrets.DATABASE_NAME}} + TEST_DATABASE_NAME={{secrets.TEST_DATABASE_NAME}} + DATABASE_NAME1={{secrets.DATABASE_NAME1}} + DATABASE_USERNAME={{secrets.DATABASE_USERNAME}} + SECRET_KEY={{secrets.SECRET_KEY}} + ALGORITHM={{secrets.ALGORITHM}} + ACCESS_TOKEN_EXPIRE_MINUTES={{secrets.ACCESS_TOKEN_EXPIRE_MINUTES}} runs-on: windows-latest steps: - name: Hello User From 37cbb1e201509407c58b6a5049ae798594318e8a Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Fri, 17 Jan 2025 18:09:28 +0530 Subject: [PATCH 36/55] added $ symbol minor fixes --- .github/workflows/github-actions-demo.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/github-actions-demo.yml b/.github/workflows/github-actions-demo.yml index 5c25777..9180f3c 100644 --- a/.github/workflows/github-actions-demo.yml +++ b/.github/workflows/github-actions-demo.yml @@ -11,16 +11,16 @@ jobs: environment: name: testing env: - DATABASE_HOSTNAME={{secrets.DATABASE_HOSTNAME}} - DATABASE_PORT={{secrets.DATABASE_PORT}} - DATABASE_PASSWORD={{secrets.DATABASE_PASSWORD}} - DATABASE_NAME={{secrets.DATABASE_NAME}} - TEST_DATABASE_NAME={{secrets.TEST_DATABASE_NAME}} - DATABASE_NAME1={{secrets.DATABASE_NAME1}} - DATABASE_USERNAME={{secrets.DATABASE_USERNAME}} - SECRET_KEY={{secrets.SECRET_KEY}} - ALGORITHM={{secrets.ALGORITHM}} - ACCESS_TOKEN_EXPIRE_MINUTES={{secrets.ACCESS_TOKEN_EXPIRE_MINUTES}} + DATABASE_HOSTNAME=${{secrets.DATABASE_HOSTNAME}} + DATABASE_PORT=${{secrets.DATABASE_PORT}} + DATABASE_PASSWORD=${{secrets.DATABASE_PASSWORD}} + DATABASE_NAME=${{secrets.DATABASE_NAME}} + TEST_DATABASE_NAME=${{secrets.TEST_DATABASE_NAME}} + DATABASE_NAME1=${{secrets.DATABASE_NAME1}} + DATABASE_USERNAME=${{secrets.DATABASE_USERNAME}} + SECRET_KEY=${{secrets.SECRET_KEY}} + ALGORITHM=${{secrets.ALGORITHM}} + ACCESS_TOKEN_EXPIRE_MINUTES=${{secrets.ACCESS_TOKEN_EXPIRE_MINUTES}} runs-on: windows-latest steps: - name: Hello User From c30eb6d7e21692729a95d20a40292415a59902ff Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Fri, 17 Jan 2025 18:13:26 +0530 Subject: [PATCH 37/55] added env variables --- .github/workflows/github-actions-demo.yml | 46 ++++++++++++----------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/.github/workflows/github-actions-demo.yml b/.github/workflows/github-actions-demo.yml index 9180f3c..06a7865 100644 --- a/.github/workflows/github-actions-demo.yml +++ b/.github/workflows/github-actions-demo.yml @@ -8,35 +8,39 @@ on: - "dev_fastapi" jobs: fastapi_job: + runs-on: windows-latest environment: name: testing - env: - DATABASE_HOSTNAME=${{secrets.DATABASE_HOSTNAME}} - DATABASE_PORT=${{secrets.DATABASE_PORT}} - DATABASE_PASSWORD=${{secrets.DATABASE_PASSWORD}} - DATABASE_NAME=${{secrets.DATABASE_NAME}} - TEST_DATABASE_NAME=${{secrets.TEST_DATABASE_NAME}} - DATABASE_NAME1=${{secrets.DATABASE_NAME1}} - DATABASE_USERNAME=${{secrets.DATABASE_USERNAME}} - SECRET_KEY=${{secrets.SECRET_KEY}} - ALGORITHM=${{secrets.ALGORITHM}} - ACCESS_TOKEN_EXPIRE_MINUTES=${{secrets.ACCESS_TOKEN_EXPIRE_MINUTES}} - runs-on: windows-latest steps: - name: Hello User - run: echo Hello ${{github.actor}} - - name: pulling git repo + run: echo Hello ${{ github.actor }} + + - name: Pulling Git Repository uses: actions/checkout@v4 - - name: install python version 3.10.0 + + - name: Set Up Python 3.10 uses: actions/setup-python@v5 with: python-version: '3.10.0' - - name: update pip + + - name: Upgrade Pip run: python -m pip install --upgrade pip - - name: installing dependencies + + - name: Install Dependencies run: pip install -r requirements.txt - - name: test wih pytest + + - name: Run Pytest with Coverage + env: + DATABASE_HOSTNAME: ${{ secrets.DATABASE_HOSTNAME }} + DATABASE_PORT: ${{ secrets.DATABASE_PORT }} + DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }} + DATABASE_NAME: ${{ secrets.DATABASE_NAME }} + TEST_DATABASE_NAME: ${{ secrets.TEST_DATABASE_NAME }} + DATABASE_NAME1: ${{ secrets.DATABASE_NAME1 }} + DATABASE_USERNAME: ${{ secrets.DATABASE_USERNAME }} + SECRET_KEY: ${{ secrets.SECRET_KEY }} + ALGORITHM: ${{ secrets.ALGORITHM }} + ACCESS_TOKEN_EXPIRE_MINUTES: ${{ secrets.ACCESS_TOKEN_EXPIRE_MINUTES }} run: | - pip install pytest - pytest - pytest --cov=app \ No newline at end of file + pytest + pytest --cov=app \ No newline at end of file From d4ab1e48c22b630809e42294dce12d3b3cd37d85 Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Fri, 17 Jan 2025 18:17:07 +0530 Subject: [PATCH 38/55] added pytest install in workflow steps --- .github/workflows/github-actions-demo.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/github-actions-demo.yml b/.github/workflows/github-actions-demo.yml index 06a7865..0e1f5eb 100644 --- a/.github/workflows/github-actions-demo.yml +++ b/.github/workflows/github-actions-demo.yml @@ -42,5 +42,7 @@ jobs: ALGORITHM: ${{ secrets.ALGORITHM }} ACCESS_TOKEN_EXPIRE_MINUTES: ${{ secrets.ACCESS_TOKEN_EXPIRE_MINUTES }} run: | + pip install pytest + pip install pytest-cov pytest pytest --cov=app \ No newline at end of file From c0221a6c234f8c0ff8181828424e3705f13b960e Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Fri, 17 Jan 2025 18:39:30 +0530 Subject: [PATCH 39/55] adding database string env variable --- .github/workflows/github-actions-demo.yml | 1 + migrations/env.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/github-actions-demo.yml b/.github/workflows/github-actions-demo.yml index 0e1f5eb..9c658d0 100644 --- a/.github/workflows/github-actions-demo.yml +++ b/.github/workflows/github-actions-demo.yml @@ -41,6 +41,7 @@ jobs: SECRET_KEY: ${{ secrets.SECRET_KEY }} ALGORITHM: ${{ secrets.ALGORITHM }} ACCESS_TOKEN_EXPIRE_MINUTES: ${{ secrets.ACCESS_TOKEN_EXPIRE_MINUTES }} + DATABASE_STRING: ${{ secrets.DATABASE_STRING }} run: | pip install pytest pip install pytest-cov diff --git a/migrations/env.py b/migrations/env.py index d32e861..229b885 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -10,8 +10,8 @@ # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config -# config.set_main_option("sqlalchemy.url",f'postgresql+psycopg2://{settings.database_username}:{settings.database_password}@{settings.database_hostname}/{settings.database_name1}') -config.set_main_option("sqlalchemy.url",f'{settings.database_string}') +config.set_main_option("sqlalchemy.url",f'postgresql+psycopg2://{settings.database_username}:{settings.database_password}@{settings.database_hostname}/{settings.database_name}') +# config.set_main_option("sqlalchemy.url",f'{settings.database_string}') # Interpret the config file for Python logging. From bd14653776e3524d48a3b3b14f4bbb3a17d83295 Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Fri, 17 Jan 2025 20:15:05 +0530 Subject: [PATCH 40/55] added service postgres --- .github/workflows/github-actions-demo.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/github-actions-demo.yml b/.github/workflows/github-actions-demo.yml index 9c658d0..2ec1309 100644 --- a/.github/workflows/github-actions-demo.yml +++ b/.github/workflows/github-actions-demo.yml @@ -11,6 +11,19 @@ jobs: runs-on: windows-latest environment: name: testing + services: + postgres: + image: postgres + env: + POSTGRES_PASSWORD: ${{ secrets.DATABASE_PASSWORD }} + POSTGRES_DB: ${{ secrets.TEST_DATABASE_NAME }} + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 steps: - name: Hello User run: echo Hello ${{ github.actor }} From e68f8cd9481b8c21b4017a473700f387f0c2bf44 Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Fri, 17 Jan 2025 20:16:43 +0530 Subject: [PATCH 41/55] added service postgres --- .github/workflows/github-actions-demo.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/github-actions-demo.yml b/.github/workflows/github-actions-demo.yml index 2ec1309..75f4a2b 100644 --- a/.github/workflows/github-actions-demo.yml +++ b/.github/workflows/github-actions-demo.yml @@ -18,12 +18,12 @@ jobs: POSTGRES_PASSWORD: ${{ secrets.DATABASE_PASSWORD }} POSTGRES_DB: ${{ secrets.TEST_DATABASE_NAME }} ports: - - 5432:5432 + - 5432:5432 options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 steps: - name: Hello User run: echo Hello ${{ github.actor }} From a84f08e2700a737f217d0e75b0bdc3a3ac8520ea Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Fri, 17 Jan 2025 20:18:14 +0530 Subject: [PATCH 42/55] running with ubuntu --- .github/workflows/github-actions-demo.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github-actions-demo.yml b/.github/workflows/github-actions-demo.yml index 75f4a2b..7daf0ed 100644 --- a/.github/workflows/github-actions-demo.yml +++ b/.github/workflows/github-actions-demo.yml @@ -8,7 +8,7 @@ on: - "dev_fastapi" jobs: fastapi_job: - runs-on: windows-latest + runs-on: ubuntu-latest environment: name: testing services: From 0d3877604b15a06008c9db85dffb4c75a600a6b4 Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Fri, 17 Jan 2025 20:23:28 +0530 Subject: [PATCH 43/55] updated python version --- .github/workflows/github-actions-demo.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github-actions-demo.yml b/.github/workflows/github-actions-demo.yml index 7daf0ed..bc05ad8 100644 --- a/.github/workflows/github-actions-demo.yml +++ b/.github/workflows/github-actions-demo.yml @@ -34,7 +34,7 @@ jobs: - name: Set Up Python 3.10 uses: actions/setup-python@v5 with: - python-version: '3.10.0' + python-version: '3.10.x' - name: Upgrade Pip run: python -m pip install --upgrade pip From 810e5d84437780206c9dced88094c9c7c9a9c47b Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Sun, 19 Jan 2025 13:11:31 +0530 Subject: [PATCH 44/55] automated docker push with github actions --- .github/workflows/github-actions-demo.yml | 23 ++++++++++++++++++++++- Dockerfile | 6 ++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 Dockerfile diff --git a/.github/workflows/github-actions-demo.yml b/.github/workflows/github-actions-demo.yml index bc05ad8..4358376 100644 --- a/.github/workflows/github-actions-demo.yml +++ b/.github/workflows/github-actions-demo.yml @@ -59,4 +59,25 @@ jobs: pip install pytest pip install pytest-cov pytest - pytest --cov=app \ No newline at end of file + pytest --cov=app + - name: Login into Docker + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Setup Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v3 + - name: Build and push + id: docker_build + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ secrets.DOCKER_USERNAME }}/fastapi:latest + cache-from: type=local, src=/tmp/.buildx-cache + cache-to: type=local, dest=/tmp/.buildx-cache + - name: Image digest + run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9412fd2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.10.x +WORKDIR /usr/src/app +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt +COPY . . +CMD ["uvicorn","app.main:app","--host","0.0.0.0","--port","8088"] \ No newline at end of file From a9c103e486ba7817a24c57e2039e06fffb6972b4 Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Sun, 19 Jan 2025 13:15:39 +0530 Subject: [PATCH 45/55] added builder in docker steps: --- .github/workflows/github-actions-demo.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/github-actions-demo.yml b/.github/workflows/github-actions-demo.yml index 4358376..b227afd 100644 --- a/.github/workflows/github-actions-demo.yml +++ b/.github/workflows/github-actions-demo.yml @@ -74,6 +74,7 @@ jobs: with: context: . file: ./Dockerfile + builder: ${{ steps.buildx.outputs.name }} platforms: linux/amd64,linux/arm64 push: true tags: ${{ secrets.DOCKER_USERNAME }}/fastapi:latest From febd77338b4abab399a57f7c6898dfba427676fe Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Sun, 19 Jan 2025 13:40:33 +0530 Subject: [PATCH 46/55] fixed docker steps --- .github/workflows/github-actions-demo.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/github-actions-demo.yml b/.github/workflows/github-actions-demo.yml index b227afd..51eb1f9 100644 --- a/.github/workflows/github-actions-demo.yml +++ b/.github/workflows/github-actions-demo.yml @@ -78,7 +78,7 @@ jobs: platforms: linux/amd64,linux/arm64 push: true tags: ${{ secrets.DOCKER_USERNAME }}/fastapi:latest - cache-from: type=local, src=/tmp/.buildx-cache - cache-to: type=local, dest=/tmp/.buildx-cache + cache-from: type=gha + cache-to: type=gha, mode=max - name: Image digest run: echo ${{ steps.docker_build.outputs.digest }} From e4924b063c463d41f6bf756380001cf4ab8c96da Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Sun, 19 Jan 2025 13:44:45 +0530 Subject: [PATCH 47/55] updated python version in Dockerfile to 3.10 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 9412fd2..5ece0de 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.10.x +FROM python:3.10 WORKDIR /usr/src/app COPY requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt From 5114b476f6be847b172cfa62c05f7bc07fc88f71 Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Sun, 19 Jan 2025 14:27:48 +0530 Subject: [PATCH 48/55] updaetd database string for prod postgres --- app/database.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/database.py b/app/database.py index 92c2503..482cbd7 100644 --- a/app/database.py +++ b/app/database.py @@ -6,8 +6,8 @@ # add echo = true if you like to log create query SQL_DATABASE_URL = POSTGRESS_SQL_DATABASE_URL -engine = create_engine(SQL_DATABASE_URL) -# engine = create_engine(f"{settings.database_string}", echo = True) +engine = create_engine(SQL_DATABASE_URL) #Dev Env +engine = create_engine(f"{settings.database_string}", echo = True) def set_test_database(): print("Setting Up test databse") From d53de0235018d5a6a7618061489c10601a94db93 Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Sun, 19 Jan 2025 15:36:09 +0530 Subject: [PATCH 49/55] updated port --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 5ece0de..d415d8b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,4 +3,4 @@ WORKDIR /usr/src/app COPY requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt COPY . . -CMD ["uvicorn","app.main:app","--host","0.0.0.0","--port","8088"] \ No newline at end of file +CMD ["uvicorn","app.main:app","--host","0.0.0.0","--port","9009"] \ No newline at end of file From bb4c8e88b3e532b4487b5b3999aabac2b651bf65 Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Sun, 19 Jan 2025 16:12:19 +0530 Subject: [PATCH 50/55] updaetd port and created new database for docker service --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index d415d8b..68fbfea 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,4 +3,4 @@ WORKDIR /usr/src/app COPY requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt COPY . . -CMD ["uvicorn","app.main:app","--host","0.0.0.0","--port","9009"] \ No newline at end of file +CMD ["uvicorn","app.main:app","--host","0.0.0.0","--port","9099"] \ No newline at end of file From 384af0a27dd9e268e9a392801712bb2bca197f98 Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Sun, 19 Jan 2025 16:29:13 +0530 Subject: [PATCH 51/55] updated database url --- app/database.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/database.py b/app/database.py index 482cbd7..03bdd15 100644 --- a/app/database.py +++ b/app/database.py @@ -6,8 +6,8 @@ # add echo = true if you like to log create query SQL_DATABASE_URL = POSTGRESS_SQL_DATABASE_URL -engine = create_engine(SQL_DATABASE_URL) #Dev Env -engine = create_engine(f"{settings.database_string}", echo = True) +# engine = create_engine(SQL_DATABASE_URL) #Dev Env +engine = create_engine(f"{settings.database_string}",echo = True) def set_test_database(): print("Setting Up test databse") @@ -23,7 +23,7 @@ def get_override_session(): session.close() def create_database_and_tables(): - SQLModel.metadata.create_all(engine) + SQLModel.metadata.create_all(bind=engine) print("database Created") return "Database Connected" From cbe3d331af0aaa153b7152edd247b3efd6139324 Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Sun, 19 Jan 2025 16:43:34 +0530 Subject: [PATCH 52/55] added log for database url --- app/database.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/database.py b/app/database.py index 03bdd15..298c8f8 100644 --- a/app/database.py +++ b/app/database.py @@ -25,6 +25,7 @@ def get_override_session(): def create_database_and_tables(): SQLModel.metadata.create_all(bind=engine) print("database Created") + print("database Created at :", engine.url) return "Database Connected" def close_connection(): From 7331ef2544e73e82c1e42525e71aaf947f120340 Mon Sep 17 00:00:00 2001 From: Dharmraj Rathod Date: Sun, 19 Jan 2025 18:11:50 +0530 Subject: [PATCH 53/55] removing the drop databse statements --- app/main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/main.py b/app/main.py index ec9919d..c97c5e9 100644 --- a/app/main.py +++ b/app/main.py @@ -4,18 +4,18 @@ from fastapi.middleware.cors import CORSMiddleware import uvicorn from app.config import settings -from app.database import create_database_and_tables, drop_database_and_tables +from app.database import create_database_and_tables, drop_database_and_tables, close_connection from .model import * @asynccontextmanager async def lifespan(app: FastAPI): + # app.state.db = drop_database_and_tables() app.state.db = create_database_and_tables() print(app.state.db) yield - # app.state.db = close_connection() - app.state.db = drop_database_and_tables() + app.state.db = close_connection() print(app.state.db) From df31642f22b8ba5d3cf174f62c3fcfd556187cec Mon Sep 17 00:00:00 2001 From: DharmrajRathod6 <46452996+RathQd@users.noreply.github.com> Date: Mon, 3 Mar 2025 16:14:29 +0530 Subject: [PATCH 54/55] Update README.md --- README.md | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 72 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index dc4329a..1563cd9 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,74 @@ -# python_fastapi +# ๐Ÿš€ Social Media (Basic) - FastAPI -Building the linkedIn Clone with FastAPI from scratch with pipeline of gitlab +A **Social Media** built from scratch using **FastAPI**, featuring a scalable and secure backend with Github Actions CI/CD pipeline integration. + +## ๐Ÿ“Œ Features +- **๐Ÿ”— RESTful API** with FastAPI +- **๐Ÿ’พ Database**: PostgreSQL with SQLModel ORM +- **โœ… Data Validation**: Pydantic +- **๐Ÿ”„ Database Migrations**: Alembic +- **๐Ÿ” Secure Endpoints**: JWT Authentication +- **๐Ÿณ Dockerized Application** +- **๐Ÿ› ๏ธ Continuous Integration**: GitLab CI/CD Pipeline +- **๐Ÿงช Automated Testing**: Pytest + +## ๐Ÿ—๏ธ Tech Stack +- **FastAPI** - High-performance web framework +- **SQLModel** - ORM for database interactions +- **Pydantic** - Data validation and settings management +- **Alembic** - Database migration tool +- **JWT Tokens** - Secure authentication & authorization +- **PostgreSQL** - Relational database +- **Docker** - Containerized deployment +- **Pytest** - Automated API testing +- **Github Actions CI/CD** - Continuous integration and deployment + +## ๐Ÿš€ Getting Started +### 1๏ธโƒฃ Clone the Repository +```bash +git clone https://github.com/RathQd/python_fastapi.git +cd python_fastapi +``` + +### 2๏ธโƒฃ Setup Environment Variables +Create a `.env` file in the root directory and configure your database & JWT settings. +```env +DATABASE_URL= $URL$ +SECRET_KEY= $KEY$ +ALGORITHM= $ALGO$ +ACCESS_TOKEN_EXPIRE_MINUTES= $TIME_IN_MINs$ +``` + +#### ๐Ÿ–ฅ๏ธ Without Docker (Local Environment) +```bash +pip install -r requirements.txt +uvicorn app.main:app --reload +``` + +### 4๏ธโƒฃ Run Migrations +```bash +alembic upgrade head +``` + +### 5๏ธโƒฃ Run Tests +```bash +pytest already integrated with github actions CI/CD. +``` + +## ๐Ÿ“ก API Documentation +FastAPI provides interactive API docs: +- **Swagger UI**: `http://localhost:8000/docs` +- **ReDoc**: `http://localhost:8000/redoc` + +## ๐Ÿ“ฆ Deployment with Github actions CI/CD +This project integrates a **Github actions CI/CD pipeline** for automated testing and deployment. + +## ๐Ÿค Contributing +Contributions are welcome! Feel free to open issues or submit PRs. + +## ๐Ÿ“œ License +This project is licensed under the **MIT License**. + +--- +**Star โญ the repo if you found it useful!** -Demonstrating Fastapi Usage From 7c1a984f7e231cd6d77d0f3f6c62a4245fed4260 Mon Sep 17 00:00:00 2001 From: DharmrajRathod6 <46452996+RathQd@users.noreply.github.com> Date: Tue, 4 Mar 2025 21:15:40 +0530 Subject: [PATCH 55/55] Update README.md --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 1563cd9..8b7409a 100644 --- a/README.md +++ b/README.md @@ -66,9 +66,6 @@ This project integrates a **Github actions CI/CD pipeline** for automated testin ## ๐Ÿค Contributing Contributions are welcome! Feel free to open issues or submit PRs. -## ๐Ÿ“œ License -This project is licensed under the **MIT License**. - --- **Star โญ the repo if you found it useful!**