Spaces:
Runtime error
Runtime error
PrabhuKiranKonda
commited on
Commit
•
2f88c81
1
Parent(s):
096db82
initial commit
Browse files- Dockerfile +10 -0
- __init__.py +0 -0
- main.py +111 -0
- models.py +41 -0
- psql_database.py +29 -0
- requirements.txt +5 -0
- schemas.py +54 -0
- services.py +151 -0
Dockerfile
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM python:3.9-slim
|
2 |
+
WORKDIR /app
|
3 |
+
COPY requirements.txt ./requirements.txt
|
4 |
+
# Install uvicorn
|
5 |
+
RUN pip install uvicorn
|
6 |
+
# Install dependencies
|
7 |
+
RUN pip install -r requirements.txt
|
8 |
+
COPY . /app
|
9 |
+
ENTRYPOINT ["uvicorn", "main:app"]
|
10 |
+
CMD ["--host", "0.0.0.0", "--port", "7860"]
|
__init__.py
ADDED
File without changes
|
main.py
ADDED
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import FastAPI, HTTPException, security, Depends
|
2 |
+
import sqlalchemy.orm as orm
|
3 |
+
import services as services
|
4 |
+
import schemas as schemas
|
5 |
+
import models as models
|
6 |
+
from fastapi.middleware.cors import CORSMiddleware
|
7 |
+
|
8 |
+
|
9 |
+
app = FastAPI(title="TODO API",
|
10 |
+
description='Todo App API using FastAPI, PostgreSQL and authentication support',
|
11 |
+
version='0.0.1')
|
12 |
+
|
13 |
+
app.add_middleware(
|
14 |
+
CORSMiddleware,
|
15 |
+
allow_origins=["*"],
|
16 |
+
allow_credentials=True,
|
17 |
+
allow_methods=["*"],
|
18 |
+
allow_headers=["*"],
|
19 |
+
)
|
20 |
+
|
21 |
+
|
22 |
+
@app.get("/")
|
23 |
+
async def root():
|
24 |
+
return {"Title": "Todo API",
|
25 |
+
"Description": "Todo App API using FastAPI, PostgreSQL and authentication support",
|
26 |
+
"Version": "0.0.1"}
|
27 |
+
|
28 |
+
|
29 |
+
# create user
|
30 |
+
@app.post("/api/users/")
|
31 |
+
async def create_user(user: schemas.userCreate,
|
32 |
+
db: orm.Session = Depends(services.get_db)):
|
33 |
+
|
34 |
+
db_user = await services.get_user_by_email(email=user.email.lower(), db=db)
|
35 |
+
if db_user:
|
36 |
+
raise HTTPException(status_code=400, detail="Email already registered")
|
37 |
+
# Await the create_user function
|
38 |
+
user = await services.create_user(user=user, db=db)
|
39 |
+
token = await services.create_token(user=user)
|
40 |
+
return token
|
41 |
+
|
42 |
+
|
43 |
+
@app.post("/api/token/")
|
44 |
+
async def generate_token(form_data: security.OAuth2PasswordRequestForm = Depends(),
|
45 |
+
db: orm.Session = Depends(services.get_db)):
|
46 |
+
|
47 |
+
user = await services.authenticate_user(form_data.username.lower(), form_data.password, db=db)
|
48 |
+
if not user:
|
49 |
+
raise HTTPException(
|
50 |
+
status_code=401, detail="Incorrect email or password")
|
51 |
+
token = await services.create_token(user=user)
|
52 |
+
return {
|
53 |
+
"access_token": token['access_token'],
|
54 |
+
"first_name": user.first_name,
|
55 |
+
"last_name": user.last_name,
|
56 |
+
}
|
57 |
+
|
58 |
+
|
59 |
+
@app.get('/api/users/me', response_model=schemas.User)
|
60 |
+
async def get_user(user: schemas.User = Depends(services.get_current_user)):
|
61 |
+
return user
|
62 |
+
|
63 |
+
|
64 |
+
@app.post("/api/users/todo")
|
65 |
+
async def create_todo(todo: schemas.TodoCreate, user: schemas.User = Depends(services.get_current_user), db: orm.Session = Depends(services.get_db)):
|
66 |
+
return await services.create_todo(user=user, todo=todo, db=db)
|
67 |
+
|
68 |
+
|
69 |
+
@app.get("/api/users/todo")
|
70 |
+
async def get_todos(user: schemas.User = Depends(services.get_current_user), db: orm.Session = Depends(services.get_db)):
|
71 |
+
return await services.get_todos(user=user, db=db)
|
72 |
+
|
73 |
+
|
74 |
+
@app.put("/api/users/todo/{todo_id}")
|
75 |
+
async def update_todo(todo_id: int,
|
76 |
+
todo: schemas.TodoCreate,
|
77 |
+
user: schemas.User = Depends(services.get_current_user),
|
78 |
+
db: orm.Session = Depends(services.get_db)):
|
79 |
+
return {
|
80 |
+
"todo": await services.update_todo(user=user, db=db, todo=todo, todo_id=todo_id),
|
81 |
+
"message": "Todo updated successfully"
|
82 |
+
}
|
83 |
+
|
84 |
+
|
85 |
+
@app.delete("/api/users/todo/{todo_id}")
|
86 |
+
async def delete_todo(todo_id: int,
|
87 |
+
user: schemas.User = Depends(services.get_current_user),
|
88 |
+
db: orm.Session = Depends(services.get_db)):
|
89 |
+
return {
|
90 |
+
"todo": await services.delete_todo(user=user, db=db, todo_id=todo_id),
|
91 |
+
"message": "Todo deleted successfully"
|
92 |
+
}
|
93 |
+
|
94 |
+
|
95 |
+
@app.get("/api/users/todo/today")
|
96 |
+
async def get_today_todos(user: schemas.User = Depends(services.get_current_user), db: orm.Session = Depends(services.get_db)):
|
97 |
+
return await services.get_today_todos(user=user, db=db)
|
98 |
+
|
99 |
+
|
100 |
+
@app.get("/api/users/todo/overdue")
|
101 |
+
async def get_overdue_todos(user: schemas.User = Depends(services.get_current_user), db: orm.Session = Depends(services.get_db)):
|
102 |
+
return await services.get_overdue_todos(user=user, db=db)
|
103 |
+
|
104 |
+
|
105 |
+
@app.get("/api/users/todo/upcoming")
|
106 |
+
async def get_upcoming_todos(user: schemas.User = Depends(services.get_current_user), db: orm.Session = Depends(services.get_db)):
|
107 |
+
return await services.get_upcoming_todos(user=user, db=db)
|
108 |
+
|
109 |
+
@app.get("/api/users/todo/completed")
|
110 |
+
async def get_completed_todos(user: schemas.User = Depends(services.get_current_user), db: orm.Session = Depends(services.get_db)):
|
111 |
+
return await services.get_completed_todos(user=user, db=db)
|
models.py
ADDED
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from sqlalchemy import create_engine, Column, Integer, String, Boolean, Date, ForeignKey, CheckConstraint
|
2 |
+
from sqlalchemy.ext.declarative import declarative_base
|
3 |
+
from sqlalchemy.orm import relationship
|
4 |
+
from psql_database import engine, Base
|
5 |
+
from passlib.hash import bcrypt
|
6 |
+
|
7 |
+
# Define the User model
|
8 |
+
class User(Base):
|
9 |
+
__tablename__ = 'users'
|
10 |
+
user_id = Column(Integer, primary_key=True)
|
11 |
+
first_name = Column(String(50), nullable=False)
|
12 |
+
last_name = Column(String(50), nullable=False)
|
13 |
+
email = Column(String(100), nullable=False, unique=True)
|
14 |
+
password = Column(String(100), nullable=False) # HASHED PASSWORD
|
15 |
+
|
16 |
+
todos = relationship('Todo', back_populates='user')
|
17 |
+
|
18 |
+
|
19 |
+
def verify_password(self, plain_password):
|
20 |
+
return bcrypt.verify(plain_password, self.password)
|
21 |
+
|
22 |
+
|
23 |
+
|
24 |
+
# Define the Todo model
|
25 |
+
class Todo(Base):
|
26 |
+
__tablename__ = 'todos'
|
27 |
+
todo_id = Column(Integer, primary_key=True)
|
28 |
+
user_id = Column(Integer, ForeignKey('users.user_id'), nullable=False)
|
29 |
+
task_name = Column(String(100), nullable=False)
|
30 |
+
task_description = Column(String)
|
31 |
+
priority = Column(Integer, CheckConstraint('priority >= 1 AND priority <= 3', name="priority should be either 1 or 2 or 3"), nullable=False) # 1: high, 2: medium, 3: low
|
32 |
+
category = Column(String(50))
|
33 |
+
due_date = Column(Date, nullable=False)
|
34 |
+
status = Column(Boolean, default=False)
|
35 |
+
|
36 |
+
user = relationship('User', back_populates='todos')
|
37 |
+
|
38 |
+
# Create the tables
|
39 |
+
def create_database_tables():
|
40 |
+
return Base.metadata.create_all(bind=engine)
|
41 |
+
|
psql_database.py
ADDED
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import urllib.parse as up
|
3 |
+
import sqlalchemy as sa
|
4 |
+
from sqlalchemy.orm import sessionmaker
|
5 |
+
from sqlalchemy.ext.declarative import declarative_base
|
6 |
+
|
7 |
+
# Parse the DATABASE_URL using urllib.parse
|
8 |
+
up.uses_netloc.append("postgres")
|
9 |
+
url = up.urlparse(
|
10 |
+
"postgres://xphzyodo:[email protected]/xphzyodo")
|
11 |
+
|
12 |
+
|
13 |
+
# Create the connection string
|
14 |
+
conn_string = f'postgresql+psycopg2://{url.username}:{url.password}@{url.hostname}/{url.path[1:]}'
|
15 |
+
|
16 |
+
# Create the engine using the connection string
|
17 |
+
engine = sa.create_engine(conn_string)
|
18 |
+
|
19 |
+
# Reassign the engine to your existing engine variable
|
20 |
+
engine = engine
|
21 |
+
|
22 |
+
# Create the session factory
|
23 |
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
24 |
+
|
25 |
+
# Reassign the session factory to your existing SessionLocal variable
|
26 |
+
SessionLocal = SessionLocal
|
27 |
+
|
28 |
+
# Reassign the Base to your existing Base variable
|
29 |
+
Base = declarative_base()
|
requirements.txt
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
fastapi==0.97.0
|
2 |
+
passlib==1.7.4
|
3 |
+
pydantic==1.10.2
|
4 |
+
PyJWT==2.7.0
|
5 |
+
SQLAlchemy==1.4.48
|
schemas.py
ADDED
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# why schemas? it is a way to define the structure of the data sent to the server and the data received from the server
|
2 |
+
from pydantic import BaseModel, Field
|
3 |
+
import datetime as dt
|
4 |
+
|
5 |
+
# template for user data. this is used to validate the data sent to the server
|
6 |
+
|
7 |
+
|
8 |
+
class userBase(BaseModel):
|
9 |
+
first_name: str = Field(...)
|
10 |
+
last_name: str = Field(...)
|
11 |
+
email: str = Field(...,)
|
12 |
+
|
13 |
+
|
14 |
+
class userCreate(userBase):
|
15 |
+
password: str = Field(...) # hashed password
|
16 |
+
|
17 |
+
class Config:
|
18 |
+
orm_mode = True # to tell pydantic to read the data even if it is not a dict but an ORM model
|
19 |
+
schema_extra = {
|
20 |
+
"example": {
|
21 |
+
"first_name": "John",
|
22 |
+
"last_name": "Doe",
|
23 |
+
"email": "[email protected]",
|
24 |
+
"password": "password",
|
25 |
+
}
|
26 |
+
}
|
27 |
+
|
28 |
+
|
29 |
+
class User(userBase):
|
30 |
+
user_id: int
|
31 |
+
|
32 |
+
class Config:
|
33 |
+
orm_mode = True
|
34 |
+
|
35 |
+
|
36 |
+
class TodoBase(BaseModel):
|
37 |
+
task_name: str
|
38 |
+
task_description: str
|
39 |
+
priority: int
|
40 |
+
category: str
|
41 |
+
due_date: dt.date
|
42 |
+
status: bool = False
|
43 |
+
|
44 |
+
|
45 |
+
class TodoCreate(TodoBase):
|
46 |
+
pass
|
47 |
+
|
48 |
+
|
49 |
+
class Todo(TodoBase):
|
50 |
+
todo_id: int
|
51 |
+
user_id: int
|
52 |
+
|
53 |
+
class Config:
|
54 |
+
orm_mode = True
|
services.py
ADDED
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from copy import deepcopy
|
2 |
+
import models
|
3 |
+
from psql_database import engine, Base, SessionLocal
|
4 |
+
import sqlalchemy as sa
|
5 |
+
import schemas as schemas
|
6 |
+
import passlib.hash as pwd_hash
|
7 |
+
import re
|
8 |
+
from fastapi import HTTPException, Depends, security
|
9 |
+
import jwt
|
10 |
+
|
11 |
+
|
12 |
+
OAUTH2_SCHEME = security.OAuth2PasswordBearer(tokenUrl="/api/token")
|
13 |
+
|
14 |
+
JWT_SECRET = 'myjwtsecret'
|
15 |
+
|
16 |
+
|
17 |
+
def validate_email(email):
|
18 |
+
pattern = r'^[\w\.-]+@[\w\.-]+\.\w+$'
|
19 |
+
return bool(re.match(pattern, email))
|
20 |
+
|
21 |
+
|
22 |
+
def create_database_tables():
|
23 |
+
return Base.metadata.create_all(bind=engine)
|
24 |
+
|
25 |
+
|
26 |
+
def get_db():
|
27 |
+
db = SessionLocal()
|
28 |
+
try:
|
29 |
+
yield db
|
30 |
+
finally:
|
31 |
+
db.close()
|
32 |
+
|
33 |
+
|
34 |
+
async def get_user_by_email(email: str, db: sa.orm.Session):
|
35 |
+
return db.query(models.User).filter(models.User.email == email).first()
|
36 |
+
|
37 |
+
|
38 |
+
async def create_user(user: schemas.userCreate, db: sa.orm.Session):
|
39 |
+
|
40 |
+
if not validate_email(user.email):
|
41 |
+
raise HTTPException(status_code=400, detail="Invalid email")
|
42 |
+
|
43 |
+
user_obj = models.User(
|
44 |
+
first_name=user.first_name,
|
45 |
+
last_name=user.last_name,
|
46 |
+
email=user.email,
|
47 |
+
password=pwd_hash.bcrypt.hash(user.password)
|
48 |
+
)
|
49 |
+
|
50 |
+
db.add(user_obj)
|
51 |
+
db.commit() # commit operation
|
52 |
+
db.refresh(user_obj) # Await the refresh operation
|
53 |
+
return user_obj
|
54 |
+
|
55 |
+
|
56 |
+
async def authenticate_user(email, password, db: sa.orm.Session):
|
57 |
+
user = await get_user_by_email(email=email, db=db)
|
58 |
+
|
59 |
+
if not user:
|
60 |
+
raise HTTPException(status_code=404, detail="User not found")
|
61 |
+
return user if user.verify_password(password) else False
|
62 |
+
|
63 |
+
|
64 |
+
async def create_token(user: models.User):
|
65 |
+
user_dict = {
|
66 |
+
"first_name": user.first_name,
|
67 |
+
"last_name": user.last_name,
|
68 |
+
"email": user.email,
|
69 |
+
}
|
70 |
+
token = jwt.encode(user_dict, JWT_SECRET)
|
71 |
+
|
72 |
+
return dict(access_token=token, token_type='bearer', status='success')
|
73 |
+
|
74 |
+
|
75 |
+
async def get_current_user(db: sa.orm.Session = Depends(get_db), token: str = Depends(OAUTH2_SCHEME)):
|
76 |
+
try:
|
77 |
+
payload = jwt.decode(token, JWT_SECRET, algorithms=['HS256'])
|
78 |
+
print(payload)
|
79 |
+
user = db.query(models.User).filter(
|
80 |
+
models.User.email == payload['email']).first()
|
81 |
+
if not user:
|
82 |
+
raise HTTPException(status_code=404, detail="User not found")
|
83 |
+
return schemas.User(**user.__dict__)
|
84 |
+
except Exception as e:
|
85 |
+
raise HTTPException(status_code=401, detail="Invalid token") from e
|
86 |
+
|
87 |
+
|
88 |
+
async def create_todo(user: schemas.User, todo: schemas.TodoCreate, db: sa.orm.Session = Depends(get_db)):
|
89 |
+
print(todo.dict())
|
90 |
+
|
91 |
+
todo_obj = models.Todo(
|
92 |
+
task_name=todo.task_name,
|
93 |
+
task_description=todo.task_description,
|
94 |
+
priority=todo.priority,
|
95 |
+
category=todo.category,
|
96 |
+
due_date=todo.due_date,
|
97 |
+
status=todo.status,
|
98 |
+
user_id=user.user_id,
|
99 |
+
)
|
100 |
+
|
101 |
+
db.add(todo_obj)
|
102 |
+
db.commit()
|
103 |
+
db.refresh(todo_obj)
|
104 |
+
return todo_obj
|
105 |
+
|
106 |
+
|
107 |
+
async def get_todos(user: schemas.User, db: sa.orm.Session = Depends(get_db)):
|
108 |
+
return db.query(models.Todo).filter(models.Todo.user_id == user.user_id).all()
|
109 |
+
|
110 |
+
|
111 |
+
async def delete_todo(todo_id: int, user: schemas.User, db: sa.orm.Session = Depends(get_db)):
|
112 |
+
todo = db.query(models.Todo).filter(models.Todo.user_id ==
|
113 |
+
user.user_id, models.Todo.todo_id == todo_id).first()
|
114 |
+
if not todo:
|
115 |
+
raise HTTPException(status_code=404, detail="Todo not found")
|
116 |
+
db.delete(todo)
|
117 |
+
db.commit()
|
118 |
+
return todo
|
119 |
+
|
120 |
+
|
121 |
+
async def update_todo(todo_id: int, user: schemas.User, todo: schemas.TodoCreate, db: sa.orm.Session = Depends(get_db)):
|
122 |
+
todo_db = db.query(models.Todo).filter(
|
123 |
+
models.Todo.user_id == user.user_id, models.Todo.todo_id == todo_id).first()
|
124 |
+
if not todo_db:
|
125 |
+
raise HTTPException(status_code=404, detail="Todo not found")
|
126 |
+
todo_db.task_name = todo.task_name
|
127 |
+
todo_db.task_description = todo.task_description or todo.task_description
|
128 |
+
todo_db.priority = todo.priority
|
129 |
+
todo_db.category = todo.category
|
130 |
+
todo_db.due_date = todo.due_date
|
131 |
+
todo_db.status = todo.status
|
132 |
+
|
133 |
+
db.commit()
|
134 |
+
db.refresh(todo_db)
|
135 |
+
return todo_db
|
136 |
+
|
137 |
+
|
138 |
+
async def get_today_todos(user: schemas.User, db: sa.orm.Session = Depends(get_db)):
|
139 |
+
return db.query(models.Todo).filter(models.Todo.user_id == user.user_id, sa.func.date(models.Todo.due_date) == sa.func.date(sa.func.now())).all()
|
140 |
+
|
141 |
+
|
142 |
+
async def get_overdue_todos(user: schemas.User, db: sa.orm.Session = Depends(get_db)):
|
143 |
+
return db.query(models.Todo).filter(models.Todo.user_id == user.user_id, models.Todo.due_date < sa.func.now()).all()
|
144 |
+
|
145 |
+
|
146 |
+
async def get_upcoming_todos(user: schemas.User, db: sa.orm.Session = Depends(get_db)):
|
147 |
+
return db.query(models.Todo).filter(models.Todo.user_id == user.user_id, models.Todo.due_date > sa.func.now()).all()
|
148 |
+
|
149 |
+
|
150 |
+
async def get_completed_todos(user: schemas.User, db: sa.orm.Session = Depends(get_db)):
|
151 |
+
return db.query(models.Todo).filter(models.Todo.user_id == user.user_id, models.Todo.status == True).all()
|