Skip to content

Commit ae3394a

Browse files
committed
api full working after .envwith pytest ruff unit testing cicd and improvement
1 parent bda7b56 commit ae3394a

File tree

10 files changed

+184
-138
lines changed

10 files changed

+184
-138
lines changed

.github/workflows/ci.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: CI/CD Pipeline
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
branches:
9+
- main
10+
11+
jobs:
12+
build:
13+
runs-on: ubuntu-latest
14+
15+
steps:
16+
- uses: actions/checkout@v4
17+
- name: Set up Python
18+
uses: actions/setup-python@v5
19+
with:
20+
python-version: '3.11'
21+
- name: Install dependencies
22+
run: |
23+
python -m pip install --upgrade pip
24+
pip install -r requirements.txt
25+
pip install ruff pytest
26+
- name: Run Ruff Linting
27+
run: ruff check .
28+
- name: Run Tests
29+
run: pytest

README.md

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,32 +37,27 @@ This is project designed to learn how to build RESTful APIs using **FastAPI**.
3737
- JWT-based login using real password authentication, where the doctor ID in the token is used to authorize patient data operations
3838
use passlib for password hashing ,PyJWT for creating and verifying JWTs
3939

40-
- ADD pytest for allowing simple, scalable test writing and execution ,improves code reliability and makes it easier to catch bugs early.
40+
1. ADD pytest for allowing simple, scalable test writing and execution ,improves code reliability and makes it easier to catch bugs early.
4141

42-
43-
44-
## Notes for Improvement
45-
46-
47-
1. Improve Code Quality with Linting & Formatting
42+
2. Improve Code Quality with Linting & Formatting
4843
* installed ruff, b used it to
4944
check and format the code.
5045

5146

5247

53-
2. Organize Endpoints with API Routers
48+
3. Organize Endpoints with API Routers
5449
* What: Your app.py file will get crowded as you add more endpoints. We can
5550
split the patient-related and doctor-related endpoints into separate files
5651
(e.g., routers/patients.py, routers/auth.py) to keep the code clean and
5752
modular.
5853

59-
3. Enhance Configuration Management
54+
4. Enhance Configuration Management
6055
* What: Currently, you load environment variables directly in app.py. We can
6156
create a dedicated, type-safe config.py file using Pydantic's BaseSettings.
6257
This validates your settings on startup (like ensuring DATABASE_URL is set)
6358
and provides better autocompletion.
6459

65-
4. Set Up a CI/CD Pipeline
60+
5. Set Up a CI/CD Pipeline
6661
* What: This is a professional best practice. We can create a GitHub Actions
6762
workflow that automatically runs your tests and linter every time you push
6863
new code. This acts as a gatekeeper to ensure that no broken or poorly

app.py

Lines changed: 11 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,13 @@
1-
import os
2-
from dotenv import load_dotenv
3-
4-
load_dotenv(dotenv_path=os.path.join(os.path.dirname(__file__), '.env'))
5-
6-
from fastapi import FastAPI, Path, HTTPException, Query, Depends
7-
from fastapi.responses import JSONResponse
8-
from typing import Annotated, Literal, Optional
9-
from fastapi.middleware.cors import CORSMiddleware
101
from contextlib import asynccontextmanager
11-
import pymongo
2+
from fastapi import FastAPI
3+
from fastapi.middleware.cors import CORSMiddleware
124

135
from database import init_db
14-
from auth import create_access_token, authenticate_user, get_current_user, get_current_doctor, verify_password, get_password_hash
15-
from models.patient import Patient, PatientUpdate, PatientCreate
16-
from models.doctor import Doctor, DoctorCreate
6+
from routers.auth import router as auth_router
7+
from routers.patients import router as patients_router
8+
from config import Settings
9+
10+
settings = Settings()
1711

1812

1913
@asynccontextmanager
@@ -27,35 +21,13 @@ async def lifespan(app: FastAPI):
2721

2822
app.add_middleware(
2923
CORSMiddleware,
30-
allow_origins=["*"], # allow all origins temporarily
24+
allow_origins=["*"], # allow all origins temporarily
25+
#allow_origins=["http://localhost:3000"], # Replace with your frontend domain(s) in production
3126
allow_credentials=True,
3227
allow_methods=["*"],
3328
allow_headers=["*"],
3429
)
3530

36-
@app.post("/register")
37-
async def register_doctor(doctor: DoctorCreate):
38-
existing_doctor = await Doctor.find_one(Doctor.username == doctor.username)
39-
if existing_doctor:
40-
raise HTTPException(status_code=400, detail="Username already registered")
41-
hashed_password = get_password_hash(doctor.password)
42-
new_doctor = Doctor(username=doctor.username, password=hashed_password)
43-
await new_doctor.create()
44-
return {"message": "Doctor registered successfully"}
45-
46-
@app.post("/login")
47-
async def login_for_access_token(form_data: DoctorCreate):
48-
doctor = await Doctor.find_one(Doctor.username == form_data.username)
49-
if not doctor or not verify_password(form_data.password, doctor.password):
50-
raise HTTPException(
51-
status_code=401,
52-
detail="Incorrect username or password",
53-
headers={"WWW-Authenticate": "Bearer"},
54-
)
55-
access_token = create_access_token(data={"sub": doctor.username})
56-
return {"access_token": access_token, "token_type": "bearer"}
57-
58-
5931
@app.get("/")
6032
def hello():
6133
return {'message':'Patient Management System API'}
@@ -64,77 +36,5 @@ def hello():
6436
def about():
6537
return {'message': 'A fully functional API to manage your patient records'}
6638

67-
@app.get('/view')
68-
async def view(current_doctor: str = Depends(get_current_doctor)):
69-
data = await Patient.find(Patient.doctor_id == current_doctor).to_list()
70-
return data
71-
72-
@app.get('/patient/{patient_id}')
73-
async def view_patient(patient_id: str = Path(..., description='ID of the patient in the DB', examples=['P001']), current_doctor: str = Depends(get_current_doctor)):
74-
patient = await Patient.get(patient_id)
75-
if not patient or patient.doctor_id != current_doctor:
76-
raise HTTPException(status_code=404, detail='Patient not found')
77-
return patient
78-
79-
@app.get('/sort')
80-
async def sort_patients(sort_by: str = Query(..., description='Sort on the basis of height, weight, age or _id'), order: str = Query('asc', description='sort in asc or desc order'), current_doctor: str = Depends(get_current_doctor)):
81-
82-
valid_fields = ['height', 'weight', 'age', '_id']
83-
84-
if sort_by not in valid_fields:
85-
raise HTTPException(status_code=400, detail=f'Invalid field select from {valid_fields}')
86-
87-
if order not in ['asc', 'desc']:
88-
raise HTTPException(status_code=400, detail='Invalid order select between asc and desc')
89-
90-
sort_order = pymongo.DESCENDING if order=='desc' else pymongo.ASCENDING
91-
92-
sorted_data = await Patient.find(Patient.doctor_id == current_doctor).sort((sort_by,sort_order)).to_list()
93-
94-
return sorted_data
95-
96-
@app.post('/create')
97-
async def create_patient(patient_data: PatientCreate, current_doctor: str = Depends(get_current_doctor)):
98-
99-
# check if the patient already exists
100-
existing_patient = await Patient.get(patient_data.id)
101-
if existing_patient:
102-
raise HTTPException(status_code=400, detail='Patient already exists')
103-
104-
patient = Patient(**patient_data.model_dump(), doctor_id=current_doctor)
105-
# new patient add to the database
106-
await patient.create()
107-
108-
return JSONResponse(status_code=201, content={'message':'patient created successfully'})
109-
110-
111-
@app.put('/edit/{patient_id}')
112-
async def update_patient(patient_id: str, patient_update: PatientUpdate, current_doctor: str = Depends(get_current_doctor)):
113-
114-
patient = await Patient.get(patient_id)
115-
116-
if not patient or patient.doctor_id != current_doctor:
117-
raise HTTPException(status_code=404, detail='Patient not found')
118-
119-
patient_update_dict = patient_update.model_dump(exclude_unset=True)
120-
121-
for key, value in patient_update_dict.items():
122-
setattr(patient, key, value)
123-
124-
await patient.save()
125-
126-
127-
return JSONResponse(status_code=200, content={'message':'patient updated'})
128-
129-
@app.delete('/delete/{patient_id}')
130-
async def delete_patient(patient_id: str, current_doctor: str = Depends(get_current_doctor)):
131-
132-
patient = await Patient.get(patient_id)
133-
134-
if not patient or patient.doctor_id != current_doctor:
135-
raise HTTPException(status_code=404, detail='Patient not found')
136-
137-
await patient.delete()
138-
139-
140-
return JSONResponse(status_code=200, content={'message':'patient deleted'})
39+
app.include_router(auth_router, prefix="/auth", tags=["auth"])
40+
app.include_router(patients_router, prefix="/patients", tags=["patients"])

auth.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
from passlib.context import CryptContext
44
import jwt
55
from datetime import datetime, timedelta
6-
import os
7-
import bcrypt
6+
from config import Settings
87

9-
SECRET_KEY = os.getenv("SECRET_KEY")
8+
settings = Settings()
9+
SECRET_KEY = settings.SECRET_KEY
1010
ALGORITHM = "HS256"
1111
ACCESS_TOKEN_EXPIRE_MINUTES = 30
1212

config.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from pydantic_settings import BaseSettings
2+
3+
class Settings(BaseSettings):
4+
DATABASE_URL: str
5+
SECRET_KEY: str
6+
7+
class Config:
8+
env_file = ".env"
9+

database.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import motor.motor_asyncio
44
from models.patient import Patient
55
from models.doctor import Doctor
6-
import os
6+
from config import Settings
7+
8+
settings = Settings()
79

810
async def init_db():
9-
db_url = os.getenv("DATABASE_URL")
11+
db_url = settings.DATABASE_URL
1012
client = motor.motor_asyncio.AsyncIOMotorClient(db_url)
1113
await init_beanie(database=client.db_name, document_models=[Patient, Doctor])

requirements.txt

134 Bytes
Binary file not shown.

routers/auth.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from fastapi import APIRouter, HTTPException
2+
from auth import create_access_token, verify_password, get_password_hash
3+
from models.doctor import Doctor, DoctorCreate
4+
5+
router = APIRouter()
6+
7+
@router.post("/register")
8+
async def register_doctor(doctor: DoctorCreate):
9+
existing_doctor = await Doctor.find_one(Doctor.username == doctor.username)
10+
if existing_doctor:
11+
raise HTTPException(status_code=400, detail="Username already registered")
12+
hashed_password = get_password_hash(doctor.password)
13+
new_doctor = Doctor(username=doctor.username, password=hashed_password)
14+
await new_doctor.create()
15+
return {"message": "Doctor registered successfully"}
16+
17+
@router.post("/login")
18+
async def login_for_access_token(form_data: DoctorCreate):
19+
doctor = await Doctor.find_one(Doctor.username == form_data.username)
20+
if not doctor or not verify_password(form_data.password, doctor.password):
21+
raise HTTPException(
22+
status_code=401,
23+
detail="Incorrect username or password",
24+
headers={"WWW-Authenticate": "Bearer"},
25+
)
26+
access_token = create_access_token(data={"sub": doctor.username})
27+
return {"access_token": access_token, "token_type": "bearer"}

routers/patients.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
from fastapi import APIRouter, Path, HTTPException, Query, Depends
2+
from fastapi.responses import JSONResponse
3+
import pymongo
4+
5+
from auth import get_current_doctor
6+
from models.patient import Patient, PatientUpdate, PatientCreate
7+
8+
router = APIRouter()
9+
10+
@router.get("/view")
11+
async def view(current_doctor: str = Depends(get_current_doctor)):
12+
data = await Patient.find(Patient.doctor_id == current_doctor).to_list()
13+
return data
14+
15+
@router.get("/patient/{patient_id}")
16+
async def view_patient(patient_id: str = Path(..., description='ID of the patient in the DB', examples=['P001']), current_doctor: str = Depends(get_current_doctor)):
17+
patient = await Patient.get(patient_id)
18+
if not patient or patient.doctor_id != current_doctor:
19+
raise HTTPException(status_code=404, detail='Patient not found')
20+
return patient
21+
22+
@router.get("/sort")
23+
async def sort_patients(sort_by: str = Query(..., description='Sort on the basis of height, weight, age or _id'), order: str = Query('asc', description='sort in asc or desc order'), current_doctor: str = Depends(get_current_doctor)):
24+
25+
valid_fields = ['height', 'weight', 'age', '_id']
26+
27+
if sort_by not in valid_fields:
28+
raise HTTPException(status_code=400, detail=f'Invalid field select from {valid_fields}')
29+
30+
if order not in ['asc', 'desc']:
31+
raise HTTPException(status_code=400, detail='Invalid order select between asc and desc')
32+
33+
sort_order = pymongo.DESCENDING if order=='desc' else pymongo.ASCENDING
34+
35+
sorted_data = await Patient.find(Patient.doctor_id == current_doctor).sort((sort_by,sort_order)).to_list()
36+
37+
return sorted_data
38+
39+
@router.post("/create")
40+
async def create_patient(patient_data: PatientCreate, current_doctor: str = Depends(get_current_doctor)):
41+
42+
# check if the patient already exists
43+
existing_patient = await Patient.get(patient_data.id)
44+
if existing_patient:
45+
raise HTTPException(status_code=400, detail='Patient already exists')
46+
47+
patient = Patient(**patient_data.model_dump(), doctor_id=current_doctor)
48+
# new patient add to the database
49+
await patient.create()
50+
51+
return JSONResponse(status_code=201, content={'message':'patient created successfully'})
52+
53+
54+
@router.put("/edit/{patient_id}")
55+
async def update_patient(patient_id: str, patient_update: PatientUpdate, current_doctor: str = Depends(get_current_doctor)):
56+
57+
patient = await Patient.get(patient_id)
58+
59+
if not patient or patient.doctor_id != current_doctor:
60+
raise HTTPException(status_code=404, detail='Patient not found')
61+
62+
patient_update_dict = patient_update.model_dump(exclude_unset=True)
63+
64+
for key, value in patient_update_dict.items():
65+
setattr(patient, key, value)
66+
67+
await patient.save()
68+
69+
70+
return JSONResponse(status_code=200, content={'message':'patient updated'})
71+
72+
@router.delete("/delete/{patient_id}")
73+
async def delete_patient(patient_id: str, current_doctor: str = Depends(get_current_doctor)):
74+
75+
patient = await Patient.get(patient_id)
76+
77+
if not patient or patient.doctor_id != current_doctor:
78+
raise HTTPException(status_code=404, detail='Patient not found')
79+
80+
await patient.delete()
81+
82+
83+
return JSONResponse(status_code=200, content={'message':'patient deleted'})

0 commit comments

Comments
 (0)