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