feat: initial commit
This commit is contained in:
commit
80c61dc127
|
|
@ -0,0 +1,108 @@
|
||||||
|
# Created by .ignore support plugin (hsz.mobi)
|
||||||
|
### Python template
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# celery beat schedule file
|
||||||
|
celerybeat-schedule
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
|
||||||
|
.idea/*
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
# 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 and tzdata 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
|
||||||
|
#
|
||||||
|
# Use os.pathsep. Default configuration used for new projects.
|
||||||
|
version_path_separator = os
|
||||||
|
|
||||||
|
# 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 = postgresql://postgres:/*7984@localhost:5432/ai-appointment
|
||||||
|
|
||||||
|
|
||||||
|
[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 = check --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
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from middleware.auth_dependency import auth_required
|
||||||
|
|
||||||
|
from .endpoints import clinics, doctors, calender, appointments, patients, admin, auth
|
||||||
|
|
||||||
|
api_router = APIRouter()
|
||||||
|
# api_router.include_router(twilio.router, prefix="/twilio")
|
||||||
|
api_router.include_router(clinics.router, prefix="/clinics", tags=["clinics"])
|
||||||
|
api_router.include_router(doctors.router, prefix="/doctors", tags=["doctors"])
|
||||||
|
api_router.include_router(calender.router, prefix="/calender", tags=["calender"])
|
||||||
|
api_router.include_router(appointments.router, prefix="/appointments", tags=["appointments"])
|
||||||
|
api_router.include_router(patients.router, prefix="/patients", tags=["patients"])
|
||||||
|
api_router.include_router(admin.router, prefix="/admin", dependencies=[Depends(auth_required)], tags=["admin"])
|
||||||
|
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
from fastapi import APIRouter, status
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", status_code=status.HTTP_200_OK)
|
||||||
|
def get_admin():
|
||||||
|
return {"message": "Admin"}
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
# database
|
||||||
|
from database import get_db
|
||||||
|
from schemas.ResponseSchemas import AppointmentDetailed, AppointmentSchema
|
||||||
|
from models.Appointments import Appointments
|
||||||
|
from schemas.CreateSchemas import AppointmentCreate, AppointmentCreateWithNames
|
||||||
|
from models.Doctors import Doctors
|
||||||
|
from models.Patients import Patients
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/", response_model=List[AppointmentDetailed], status_code=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
def get_appointments(
|
||||||
|
doc_name: str | None = None,
|
||||||
|
patient_name: str | None = None,
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 100,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Get a list of appointments with optional pagination.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
query = db.query(Appointments)
|
||||||
|
if doc_name:
|
||||||
|
query = query.join(Appointments.doctor).filter(
|
||||||
|
Doctors.name.ilike(f"%{doc_name}%")
|
||||||
|
)
|
||||||
|
if patient_name:
|
||||||
|
query = query.join(Appointments.patient).filter(
|
||||||
|
Patients.name.ilike(f"%{patient_name}%")
|
||||||
|
)
|
||||||
|
|
||||||
|
appointments = query.offset(skip).limit(limit).all()
|
||||||
|
|
||||||
|
return appointments
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=str(e.__cause__),
|
||||||
|
) from e
|
||||||
|
|
||||||
|
|
||||||
|
# @router.post("/", response_model=AppointmentSchema, status_code=status.HTTP_201_CREATED)
|
||||||
|
# def create_appointment(appointment: AppointmentCreate, db: Session = Depends(get_db)):
|
||||||
|
# """
|
||||||
|
# Create a new appointment.
|
||||||
|
# """
|
||||||
|
# try:
|
||||||
|
# db_appointment = Appointments(**appointment.model_dump())
|
||||||
|
# db.add(db_appointment)
|
||||||
|
# db.commit()
|
||||||
|
# db.refresh(db_appointment)
|
||||||
|
# return db_appointment
|
||||||
|
# except Exception as e:
|
||||||
|
# db.rollback()
|
||||||
|
# raise HTTPException(
|
||||||
|
# status_code=500,
|
||||||
|
# detail=str(e.__cause__),
|
||||||
|
# ) from e
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/", response_model=AppointmentSchema, status_code=status.HTTP_201_CREATED)
|
||||||
|
def create_appointment_with_names(
|
||||||
|
appointment: AppointmentCreateWithNames, db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Create a new appointment using doctor name and patient name instead of IDs.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Find doctor by name
|
||||||
|
doctor = (
|
||||||
|
db.query(Doctors).filter(Doctors.name == appointment.doctor_name).first()
|
||||||
|
)
|
||||||
|
if not doctor:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"Doctor with name '{appointment.doctor_name}' not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Find patient by name
|
||||||
|
patient = (
|
||||||
|
db.query(Patients).filter(Patients.name == appointment.patient_name).first()
|
||||||
|
)
|
||||||
|
if not patient:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"Patient with name '{appointment.patient_name}' not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create appointment with doctor_id and patient_id
|
||||||
|
db_appointment = Appointments(
|
||||||
|
doctor_id=doctor.id,
|
||||||
|
patient_id=patient.id,
|
||||||
|
appointment_time=appointment.appointment_time,
|
||||||
|
status=appointment.status,
|
||||||
|
)
|
||||||
|
|
||||||
|
db.add(db_appointment)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_appointment)
|
||||||
|
return db_appointment
|
||||||
|
except HTTPException:
|
||||||
|
db.rollback()
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=str(e.__cause__),
|
||||||
|
) from e
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from services.authService import AuthService
|
||||||
|
from schemas.CreateSchemas import UserCreate
|
||||||
|
from schemas.ApiResponse import ApiResponse
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.post("/login")
|
||||||
|
async def login(email: str, password: str):
|
||||||
|
response = await AuthService().login(email, password)
|
||||||
|
return ApiResponse(
|
||||||
|
data=response,
|
||||||
|
message="Login successful"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/register")
|
||||||
|
async def register(user_data: UserCreate):
|
||||||
|
response = await AuthService().register(user_data)
|
||||||
|
return ApiResponse(
|
||||||
|
data=response,
|
||||||
|
message="User registered successfully"
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
# database
|
||||||
|
from database import get_db
|
||||||
|
from models.Calendar import Calenders
|
||||||
|
from schemas.CreateSchemas import CalendarCreate
|
||||||
|
from schemas.ResponseSchemas import Calendar
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_model=List[Calendar])
|
||||||
|
def get_calendar_events(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
|
||||||
|
"""
|
||||||
|
Get a list of calendar events with optional pagination.
|
||||||
|
"""
|
||||||
|
# Placeholder for actual database query
|
||||||
|
events = db.query("CalendarEvents").offset(skip).limit(limit).all()
|
||||||
|
return events
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/", response_model=Calendar, status_code=status.HTTP_201_CREATED)
|
||||||
|
def create_calendar_event(event: CalendarCreate, db: Session = Depends(get_db)):
|
||||||
|
"""
|
||||||
|
Create a new calendar event.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
db_event = Calenders(**event.model_dump())
|
||||||
|
db.add(db_event)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_event)
|
||||||
|
return db_event
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=str(e.__cause__),
|
||||||
|
) from e
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
from asyncio.log import logger
|
||||||
|
from typing import List
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
# database
|
||||||
|
from database import get_db
|
||||||
|
|
||||||
|
# schemas
|
||||||
|
from schemas.ResponseSchemas import Clinic, ClinicWithDoctors
|
||||||
|
from schemas.CreateSchemas import ClinicCreate
|
||||||
|
from schemas.UpdateSchemas import ClinicUpdate
|
||||||
|
from models.Clinics import Clinics
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
from utils.constants import DEFAULT_SKIP, DEFAULT_LIMIT
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_model=List[Clinic])
|
||||||
|
async def get_clinics(
|
||||||
|
skip: int = DEFAULT_SKIP, limit: int = DEFAULT_LIMIT, db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
clinics = db.query(Clinics).offset(skip).limit(limit).all()
|
||||||
|
return clinics
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{clinic_id}", response_model=ClinicWithDoctors)
|
||||||
|
async def get_clinic(clinic_id: int, db: Session = Depends(get_db)):
|
||||||
|
db_clinic = db.query(Clinics).where(Clinics.id == clinic_id).first()
|
||||||
|
if db_clinic is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Clinic not found")
|
||||||
|
return db_clinic
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/", response_model=Clinic, status_code=status.HTTP_201_CREATED)
|
||||||
|
async def create_clinic(clinic: ClinicCreate, db: Session = Depends(get_db)):
|
||||||
|
try:
|
||||||
|
db_clinic = Clinics(**clinic.model_dump())
|
||||||
|
db.add(db_clinic)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_clinic)
|
||||||
|
return db_clinic
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=str(e.__cause__),
|
||||||
|
) from e
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{clinic_id}", response_model=Clinic)
|
||||||
|
async def update_clinic(
|
||||||
|
clinic_id: int, clinic: ClinicUpdate, db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
db_clinic = db.query(Clinics).filter(Clinics.id == clinic_id).first()
|
||||||
|
if db_clinic is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Clinic not found")
|
||||||
|
|
||||||
|
update_data = clinic.model_dump(exclude_unset=True)
|
||||||
|
for key, value in update_data.items():
|
||||||
|
setattr(db_clinic, key, value)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_clinic)
|
||||||
|
return db_clinic
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{clinic_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
async def delete_clinic(clinic_id: int, db: Session = Depends(get_db)):
|
||||||
|
db_clinic = db.query(Clinics).where(Clinics.id == clinic_id).first()
|
||||||
|
if db_clinic is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Clinic not found")
|
||||||
|
|
||||||
|
db.delete(db_clinic)
|
||||||
|
db.commit()
|
||||||
|
return None
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
from asyncio.log import logger
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from typing import List, Dict
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from sqlalchemy import and_
|
||||||
|
|
||||||
|
# database
|
||||||
|
from database import get_db
|
||||||
|
|
||||||
|
# schemas
|
||||||
|
from models.Doctors import Doctors
|
||||||
|
from models.Appointments import Appointments
|
||||||
|
from models.Calendar import Calenders
|
||||||
|
from schemas.ResponseSchemas import (
|
||||||
|
Doctor,
|
||||||
|
DoctorWithAppointments,
|
||||||
|
DoctorWithCalendar,
|
||||||
|
CalendarTimeSchema,
|
||||||
|
)
|
||||||
|
from schemas.CreateSchemas import DoctorCreate
|
||||||
|
from schemas.UpdateSchemas import DoctorUpdate
|
||||||
|
from enums.enums import AppointmentStatus
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/", response_model=Doctor, status_code=status.HTTP_201_CREATED)
|
||||||
|
def create_doctor(doctor: DoctorCreate, db: Session = Depends(get_db)):
|
||||||
|
try:
|
||||||
|
db_doctor = Doctors(**doctor.model_dump())
|
||||||
|
db.add(db_doctor)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_doctor)
|
||||||
|
return db_doctor
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=str(e.__cause__),
|
||||||
|
) from e
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_model=List[DoctorWithCalendar])
|
||||||
|
def read_doctors(
|
||||||
|
doctor_name: str | None = None,
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 100,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
query = db.query(Doctors)
|
||||||
|
if doctor_name:
|
||||||
|
query = query.filter(Doctors.name.ilike(f"%{doctor_name}%"))
|
||||||
|
|
||||||
|
doctors = query.offset(skip).limit(limit).all()
|
||||||
|
return doctors
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/available-slots/{doctor_name}", response_model=Dict[str, List[str]])
|
||||||
|
def get_available_slots(
|
||||||
|
doctor_name: str | None = None,
|
||||||
|
date: str | None = datetime.now().strftime("%Y-%m-%d"),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Get available slots for a doctor on a specific date.
|
||||||
|
date format: YYYY-MM-DD
|
||||||
|
"""
|
||||||
|
# Get the doctor
|
||||||
|
print(f"-----------------doctor_name: {doctor_name}")
|
||||||
|
doctor = db.query(Doctors).filter(Doctors.name.ilike(f"%{doctor_name}%")).first()
|
||||||
|
if not doctor:
|
||||||
|
raise HTTPException(status_code=404, detail="Doctor not found")
|
||||||
|
|
||||||
|
# Get all calendar slots for the doctor
|
||||||
|
calendar_slots = db.query(Calenders).filter(Calenders.doc_id == doctor.id).all()
|
||||||
|
if not calendar_slots:
|
||||||
|
return {"available_slots": []}
|
||||||
|
|
||||||
|
available_slots = [slot.time for slot in calendar_slots]
|
||||||
|
|
||||||
|
try:
|
||||||
|
target_date = datetime.strptime(date, "%Y-%m-%d").date()
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400, detail="Invalid date format. Use YYYY-MM-DD"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get all appointments for the doctor on the specified date
|
||||||
|
appointments = (
|
||||||
|
db.query(Appointments)
|
||||||
|
.filter(
|
||||||
|
and_(
|
||||||
|
Appointments.doctor_id == doctor.id,
|
||||||
|
Appointments.appointment_time >= target_date,
|
||||||
|
Appointments.appointment_time < target_date + timedelta(days=1),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Remove slots that have appointments
|
||||||
|
for appointment in appointments:
|
||||||
|
appointment_time = appointment.appointment_time.strftime("%H:%M")
|
||||||
|
if appointment_time in available_slots and (
|
||||||
|
not appointment.status == AppointmentStatus.COMPLETED
|
||||||
|
):
|
||||||
|
available_slots.remove(appointment_time)
|
||||||
|
|
||||||
|
return {"available_slots": available_slots}
|
||||||
|
|
||||||
|
|
||||||
|
# @router.get("/{doctor_name}", response_model=DoctorWithAppointments)
|
||||||
|
# def read_doctor(doctor_name: str, db: Session = Depends(get_db)):
|
||||||
|
# db_doctor = db.query(Doctors).filter(Doctors.name.ilike(f"%{doctor_name}%")).all()
|
||||||
|
# return db_doctor
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{doctor_id}", response_model=Doctor)
|
||||||
|
def update_doctor(doctor_id: int, doctor: DoctorUpdate, db: Session = Depends(get_db)):
|
||||||
|
db_doctor = db.query(Doctors).filter(Doctors.id == doctor_id).first()
|
||||||
|
if db_doctor is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Doctor not found")
|
||||||
|
|
||||||
|
update_data = doctor.model_dump(exclude_unset=True)
|
||||||
|
for key, value in update_data.items():
|
||||||
|
setattr(db_doctor, key, value)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_doctor)
|
||||||
|
return db_doctor
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{doctor_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
def delete_doctor(doctor_id: int, db: Session = Depends(get_db)):
|
||||||
|
db_doctor = db.query(Doctors).filter(Doctors.id == doctor_id).first()
|
||||||
|
if db_doctor is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Doctor not found")
|
||||||
|
|
||||||
|
db.delete(db_doctor)
|
||||||
|
db.commit()
|
||||||
|
return None
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
# database
|
||||||
|
from database import get_db
|
||||||
|
from models.Patients import Patients
|
||||||
|
from schemas.CreateSchemas import PatientCreate
|
||||||
|
from schemas.ResponseSchemas import Patient
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_model=List[Patient])
|
||||||
|
def read_patients(
|
||||||
|
name: str | None = None,
|
||||||
|
dob: str | None = None,
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 100,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Get a list of patients with optional pagination.
|
||||||
|
"""
|
||||||
|
query = db.query(Patients)
|
||||||
|
if name:
|
||||||
|
query = query.filter(Patients.name.ilike(f"%{name}%"))
|
||||||
|
if dob:
|
||||||
|
query = query.filter(Patients.dob == dob)
|
||||||
|
patients = query.offset(skip).limit(limit).all()
|
||||||
|
return patients
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/", response_model=Patient, status_code=status.HTTP_201_CREATED)
|
||||||
|
def create_patient(patient: PatientCreate, db: Session = Depends(get_db)):
|
||||||
|
"""
|
||||||
|
Create a new patient.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
db_patient = Patients(**patient.model_dump())
|
||||||
|
db.add(db_patient)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_patient)
|
||||||
|
return db_patient
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=str(e.__cause__),
|
||||||
|
) from e
|
||||||
|
|
@ -0,0 +1,183 @@
|
||||||
|
# import asyncio
|
||||||
|
# import json
|
||||||
|
# import logging
|
||||||
|
# import os
|
||||||
|
# from fastapi import APIRouter, Request, WebSocket, status
|
||||||
|
# from twilio.twiml.voice_response import VoiceResponse, Connect
|
||||||
|
# from twilio.rest import Client
|
||||||
|
# from fastapi import WebSocket, Request, Response
|
||||||
|
# from enum import Enum
|
||||||
|
# from typing import Optional
|
||||||
|
|
||||||
|
# from services.bot import run_bot
|
||||||
|
# from services.call_state import CallState
|
||||||
|
|
||||||
|
# logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# router = APIRouter()
|
||||||
|
|
||||||
|
# BASE_WS_URL = "wss://13.229.247.61:8000/api/twilio"
|
||||||
|
# BASE_URL = "http://13.229.247.61:8000/api/twilio"
|
||||||
|
|
||||||
|
# DTMF_SWITCH_KEY = "*" # Key to switch between models
|
||||||
|
|
||||||
|
# LOG_EVENT_TYPES = [
|
||||||
|
# "error",
|
||||||
|
# "response.content.done",
|
||||||
|
# "rate_limits.updated",
|
||||||
|
# "response.done",
|
||||||
|
# "input_audio_buffer.committed",
|
||||||
|
# "input_audio_buffer.speech_stopped",
|
||||||
|
# "input_audio_buffer.speech_started",
|
||||||
|
# "session.created",
|
||||||
|
# ]
|
||||||
|
|
||||||
|
# SHOW_TIMING_MATH = False
|
||||||
|
|
||||||
|
# MENU_OPTIONS = """
|
||||||
|
# Press 1 for Model 1.
|
||||||
|
# Press 2 for Model 2.
|
||||||
|
# Press 3 for Model 3.
|
||||||
|
# Press 4 for Model 4.
|
||||||
|
# Press 5 for Model 5.
|
||||||
|
# Press 0 to repeat the options.
|
||||||
|
# """
|
||||||
|
|
||||||
|
# call_state = CallState
|
||||||
|
|
||||||
|
|
||||||
|
# # Define model options as enum for type safety
|
||||||
|
# class ModelOption(int, Enum):
|
||||||
|
# MODEL_1 = 1
|
||||||
|
# MODEL_2 = 2
|
||||||
|
# MODEL_3 = 3
|
||||||
|
# MODEL_4 = 4
|
||||||
|
# MODEL_5 = 5
|
||||||
|
|
||||||
|
|
||||||
|
# @router.post("/call")
|
||||||
|
# async def get_call():
|
||||||
|
|
||||||
|
# SID = os.getenv("TWILIO_SID")
|
||||||
|
# AUTH_TOKEN = os.getenv("TWILIO_AUTH")
|
||||||
|
|
||||||
|
# client = Client(SID, AUTH_TOKEN)
|
||||||
|
|
||||||
|
# call = client.calls.create(
|
||||||
|
# from_="+14149466486",
|
||||||
|
# to="+917984372159",
|
||||||
|
# record=True,
|
||||||
|
# url=f"{BASE_URL}/receive",
|
||||||
|
# )
|
||||||
|
|
||||||
|
# return status.HTTP_200_OK
|
||||||
|
|
||||||
|
|
||||||
|
# # @router.websocket("/media-stream/{id}")
|
||||||
|
# # async def handle_media_stream(websocket: WebSocket, id: int):
|
||||||
|
# # """Handle WebSocket connections between Twilio and OpenAI."""
|
||||||
|
# # print(f"Client connected with id: {id}")
|
||||||
|
# # await websocket.accept()
|
||||||
|
# # start_data = websocket.iter_text()
|
||||||
|
# # await start_data.__anext__()
|
||||||
|
# # call_data = json.loads(await start_data.__anext__())
|
||||||
|
# # print(call_data, flush=True)
|
||||||
|
# # stream_sid = call_data["start"]["streamSid"]
|
||||||
|
# # print("WebSocket connection accepted")
|
||||||
|
# # await run_bot(websocket, stream_sid, False, option=id)
|
||||||
|
|
||||||
|
|
||||||
|
# @router.websocket("/media-stream/{id}")
|
||||||
|
# async def handle_media_stream(websocket: WebSocket, id: int):
|
||||||
|
# """Handle WebSocket connections between Twilio and OpenAI."""
|
||||||
|
# logger.info(f"Client connected with id: {id}")
|
||||||
|
# print(f"Client connected with id: {id}")
|
||||||
|
# await websocket.accept()
|
||||||
|
# start_data = websocket.iter_text()
|
||||||
|
# await start_data.__anext__()
|
||||||
|
# call_data = json.loads(await start_data.__anext__())
|
||||||
|
# print(call_data, flush=True)
|
||||||
|
# logger.info(call_data)
|
||||||
|
# stream_sid = call_data["start"]["streamSid"]
|
||||||
|
# print("WebSocket connection accepted")
|
||||||
|
# logger.info("WebSocket connection accepted")
|
||||||
|
# await run_bot(websocket, stream_sid, False, option=id)
|
||||||
|
|
||||||
|
|
||||||
|
# # @router.post("/receive")
|
||||||
|
# # async def receive_call(req: Request):
|
||||||
|
# # print("----------------- received call -----------------")
|
||||||
|
|
||||||
|
# # resp = VoiceResponse()
|
||||||
|
# # connect = Connect()
|
||||||
|
# # connect.stream(
|
||||||
|
# # url=f"wss://allegedly-known-wasp.ngrok-free.app/api/twilio/media-stream"
|
||||||
|
# # )
|
||||||
|
# # resp.append(connect)
|
||||||
|
# # return Response(content=str(resp), media_type="application/xml")
|
||||||
|
|
||||||
|
|
||||||
|
# @router.post("/receive")
|
||||||
|
# async def receive_call(req: Request):
|
||||||
|
# print("----------------- received call -----------------")
|
||||||
|
# resp = VoiceResponse()
|
||||||
|
|
||||||
|
# # Gather DTMF input
|
||||||
|
# gather = resp.gather(
|
||||||
|
# num_digits=1, action="/api/twilio/handle-key", method="POST", timeout=10
|
||||||
|
# )
|
||||||
|
|
||||||
|
# # Initial greeting and menu options
|
||||||
|
# gather.say(MENU_OPTIONS)
|
||||||
|
|
||||||
|
# # If no input received, redirect back to the main menu
|
||||||
|
# resp.redirect("/api/twilio/receive")
|
||||||
|
|
||||||
|
# return Response(content=str(resp), media_type="application/xml")
|
||||||
|
|
||||||
|
|
||||||
|
# @router.post("/handle-key")
|
||||||
|
# async def handle_key_press(req: Request):
|
||||||
|
# """Process the key pressed by the caller and connect to the appropriate model."""
|
||||||
|
# try:
|
||||||
|
# form_data = await req.form()
|
||||||
|
# digits_pressed = form_data.get("Digits", "")
|
||||||
|
# print(f"User pressed: {digits_pressed}")
|
||||||
|
|
||||||
|
# resp = VoiceResponse()
|
||||||
|
|
||||||
|
# if digits_pressed == "0":
|
||||||
|
# # Repeat options
|
||||||
|
# resp.redirect("/api/twilio/receive")
|
||||||
|
# elif digits_pressed in "12345":
|
||||||
|
# # Valid model selection
|
||||||
|
# model_id = int(digits_pressed)
|
||||||
|
# resp.say(f"You have selected model {model_id}.")
|
||||||
|
|
||||||
|
# # Connect to WebSocket
|
||||||
|
# connect = Connect()
|
||||||
|
# connect.stream(url=f"{BASE_WS_URL}/media-stream/{model_id}")
|
||||||
|
# resp.append(connect)
|
||||||
|
# else:
|
||||||
|
# # Invalid selection
|
||||||
|
# resp.say("Invalid selection. Please try again.")
|
||||||
|
# resp.redirect("/api/twilio/receive")
|
||||||
|
|
||||||
|
# return Response(content=str(resp), media_type="application/xml")
|
||||||
|
|
||||||
|
# except Exception as e:
|
||||||
|
# print(f"Error handling key press: {str(e)}")
|
||||||
|
# resp = VoiceResponse()
|
||||||
|
# resp.say(
|
||||||
|
# "We encountered a problem processing your request. Please try again later."
|
||||||
|
# )
|
||||||
|
# return Response(content=str(resp), media_type="application/xml")
|
||||||
|
|
||||||
|
|
||||||
|
# @router.post("/error")
|
||||||
|
# async def read_item(req: Request):
|
||||||
|
# print(await req.body())
|
||||||
|
# print(await req.form())
|
||||||
|
# logger.error(await req.body())
|
||||||
|
# return status.HTTP_200_OK
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import dotenv
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
import os
|
||||||
|
|
||||||
|
dotenv.load_dotenv()
|
||||||
|
|
||||||
|
engine = create_engine(
|
||||||
|
os.getenv("DB_URL"),
|
||||||
|
pool_pre_ping=True, # Check connection before using it
|
||||||
|
pool_size=5, # Connection pool size
|
||||||
|
max_overflow=10, # Max extra connections when pool is full
|
||||||
|
pool_recycle=3600, # Recycle connections after 1 hour
|
||||||
|
echo=True, # Log SQL queries
|
||||||
|
)
|
||||||
|
|
||||||
|
Base = declarative_base() # Base class for ORM models
|
||||||
|
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class AppointmentStatus(Enum):
|
||||||
|
PENDING = "pending"
|
||||||
|
CONFIRMED = "confirmed"
|
||||||
|
CANCELLED = "cancelled"
|
||||||
|
COMPLETED = "completed"
|
||||||
|
|
||||||
|
|
||||||
|
class ClinicStatus(Enum):
|
||||||
|
ACTIVE = "active"
|
||||||
|
INACTIVE = "inactive"
|
||||||
|
UNDER_REVIEW = "under_review"
|
||||||
|
REQUESTED_DOCTOR = "requested_doctor"
|
||||||
|
REJECTED = "rejected"
|
||||||
|
PAYMENT_DUE = "payment_due"
|
||||||
|
|
||||||
|
class ClinicUserRoles(Enum):
|
||||||
|
DIRECTOR = "director"
|
||||||
|
PRACTICE_MANAGER = "practice_manager"
|
||||||
|
|
||||||
|
class ClinicDoctorStatus(Enum):
|
||||||
|
ACTIVE = "active"
|
||||||
|
INACTIVE = "inactive"
|
||||||
|
|
||||||
|
class ClinicDoctorType(Enum):
|
||||||
|
DOCTOR = "doctor"
|
||||||
|
NURSE = "nurse"
|
||||||
|
|
||||||
|
class UserType(Enum):
|
||||||
|
SUPER_ADMIN = "super_admin"
|
||||||
|
CLINIC_ADMIN = "clinic_admin"
|
||||||
|
|
||||||
|
class Integration(Enum):
|
||||||
|
BP = "bp"
|
||||||
|
MEDICAL_DIRECTOR = "medical_director"
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
from .api_exceptions import ApiException
|
||||||
|
from .business_exception import BusinessValidationException
|
||||||
|
from .validation_exception import ValidationException
|
||||||
|
from .forbidden_exception import ForbiddenException
|
||||||
|
from .internal_server_error_exception import InternalServerErrorException
|
||||||
|
from .resource_not_found_exception import ResourceNotFoundException
|
||||||
|
from .unauthorized_exception import UnauthorizedException
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"ApiException",
|
||||||
|
"BusinessValidationException",
|
||||||
|
"ValidationException",
|
||||||
|
"ForbiddenException",
|
||||||
|
"InternalServerErrorException",
|
||||||
|
"ResourceNotFoundException",
|
||||||
|
"UnauthorizedException",
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
class ApiException(Exception):
|
||||||
|
"""Base API exception class for HTTP errors."""
|
||||||
|
|
||||||
|
def __init__(self, status_code: int, message: str):
|
||||||
|
self.status_code = status_code
|
||||||
|
self.message = message
|
||||||
|
super().__init__(self.message)
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
class BusinessValidationException(Exception):
|
||||||
|
"""Exception for business logic validation errors."""
|
||||||
|
|
||||||
|
def __init__(self, message: str):
|
||||||
|
self.message = message
|
||||||
|
super().__init__(self.message)
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
from http import HTTPStatus
|
||||||
|
from .api_exceptions import ApiException
|
||||||
|
|
||||||
|
|
||||||
|
class ForbiddenException(ApiException):
|
||||||
|
"""Exception for forbidden access errors."""
|
||||||
|
|
||||||
|
def __init__(self, message: str = "Forbidden"):
|
||||||
|
super().__init__(HTTPStatus.FORBIDDEN, message)
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
from http import HTTPStatus
|
||||||
|
from .api_exceptions import ApiException
|
||||||
|
|
||||||
|
|
||||||
|
class InternalServerErrorException(ApiException):
|
||||||
|
"""Exception for internal server errors."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(
|
||||||
|
HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
"An unexpected error has occurred. Please contact the administrator."
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
from http import HTTPStatus
|
||||||
|
from .api_exceptions import ApiException
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceNotFoundException(ApiException):
|
||||||
|
"""Exception for resource not found errors."""
|
||||||
|
|
||||||
|
def __init__(self, message: str):
|
||||||
|
super().__init__(HTTPStatus.NOT_FOUND, message)
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
from http import HTTPStatus
|
||||||
|
from .api_exceptions import ApiException
|
||||||
|
|
||||||
|
|
||||||
|
class UnauthorizedException(ApiException):
|
||||||
|
"""Exception for unauthorized access errors."""
|
||||||
|
|
||||||
|
def __init__(self, message: str = "Failed to authenticate."):
|
||||||
|
super().__init__(HTTPStatus.UNAUTHORIZED, message)
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
class ValidationException(Exception):
|
||||||
|
"""Exception for data validation errors."""
|
||||||
|
|
||||||
|
def __init__(self, message: str):
|
||||||
|
self.message = message
|
||||||
|
super().__init__(self.message)
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
import dotenv
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
import logging
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
|
# db
|
||||||
|
from database import Base, engine
|
||||||
|
|
||||||
|
# IMPORTANT: Import all models to register them with SQLAlchemy
|
||||||
|
# from models.Clinics import Clinics
|
||||||
|
# from models.Doctors import Doctors
|
||||||
|
# from models.Patients import Patients
|
||||||
|
# from models.Appointments import Appointments
|
||||||
|
# from models.Calendar import Calenders
|
||||||
|
|
||||||
|
# routers
|
||||||
|
from apis import api_router
|
||||||
|
|
||||||
|
# middleware
|
||||||
|
from middleware.ErrorHandlerMiddleware import ErrorHandlerMiddleware, configure_exception_handlers
|
||||||
|
|
||||||
|
dotenv.load_dotenv()
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
logger.info("Starting application")
|
||||||
|
try:
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
logger.info("Created database tables")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error creating database tables: {e}")
|
||||||
|
raise e
|
||||||
|
yield
|
||||||
|
logger.info("Stopping application")
|
||||||
|
|
||||||
|
|
||||||
|
app = FastAPI(
|
||||||
|
lifespan=lifespan,
|
||||||
|
)
|
||||||
|
|
||||||
|
# CORS middleware
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"], # Allows all origins
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"], # Allows all methods
|
||||||
|
allow_headers=["*"], # Allows all headers
|
||||||
|
)
|
||||||
|
|
||||||
|
# Error handler middleware
|
||||||
|
app.add_middleware(ErrorHandlerMiddleware)
|
||||||
|
|
||||||
|
# Configure exception handlers
|
||||||
|
configure_exception_handlers(app)
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def hello_world():
|
||||||
|
return {"Hello": "World"}
|
||||||
|
|
||||||
|
|
||||||
|
# Routes
|
||||||
|
app.include_router(api_router, prefix="/api")
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
from fastapi import Request, status
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from fastapi.exceptions import RequestValidationError
|
||||||
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
|
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from exceptions import (
|
||||||
|
ApiException,
|
||||||
|
BusinessValidationException,
|
||||||
|
ValidationException
|
||||||
|
)
|
||||||
|
from schemas.ApiResponse import ApiResponse
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorHandlerMiddleware(BaseHTTPMiddleware):
|
||||||
|
"""Middleware for handling exceptions globally in the application."""
|
||||||
|
|
||||||
|
async def dispatch(self, request: Request, call_next):
|
||||||
|
try:
|
||||||
|
return await call_next(request)
|
||||||
|
except Exception as exc:
|
||||||
|
return self.handle_exception(exc)
|
||||||
|
|
||||||
|
def handle_exception(self, exc: Exception) -> JSONResponse:
|
||||||
|
if isinstance(exc, ApiException):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=exc.status_code,
|
||||||
|
content=ApiResponse.from_api_exception(exc)
|
||||||
|
)
|
||||||
|
elif isinstance(exc, StarletteHTTPException):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=exc.status_code,
|
||||||
|
content=ApiResponse(
|
||||||
|
message=str(exc.detail),
|
||||||
|
error=str(traceback.format_exc())
|
||||||
|
).model_dump(exclude_none=True)
|
||||||
|
)
|
||||||
|
elif isinstance(exc, (ValidationException, BusinessValidationException)):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
content=ApiResponse(
|
||||||
|
message=str(exc),
|
||||||
|
error=str(traceback.format_exc())
|
||||||
|
).model_dump(exclude_none=True)
|
||||||
|
)
|
||||||
|
elif isinstance(exc, RequestValidationError):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||||
|
content=ApiResponse(
|
||||||
|
message="Validation error",
|
||||||
|
error=str(exc.errors())
|
||||||
|
).model_dump(exclude_none=True)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
content=ApiResponse(
|
||||||
|
message=str(exc),
|
||||||
|
error=str(traceback.format_exc())
|
||||||
|
).model_dump(exclude_none=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Exception handlers for FastAPI
|
||||||
|
def configure_exception_handlers(app):
|
||||||
|
"""Configure exception handlers for the FastAPI application."""
|
||||||
|
|
||||||
|
@app.exception_handler(ApiException)
|
||||||
|
async def api_exception_handler(request: Request, exc: ApiException):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=exc.status_code,
|
||||||
|
content=ApiResponse.from_api_exception(exc)
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.exception_handler(RequestValidationError)
|
||||||
|
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||||
|
content=ApiResponse(
|
||||||
|
message="Validation error",
|
||||||
|
error=str(exc.errors())
|
||||||
|
).model_dump(exclude_none=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.exception_handler(StarletteHTTPException)
|
||||||
|
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=exc.status_code,
|
||||||
|
content=ApiResponse(
|
||||||
|
message=str(exc.detail),
|
||||||
|
error=str(traceback.format_exc())
|
||||||
|
).model_dump(exclude_none=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.exception_handler(BusinessValidationException)
|
||||||
|
async def business_validation_exception_handler(request: Request, exc: BusinessValidationException):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
content=ApiResponse(
|
||||||
|
message=str(exc),
|
||||||
|
error=str(traceback.format_exc())
|
||||||
|
).model_dump(exclude_none=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.exception_handler(ValidationException)
|
||||||
|
async def custom_validation_exception_handler(request: Request, exc: ValidationException):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
content=ApiResponse(
|
||||||
|
message=str(exc),
|
||||||
|
error=str(traceback.format_exc())
|
||||||
|
).model_dump(exclude_none=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.exception_handler(Exception)
|
||||||
|
async def general_exception_handler(request: Request, exc: Exception):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
content=ApiResponse(
|
||||||
|
message=str(exc),
|
||||||
|
error=str(traceback.format_exc())
|
||||||
|
).model_dump(exclude_none=True)
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
from fastapi import HTTPException, Depends
|
||||||
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||||
|
from services.jwtService import verify_jwt_token
|
||||||
|
from services.userServices import UserServices
|
||||||
|
from fastapi import Request
|
||||||
|
|
||||||
|
security = HTTPBearer()
|
||||||
|
|
||||||
|
async def auth_required(request: Request ,credentials: HTTPAuthorizationCredentials = Depends(security)):
|
||||||
|
"""
|
||||||
|
Dependency function to verify JWT token for protected routes
|
||||||
|
"""
|
||||||
|
if credentials.scheme != "Bearer":
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid authentication scheme")
|
||||||
|
|
||||||
|
payload = verify_jwt_token(credentials.credentials)
|
||||||
|
if payload is None:
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid authentication token")
|
||||||
|
|
||||||
|
# Get user from database
|
||||||
|
user = UserServices().get_user(payload["user_id"])
|
||||||
|
|
||||||
|
# set user to request state
|
||||||
|
request.state.user = user
|
||||||
|
request.state.payload = payload
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Generic single-database configuration.
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
from logging.config import fileConfig
|
||||||
|
|
||||||
|
from sqlalchemy import engine_from_config
|
||||||
|
from sqlalchemy import pool
|
||||||
|
|
||||||
|
from alembic import context
|
||||||
|
|
||||||
|
# this is the Alembic Config object, which provides
|
||||||
|
# access to the values within the .ini file in use.
|
||||||
|
config = context.config
|
||||||
|
|
||||||
|
# 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
|
||||||
|
from database import Base, engine
|
||||||
|
|
||||||
|
from models import *
|
||||||
|
|
||||||
|
# for 'autogenerate' support
|
||||||
|
# from myapp import mymodel
|
||||||
|
|
||||||
|
# target_metadata = mymodel.Base.metadata
|
||||||
|
target_metadata = Base.metadata
|
||||||
|
|
||||||
|
# other values from the config, defined by the needs of env.py,
|
||||||
|
# can be acquired:
|
||||||
|
# my_important_option = config.get_main_option("my_important_option")
|
||||||
|
# ... etc.
|
||||||
|
|
||||||
|
|
||||||
|
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"},
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
if context.is_offline_mode():
|
||||||
|
run_migrations_offline()
|
||||||
|
else:
|
||||||
|
run_migrations_online()
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
"""${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:
|
||||||
|
"""Upgrade schema."""
|
||||||
|
${upgrades if upgrades else "pass"}
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
|
${downgrades if downgrades else "pass"}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
from sqlalchemy import Column, DateTime, Enum, Integer, ForeignKey
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
|
from enums.enums import AppointmentStatus
|
||||||
|
from database import Base
|
||||||
|
from .CustomBase import CustomBase
|
||||||
|
|
||||||
|
|
||||||
|
class Appointments(Base, CustomBase):
|
||||||
|
__tablename__ = "appointments"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
appointment_time = Column(DateTime)
|
||||||
|
status = Column(Enum(AppointmentStatus))
|
||||||
|
|
||||||
|
doctor_id = Column(Integer, ForeignKey("doctors.id"), index=True)
|
||||||
|
doctor = relationship("Doctors", back_populates="appointments")
|
||||||
|
|
||||||
|
patient_id = Column(Integer, ForeignKey("patients.id"), index=True)
|
||||||
|
patient = relationship("Patients", back_populates="appointments")
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
from sqlalchemy import Column, Integer, String, ForeignKey
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
|
from database import Base
|
||||||
|
from .CustomBase import CustomBase
|
||||||
|
|
||||||
|
|
||||||
|
class Calenders(Base, CustomBase):
|
||||||
|
__tablename__ = "calenders"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
doc_id = Column(Integer, ForeignKey("doctors.id"), nullable=False, index=True)
|
||||||
|
# rrule = Column(String)
|
||||||
|
time = Column(String)
|
||||||
|
|
||||||
|
doctor = relationship("Doctors", back_populates="calendars")
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
from sqlalchemy import Column, Integer, String
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
|
from database import Base
|
||||||
|
from .CustomBase import CustomBase
|
||||||
|
|
||||||
|
|
||||||
|
class Clinics(Base, CustomBase):
|
||||||
|
__tablename__ = "clinics"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
name = Column(String)
|
||||||
|
address = Column(String, nullable=True)
|
||||||
|
phone = Column(String, unique=True, index=True)
|
||||||
|
email = Column(String, unique=True, index=True, nullable=True)
|
||||||
|
|
||||||
|
doctors = relationship("Doctors", back_populates="clinic")
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
from sqlalchemy import Column, DateTime, func
|
||||||
|
|
||||||
|
|
||||||
|
class CustomBase:
|
||||||
|
create_time = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
update_time = Column(
|
||||||
|
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
from sqlalchemy import Column, Integer, String, ForeignKey
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
|
from database import Base
|
||||||
|
from .CustomBase import CustomBase
|
||||||
|
|
||||||
|
|
||||||
|
class Doctors(Base, CustomBase):
|
||||||
|
__tablename__ = "doctors"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
name = Column(String)
|
||||||
|
age = Column(Integer, nullable=True)
|
||||||
|
email = Column(String, unique=True, index=True, nullable=True)
|
||||||
|
phone = Column(String, unique=True, index=True)
|
||||||
|
address = Column(String, nullable=True)
|
||||||
|
|
||||||
|
clinic_id = Column(Integer, ForeignKey("clinics.id"), nullable=False, index=True)
|
||||||
|
clinic = relationship("Clinics", back_populates="doctors")
|
||||||
|
|
||||||
|
appointments = relationship("Appointments", back_populates="doctor")
|
||||||
|
calendars = relationship("Calenders", back_populates="doctor")
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
from sqlalchemy import Column, Integer, String
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
|
from database import Base
|
||||||
|
from .CustomBase import CustomBase
|
||||||
|
|
||||||
|
|
||||||
|
class Patients(Base, CustomBase):
|
||||||
|
__tablename__ = "patients"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
name = Column(String)
|
||||||
|
age = Column(Integer, nullable=True)
|
||||||
|
email = Column(String, unique=True, index=True, nullable=True)
|
||||||
|
phone = Column(String, unique=True, index=True)
|
||||||
|
address = Column(String, nullable=True)
|
||||||
|
dob = Column(String, nullable=True)
|
||||||
|
|
||||||
|
appointments = relationship("Appointments", back_populates="patient")
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
from sqlalchemy import Column, Integer, String
|
||||||
|
from database import Base
|
||||||
|
from sqlalchemy import Enum
|
||||||
|
from enums.enums import ClinicUserRoles, UserType
|
||||||
|
from models.CustomBase import CustomBase
|
||||||
|
|
||||||
|
class Users(Base, CustomBase):
|
||||||
|
__tablename__ = "users"
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
username = Column(String, index=True)
|
||||||
|
email = Column(String, unique=True, index=True)
|
||||||
|
password = Column(String)
|
||||||
|
clinicRole = Column(Enum(ClinicUserRoles), nullable=True)
|
||||||
|
userType = Column(Enum(UserType), nullable=True)
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
from .Users import Users
|
||||||
|
from .Clinics import Clinics
|
||||||
|
from .Doctors import Doctors
|
||||||
|
from .Patients import Patients
|
||||||
|
from .Appointments import Appointments
|
||||||
|
from .Calendar import Calenders
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Users",
|
||||||
|
"Clinics",
|
||||||
|
"Doctors",
|
||||||
|
"Patients",
|
||||||
|
"Appointments",
|
||||||
|
"Calenders",
|
||||||
|
]
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,25 @@
|
||||||
|
from typing import Any, Optional, TypeVar, Generic
|
||||||
|
from pydantic import Field
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from exceptions import ApiException
|
||||||
|
|
||||||
|
|
||||||
|
T = TypeVar('T')
|
||||||
|
|
||||||
|
class ApiResponse(BaseModel, Generic[T]):
|
||||||
|
"""Standard API response model matching Node.js implementation."""
|
||||||
|
|
||||||
|
data: Optional[T] = Field(default=None, description="Response data")
|
||||||
|
error: Optional[Any] = Field(default=None, description="Error details")
|
||||||
|
message: Optional[str] = Field(default=None, description="Response message")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_api_exception(cls, exception: ApiException) -> dict:
|
||||||
|
"""Create an API response from an API exception."""
|
||||||
|
import traceback
|
||||||
|
return cls(
|
||||||
|
data=None,
|
||||||
|
message=exception.message,
|
||||||
|
error=traceback.format_exc() if exception else None
|
||||||
|
).model_dump(exclude_none=True)
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
# schemas.py
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import List, Optional
|
||||||
|
from pydantic import BaseModel, EmailStr
|
||||||
|
from enums.enums import AppointmentStatus, ClinicUserRoles, UserType
|
||||||
|
|
||||||
|
|
||||||
|
# Base schemas (shared attributes for create/read operations)
|
||||||
|
class ClinicBase(BaseModel):
|
||||||
|
name: str
|
||||||
|
address: Optional[str] = None
|
||||||
|
phone: str
|
||||||
|
email: Optional[EmailStr] = None
|
||||||
|
|
||||||
|
|
||||||
|
class DoctorBase(BaseModel):
|
||||||
|
name: str
|
||||||
|
age: Optional[int] = None
|
||||||
|
email: Optional[EmailStr] = None
|
||||||
|
phone: str
|
||||||
|
address: Optional[str] = None
|
||||||
|
clinic_id: int
|
||||||
|
|
||||||
|
|
||||||
|
class PatientBase(BaseModel):
|
||||||
|
name: str
|
||||||
|
age: Optional[int] = None
|
||||||
|
email: Optional[EmailStr] = None
|
||||||
|
phone: Optional[str] = None
|
||||||
|
address: Optional[str] = None
|
||||||
|
dob: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class AppointmentBase(BaseModel):
|
||||||
|
doctor_id: int
|
||||||
|
patient_id: int
|
||||||
|
appointment_time: datetime
|
||||||
|
status: AppointmentStatus = AppointmentStatus.CONFIRMED
|
||||||
|
|
||||||
|
|
||||||
|
class CalendarBase(BaseModel):
|
||||||
|
doc_id: int
|
||||||
|
# rrule: str # Recurrence rule in iCalendar format
|
||||||
|
time: str
|
||||||
|
|
||||||
|
|
||||||
|
class UserBase(BaseModel):
|
||||||
|
username: str
|
||||||
|
email: EmailStr
|
||||||
|
password: str
|
||||||
|
clinicRole: Optional[ClinicUserRoles] = None
|
||||||
|
userType: Optional[UserType] = None
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
from .BaseSchemas import *
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
from enums.enums import AppointmentStatus
|
||||||
|
|
||||||
|
|
||||||
|
# Create schemas (used for creating new records)
|
||||||
|
class ClinicCreate(ClinicBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DoctorCreate(DoctorBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PatientCreate(PatientBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AppointmentCreate(AppointmentBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CalendarCreate(CalendarBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AppointmentCreateWithNames(BaseModel):
|
||||||
|
doctor_name: str
|
||||||
|
patient_name: str
|
||||||
|
appointment_time: datetime
|
||||||
|
status: AppointmentStatus = AppointmentStatus.CONFIRMED
|
||||||
|
|
||||||
|
|
||||||
|
class UserCreate(UserBase):
|
||||||
|
pass
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import List
|
||||||
|
from .BaseSchemas import *
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
# Response schemas (used for API responses)
|
||||||
|
class Clinic(ClinicBase):
|
||||||
|
id: int
|
||||||
|
create_time: datetime
|
||||||
|
update_time: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
class UserResponse(UserBase):
|
||||||
|
id: int
|
||||||
|
create_time: datetime
|
||||||
|
update_time: datetime
|
||||||
|
password: str = Field(exclude=True)
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
allow_population_by_field_name = True
|
||||||
|
|
||||||
|
class Doctor(DoctorBase):
|
||||||
|
id: int
|
||||||
|
create_time: datetime
|
||||||
|
update_time: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
class Patient(PatientBase):
|
||||||
|
id: int
|
||||||
|
create_time: datetime
|
||||||
|
update_time: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
class AppointmentSchema(AppointmentBase):
|
||||||
|
id: int
|
||||||
|
create_time: datetime
|
||||||
|
update_time: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
class Calendar(CalendarBase):
|
||||||
|
id: int
|
||||||
|
create_time: datetime
|
||||||
|
update_time: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
# custom schema for response
|
||||||
|
class CalendarTimeSchema(BaseModel):
|
||||||
|
time: str
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
class ClinicSchema(BaseModel):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
address: str
|
||||||
|
phone: str
|
||||||
|
email: str
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
# Detailed response schemas with nested relationships
|
||||||
|
class ClinicWithDoctors(Clinic):
|
||||||
|
doctors: List[Doctor] = []
|
||||||
|
|
||||||
|
|
||||||
|
class DoctorWithAppointments(Doctor):
|
||||||
|
appointments: List[AppointmentSchema] = []
|
||||||
|
calendars: List[CalendarTimeSchema] = []
|
||||||
|
clinic: ClinicSchema
|
||||||
|
|
||||||
|
|
||||||
|
class DoctorWithCalendar(Doctor):
|
||||||
|
calendars: List[CalendarTimeSchema] = []
|
||||||
|
clinic: ClinicSchema
|
||||||
|
|
||||||
|
|
||||||
|
class PatientWithAppointments(Patient):
|
||||||
|
appointments: List[AppointmentSchema] = []
|
||||||
|
|
||||||
|
|
||||||
|
class AppointmentDetailed(AppointmentSchema):
|
||||||
|
|
||||||
|
class Doctor(BaseModel):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
age: int
|
||||||
|
email: str
|
||||||
|
phone: str
|
||||||
|
address: str
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
class Patient(BaseModel):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
age: int
|
||||||
|
email: str
|
||||||
|
phone: str
|
||||||
|
address: str
|
||||||
|
dob: str
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
doctor: Doctor
|
||||||
|
patient: Patient
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
from .BaseSchemas import *
|
||||||
|
|
||||||
|
|
||||||
|
# Update schemas (all fields optional for partial updates)
|
||||||
|
class ClinicUpdate(BaseModel):
|
||||||
|
name: Optional[str] = None
|
||||||
|
address: Optional[str] = None
|
||||||
|
phone: Optional[str] = None
|
||||||
|
email: Optional[EmailStr] = None
|
||||||
|
|
||||||
|
|
||||||
|
class DoctorUpdate(BaseModel):
|
||||||
|
name: Optional[str] = None
|
||||||
|
age: Optional[int] = None
|
||||||
|
email: Optional[EmailStr] = None
|
||||||
|
phone: Optional[str] = None
|
||||||
|
address: Optional[str] = None
|
||||||
|
clinic_id: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
class PatientUpdate(BaseModel):
|
||||||
|
name: Optional[str] = None
|
||||||
|
age: Optional[int] = None
|
||||||
|
email: Optional[EmailStr] = None
|
||||||
|
phone: Optional[str] = None
|
||||||
|
address: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class AppointmentUpdate(BaseModel):
|
||||||
|
doctor_id: Optional[int] = None
|
||||||
|
patient_id: Optional[int] = None
|
||||||
|
appointment_time: Optional[datetime] = None
|
||||||
|
status: Optional[AppointmentStatus] = None
|
||||||
|
|
||||||
|
|
||||||
|
class CalendarUpdate(BaseModel):
|
||||||
|
doc_id: Optional[int] = None
|
||||||
|
rrule: Optional[str] = None
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
from database import get_db
|
||||||
|
from services.jwtService import create_jwt_token
|
||||||
|
from services.userServices import UserServices
|
||||||
|
from utils.password_utils import verify_password
|
||||||
|
from schemas.CreateSchemas import UserCreate
|
||||||
|
from exceptions.unauthorized_exception import UnauthorizedException
|
||||||
|
|
||||||
|
class AuthService:
|
||||||
|
def __init__(self):
|
||||||
|
self.user_service = UserServices()
|
||||||
|
|
||||||
|
async def login(self, email, password) -> str:
|
||||||
|
|
||||||
|
# get user
|
||||||
|
user = await self.user_service.get_user_by_email(email)
|
||||||
|
|
||||||
|
# verify password
|
||||||
|
if not verify_password(password, user.password):
|
||||||
|
raise UnauthorizedException("Invalid credentials")
|
||||||
|
|
||||||
|
# remove password from user dict
|
||||||
|
user_dict = user.__dict__.copy()
|
||||||
|
user_dict.pop("password", None)
|
||||||
|
|
||||||
|
# create token
|
||||||
|
token = create_jwt_token(user_dict)
|
||||||
|
return token
|
||||||
|
|
||||||
|
async def register(self, user_data: UserCreate) -> str:
|
||||||
|
user = await self.user_service.create_user(user_data)
|
||||||
|
token = create_jwt_token(user)
|
||||||
|
return token
|
||||||
|
|
@ -0,0 +1,227 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import wave
|
||||||
|
|
||||||
|
import aiofiles
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from fastapi import WebSocket
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from pipecat.audio.vad.silero import SileroVADAnalyzer
|
||||||
|
from pipecat.pipeline.pipeline import Pipeline
|
||||||
|
from pipecat.pipeline.runner import PipelineRunner
|
||||||
|
from pipecat.pipeline.task import PipelineParams, PipelineTask
|
||||||
|
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
||||||
|
from pipecat.processors.audio.audio_buffer_processor import AudioBufferProcessor
|
||||||
|
from pipecat.serializers.twilio import TwilioFrameSerializer
|
||||||
|
|
||||||
|
from pipecat.services.elevenlabs import ElevenLabsTTSService
|
||||||
|
from pipecat.services.playht import PlayHTTTSService, Language
|
||||||
|
from pipecat.services.deepgram import DeepgramSTTService
|
||||||
|
from pipecat.services.fish import FishAudioTTSService
|
||||||
|
from pipecat.services.rime import RimeTTSService
|
||||||
|
from pipecat.services.cartesia import CartesiaTTSService
|
||||||
|
|
||||||
|
from pipecat.services.openai_realtime_beta import (
|
||||||
|
OpenAIRealtimeBetaLLMService,
|
||||||
|
SessionProperties,
|
||||||
|
TurnDetection,
|
||||||
|
)
|
||||||
|
from pipecat.services.anthropic import AnthropicLLMService
|
||||||
|
from pipecat.services.openai import OpenAILLMService
|
||||||
|
from pipecat.services.google import GoogleLLMService, GoogleLLMContext
|
||||||
|
from pipecat.transports.network.fastapi_websocket import (
|
||||||
|
FastAPIWebsocketParams,
|
||||||
|
FastAPIWebsocketTransport,
|
||||||
|
)
|
||||||
|
|
||||||
|
load_dotenv(override=True)
|
||||||
|
|
||||||
|
logger.remove(0)
|
||||||
|
logger.add(sys.stderr, level="DEBUG")
|
||||||
|
|
||||||
|
|
||||||
|
async def save_audio(
|
||||||
|
server_name: str, audio: bytes, sample_rate: int, num_channels: int
|
||||||
|
):
|
||||||
|
if len(audio) > 0:
|
||||||
|
filename = f"{server_name}_recording_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.wav"
|
||||||
|
with io.BytesIO() as buffer:
|
||||||
|
with wave.open(buffer, "wb") as wf:
|
||||||
|
wf.setsampwidth(2)
|
||||||
|
wf.setnchannels(num_channels)
|
||||||
|
wf.setframerate(sample_rate)
|
||||||
|
wf.writeframes(audio)
|
||||||
|
async with aiofiles.open(filename, "wb") as file:
|
||||||
|
await file.write(buffer.getvalue())
|
||||||
|
logger.info(f"Merged audio saved to {filename}")
|
||||||
|
else:
|
||||||
|
logger.info("No audio data to save")
|
||||||
|
|
||||||
|
|
||||||
|
async def run_bot(
|
||||||
|
websocket_client: WebSocket, stream_sid: str, testing: bool, option: int = 1
|
||||||
|
):
|
||||||
|
transport = FastAPIWebsocketTransport(
|
||||||
|
websocket=websocket_client,
|
||||||
|
params=FastAPIWebsocketParams(
|
||||||
|
audio_in_enabled=True,
|
||||||
|
audio_out_enabled=True,
|
||||||
|
add_wav_header=False,
|
||||||
|
vad_enabled=True,
|
||||||
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
|
vad_audio_passthrough=True,
|
||||||
|
serializer=TwilioFrameSerializer(stream_sid),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# llm = OpenAIRealtimeBetaLLMService(
|
||||||
|
# api_key=os.getenv("OPENAI_API_KEY"),
|
||||||
|
# session_properties=SessionProperties(
|
||||||
|
# modalities=["text"],
|
||||||
|
# turn_detection=TurnDetection(threshold=0.5, silence_duration_ms=800),
|
||||||
|
# voice=None,
|
||||||
|
# ),
|
||||||
|
# )
|
||||||
|
|
||||||
|
llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"), model="gpt-4o")
|
||||||
|
|
||||||
|
# llm = AnthropicLLMService(api_key=os.getenv("ANTRHOPIC_API_KEY"))
|
||||||
|
|
||||||
|
# llm = GoogleLLMService(api_key=os.getenv("GOOGLE_API_KEY"), model="phone_call")
|
||||||
|
|
||||||
|
stt = DeepgramSTTService(
|
||||||
|
api_key=os.getenv("DEEPGRAM_API_KEY"), audio_passthrough=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# tts = PlayHTTTSService(
|
||||||
|
# api_key=os.getenv("PLAYHT_SECRE_KEY"),
|
||||||
|
# user_id=os.getenv("PLAYHT_USERID"),
|
||||||
|
# voice_url="s3://voice-cloning-zero-shot/80ba8839-a6e6-470c-8f68-7c1e5d3ee2ff/abigailsaad/manifest.json",
|
||||||
|
# params=PlayHTTTSService.InputParams(
|
||||||
|
# language=Language.EN,
|
||||||
|
# speed=1.0,
|
||||||
|
# ),
|
||||||
|
# ) # not working
|
||||||
|
|
||||||
|
# tts = FishAudioTTSService(
|
||||||
|
# api_key=os.getenv("FISH_AUDIO_API_KEY"),
|
||||||
|
# model="b545c585f631496c914815291da4e893", # Get this from Fish Audio playground
|
||||||
|
# output_format="pcm", # Choose output format
|
||||||
|
# sample_rate=24000, # Set sample rate
|
||||||
|
# params=FishAudioTTSService.InputParams(latency="normal", prosody_speed=1.0),
|
||||||
|
# ) # not working
|
||||||
|
|
||||||
|
if option == 1:
|
||||||
|
tts = CartesiaTTSService(
|
||||||
|
api_key=os.getenv("CARTESIA_API_KEY"),
|
||||||
|
voice_id="156fb8d2-335b-4950-9cb3-a2d33befec77", # British Lady
|
||||||
|
push_silence_after_stop=testing,
|
||||||
|
)
|
||||||
|
elif option == 2:
|
||||||
|
tts = RimeTTSService(
|
||||||
|
api_key=os.getenv("RIME_API_KEY"),
|
||||||
|
voice_id="stream",
|
||||||
|
model="mistv2",
|
||||||
|
)
|
||||||
|
elif option == 3:
|
||||||
|
tts = ElevenLabsTTSService(
|
||||||
|
api_key=os.getenv("ELEVEN_LABS_API_KEY"),
|
||||||
|
voice_id="79a125e8-cd45-4c13-8a67-188112f4dd22",
|
||||||
|
push_silence_after_stop=testing,
|
||||||
|
)
|
||||||
|
elif option == 4:
|
||||||
|
tts = RimeTTSService(
|
||||||
|
api_key=os.getenv("RIME_API_KEY"),
|
||||||
|
voice_id="breeze",
|
||||||
|
model="mistv2",
|
||||||
|
)
|
||||||
|
elif option == 5:
|
||||||
|
tts = CartesiaTTSService(
|
||||||
|
api_key=os.getenv("CARTESIA_API_KEY"),
|
||||||
|
voice_id="1d3ba41a-96e6-44ad-aabb-9817c56caa68", # British Lady
|
||||||
|
push_silence_after_stop=testing,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
tts = RimeTTSService(
|
||||||
|
api_key=os.getenv("RIME_API_KEY"),
|
||||||
|
voice_id="peak",
|
||||||
|
model="mistv2",
|
||||||
|
)
|
||||||
|
|
||||||
|
messages = [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": f"""
|
||||||
|
Welcome to 365 Days Medical Centre Para Hills - we care about you.
|
||||||
|
If this is an emergency, please call triple zero.
|
||||||
|
We are open from 8 AM to 8 PM every day of the year.
|
||||||
|
All calls are recorded for training and quality purposes - please let us know if you do not wish to be recorded.
|
||||||
|
I am Nishka, your 24/7 healthcare receptionist. Which language would you like to speak?
|
||||||
|
""",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
context = OpenAILLMContext(messages)
|
||||||
|
context_aggregator = llm.create_context_aggregator(context)
|
||||||
|
|
||||||
|
# NOTE: Watch out! This will save all the conversation in memory. You can
|
||||||
|
# pass `buffer_size` to get periodic callbacks.
|
||||||
|
audiobuffer = AudioBufferProcessor(user_continuous_stream=not testing)
|
||||||
|
|
||||||
|
pipeline = Pipeline(
|
||||||
|
[
|
||||||
|
transport.input(), # Websocket input from client
|
||||||
|
stt, # Speech-To-Text
|
||||||
|
context_aggregator.user(), # User context
|
||||||
|
llm, # LLM
|
||||||
|
tts, # Text-To-Speech
|
||||||
|
transport.output(), # Websocket output to client
|
||||||
|
audiobuffer, # Used to buffer the audio in the pipeline
|
||||||
|
context_aggregator.assistant(), # Assistant context
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
task = PipelineTask(
|
||||||
|
pipeline,
|
||||||
|
params=PipelineParams(
|
||||||
|
audio_in_sample_rate=8000,
|
||||||
|
audio_out_sample_rate=8000,
|
||||||
|
allow_interruptions=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@transport.event_handler("on_client_connected")
|
||||||
|
async def on_client_connected(transport, client):
|
||||||
|
# Start recording.
|
||||||
|
await audiobuffer.start_recording()
|
||||||
|
# Kick off the conversation.
|
||||||
|
messages.append(
|
||||||
|
{"role": "system", "content": "Please introduce yourself to the user."}
|
||||||
|
)
|
||||||
|
await task.queue_frames([context_aggregator.user().get_context_frame()])
|
||||||
|
|
||||||
|
@transport.event_handler("on_client_disconnected")
|
||||||
|
async def on_client_disconnected(transport, client):
|
||||||
|
await task.cancel()
|
||||||
|
|
||||||
|
# @audiobuffer.event_handler("on_audio_data")
|
||||||
|
# async def on_audio_data(buffer, audio, sample_rate, num_channels):
|
||||||
|
# server_name = f"server_{websocket_client.client.port}"
|
||||||
|
# await save_audio(server_name, audio, sample_rate, num_channels)
|
||||||
|
|
||||||
|
# We use `handle_sigint=False` because `uvicorn` is controlling keyboard
|
||||||
|
# interruptions. We use `force_gc=True` to force garbage collection after
|
||||||
|
# the runner finishes running a task which could be useful for long running
|
||||||
|
# applications with multiple clients connecting.
|
||||||
|
runner = PipelineRunner(handle_sigint=False, force_gc=True)
|
||||||
|
|
||||||
|
await runner.run(task)
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
import jwt
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from utils.constants import JWT_SECRET, JWT_ALGORITHM, JWT_EXPIRE_MINUTES
|
||||||
|
|
||||||
|
def create_jwt_token(data: dict):
|
||||||
|
# Create a copy of the data and handle Enum and datetime serialization
|
||||||
|
to_encode = {}
|
||||||
|
for key, value in data.items():
|
||||||
|
if isinstance(value, Enum):
|
||||||
|
to_encode[key] = value.value # Convert Enum to its string value
|
||||||
|
elif isinstance(value, datetime):
|
||||||
|
to_encode[key] = value.isoformat() # Convert datetime to ISO format string
|
||||||
|
else:
|
||||||
|
to_encode[key] = value
|
||||||
|
|
||||||
|
# Safely evaluate the JWT_EXPIRE_MINUTES expression
|
||||||
|
minutes = eval(JWT_EXPIRE_MINUTES) if isinstance(JWT_EXPIRE_MINUTES, str) else JWT_EXPIRE_MINUTES
|
||||||
|
expire = datetime.now(timezone.utc) + timedelta(minutes=minutes)
|
||||||
|
to_encode.update({"exp": expire.timestamp()}) # Use timestamp for expiration
|
||||||
|
encoded_jwt = jwt.encode(to_encode, JWT_SECRET, algorithm=JWT_ALGORITHM)
|
||||||
|
return encoded_jwt
|
||||||
|
|
||||||
|
|
||||||
|
def verify_jwt_token(token: str):
|
||||||
|
try:
|
||||||
|
payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
|
||||||
|
return payload
|
||||||
|
except jwt.ExpiredSignatureError:
|
||||||
|
return None
|
||||||
|
except jwt.InvalidTokenError:
|
||||||
|
return None
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
from loguru import logger
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy import or_
|
||||||
|
|
||||||
|
from database import get_db
|
||||||
|
from models.Users import Users
|
||||||
|
from exceptions.validation_exception import ValidationException
|
||||||
|
from schemas.ResponseSchemas import UserResponse
|
||||||
|
from utils.password_utils import hash_password
|
||||||
|
from schemas.CreateSchemas import UserCreate
|
||||||
|
from exceptions.resource_not_found_exception import ResourceNotFoundException
|
||||||
|
|
||||||
|
|
||||||
|
class UserServices:
|
||||||
|
def __init__(self):
|
||||||
|
self.db: Session = next(get_db())
|
||||||
|
|
||||||
|
async def create_user(self, user_data: UserCreate):
|
||||||
|
try:
|
||||||
|
# Check if user with same username or email exists
|
||||||
|
existing_user = (
|
||||||
|
self.db.query(Users)
|
||||||
|
.filter(Users.email == user_data.email.lower())
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
if existing_user:
|
||||||
|
raise ValidationException(
|
||||||
|
"User with same email already exists"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a new user instance
|
||||||
|
new_user = Users(
|
||||||
|
username=user_data.username,
|
||||||
|
email=user_data.email.lower(),
|
||||||
|
password=hash_password(user_data.password),
|
||||||
|
clinicRole=user_data.clinicRole,
|
||||||
|
userType=user_data.userType,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add to database and commit
|
||||||
|
self.db.add(new_user)
|
||||||
|
self.db.commit()
|
||||||
|
self.db.refresh(new_user)
|
||||||
|
|
||||||
|
user_dict = new_user.__dict__.copy()
|
||||||
|
|
||||||
|
user_response = UserResponse(**user_dict).model_dump()
|
||||||
|
|
||||||
|
return user_response
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Error creating user", e)
|
||||||
|
self.db.rollback()
|
||||||
|
if e.__class__ == ValidationException:
|
||||||
|
raise ValidationException(e.message)
|
||||||
|
if e.__class__ == ResourceNotFoundException:
|
||||||
|
raise ResourceNotFoundException(e.message)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
def get_user(self, user_id) -> UserResponse:
|
||||||
|
|
||||||
|
# Query the user by ID
|
||||||
|
user = self.db.query(Users).filter(Users.id == user_id).first()
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
logger.error("User not found")
|
||||||
|
raise ResourceNotFoundException("User not found")
|
||||||
|
|
||||||
|
user_dict = user.__dict__.copy()
|
||||||
|
|
||||||
|
user_response = UserResponse(**user_dict).model_dump()
|
||||||
|
|
||||||
|
return user_response
|
||||||
|
|
||||||
|
async def get_user_by_email(self, email: str) -> UserResponse:
|
||||||
|
user = self.db.query(Users).filter(Users.email == email.lower()).first()
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
logger.error("User not found")
|
||||||
|
raise ResourceNotFoundException("User not found")
|
||||||
|
|
||||||
|
user_dict = user.__dict__.copy()
|
||||||
|
|
||||||
|
user_response = UserResponse(**user_dict)
|
||||||
|
|
||||||
|
return user_response
|
||||||
|
|
||||||
|
def update_user(self, user_id, user):
|
||||||
|
return user
|
||||||
|
|
||||||
|
def delete_user(self, user_id):
|
||||||
|
return user
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Database pagination constants
|
||||||
|
DEFAULT_SKIP = 0
|
||||||
|
DEFAULT_LIMIT = 10
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
from .password_utils import hash_password, verify_password
|
||||||
|
from .constants import JWT_SECRET, JWT_ALGORITHM, JWT_EXPIRE_MINUTES
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"hash_password",
|
||||||
|
"verify_password",
|
||||||
|
"JWT_SECRET",
|
||||||
|
"JWT_ALGORITHM",
|
||||||
|
"JWT_EXPIRE_MINUTES",
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import dotenv
|
||||||
|
import os
|
||||||
|
dotenv.load_dotenv()
|
||||||
|
|
||||||
|
DEFAULT_SKIP = 0
|
||||||
|
DEFAULT_PAGE = 1
|
||||||
|
DEFAULT_LIMIT = 10
|
||||||
|
DEFAULT_ORDER_BY = "id"
|
||||||
|
DEFAULT_ORDER = "desc"
|
||||||
|
|
||||||
|
# jwt
|
||||||
|
JWT_ALGORITHM = os.getenv("JWT_ALGORITHM")
|
||||||
|
JWT_SECRET = os.getenv("JWT_SECRET")
|
||||||
|
JWT_EXPIRE_MINUTES = os.getenv("JWT_EXPIRE_MINUTES")
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
from passlib.context import CryptContext
|
||||||
|
|
||||||
|
# Create a password context for hashing and verifying
|
||||||
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
|
|
||||||
|
def hash_password(password: str) -> str:
|
||||||
|
"""
|
||||||
|
Hash a password using bcrypt
|
||||||
|
"""
|
||||||
|
return pwd_context.hash(password)
|
||||||
|
|
||||||
|
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||||
|
"""
|
||||||
|
Verify a password against a hash
|
||||||
|
"""
|
||||||
|
return pwd_context.verify(plain_password, hashed_password)
|
||||||
Loading…
Reference in New Issue