From 340aa7e3ff613192cc6c9d1140138edae1e51d27 Mon Sep 17 00:00:00 2001 From: Steve Chen Date: Wed, 19 Mar 2025 20:34:52 -0700 Subject: [PATCH 01/18] Story2_temp --- app/clients/service/ml_models.py | 21 +++++++++++++++++++++ app/clients/service/ml_models_router.py | 21 +++++++++++++++++++++ app/main.py | 2 ++ 3 files changed, 44 insertions(+) create mode 100644 app/clients/service/ml_models.py create mode 100644 app/clients/service/ml_models_router.py diff --git a/app/clients/service/ml_models.py b/app/clients/service/ml_models.py new file mode 100644 index 00000000..074a56bf --- /dev/null +++ b/app/clients/service/ml_models.py @@ -0,0 +1,21 @@ +class MLModels: + current_model = "Random Forest" + """List of available ml models""" + available_models = ["Random Forest"] + + @staticmethod + def get_current_model(): + """Get the current active ml model""" + return MLModels.current_model + + @staticmethod + def list_available_models(): + return MLModels.available_models + + @staticmethod + def switch_model(model_name: str): + """Switch the current ml model""" + if model_name in MLModels.available_models: + MLModels.current_model = model_name + return True + return False \ No newline at end of file diff --git a/app/clients/service/ml_models_router.py b/app/clients/service/ml_models_router.py new file mode 100644 index 00000000..e96de76d --- /dev/null +++ b/app/clients/service/ml_models_router.py @@ -0,0 +1,21 @@ +from fastapi import APIRouter, HTTPException +from app.clients.service.ml_models import MLModels + +router = APIRouter(prefix="/ml_models") + +@router.get("/list") +def list_models(): + models = MLModels.list_available_models() + return {"models": models} + +@router.post("/switch/{model_name}") +def switch_models(model_name: str): + success = MLModels.switch_model(model_name) + if not success: + raise HTTPException(status_code=400, detail="Model switch failed") + return {"message": f"Model switched to {model_name}"} + +@router.get("/current") +def current_model(): + model = MLModels.get_current_model() + return {"current_model": model} diff --git a/app/main.py b/app/main.py index a8e8fa7f..adf1af76 100644 --- a/app/main.py +++ b/app/main.py @@ -9,6 +9,7 @@ from app.database import engine from app.clients.router import router as clients_router from app.auth.router import router as auth_router +from app.clients.service.ml_models_router import router as ml_models_router from fastapi.middleware.cors import CORSMiddleware # Initialize database tables @@ -20,6 +21,7 @@ # Include routers app.include_router(auth_router) app.include_router(clients_router) +app.include_router(ml_models_router) # Configure CORS middleware app.add_middleware( From f41a9e0673c1c433dbb9c9d70397adf8f1b09d43 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 20 Mar 2025 12:27:51 -0700 Subject: [PATCH 02/18] Add ability to train/save/load three different model types --- app/clients/service/ml_models.py | 2 +- app/clients/service/model.py | 97 +++++++++++++++--- .../model_Linear_Regression.pkl | Bin 0 -> 899 bytes .../model_Random_Forest_Regressor.pkl | Bin 0 -> 749557 bytes .../model_Support_Vector_Machine.pkl | Bin 0 -> 29888 bytes 5 files changed, 82 insertions(+), 17 deletions(-) create mode 100644 app/clients/service/pretrained_models/model_Linear_Regression.pkl create mode 100644 app/clients/service/pretrained_models/model_Random_Forest_Regressor.pkl create mode 100644 app/clients/service/pretrained_models/model_Support_Vector_Machine.pkl diff --git a/app/clients/service/ml_models.py b/app/clients/service/ml_models.py index 074a56bf..129ab6e2 100644 --- a/app/clients/service/ml_models.py +++ b/app/clients/service/ml_models.py @@ -1,7 +1,7 @@ class MLModels: current_model = "Random Forest" """List of available ml models""" - available_models = ["Random Forest"] + available_models = ["Linear Regression", "Random Forest Regressor", "Support Vector Machine"] @staticmethod def get_current_model(): diff --git a/app/clients/service/model.py b/app/clients/service/model.py index b2406370..16097276 100644 --- a/app/clients/service/model.py +++ b/app/clients/service/model.py @@ -1,6 +1,7 @@ """ Model training module for the Common Assessment Tool. Handles the preparation, training, and saving of the prediction model. +Pass in model name via command line """ # Standard library imports @@ -9,12 +10,24 @@ # Third-party imports import numpy as np import pandas as pd +import sys +import os from sklearn.model_selection import train_test_split +from sklearn.linear_model import LinearRegression from sklearn.ensemble import RandomForestRegressor +from sklearn import svm -def prepare_models(): +# Local imports +from ml_models import MLModels + +default_unformatted_model_path = "pretrained_models" + os.sep + "model_{}.pkl" + +def prepare_model_data(test_size=0.2, random_state=42): """ Prepare and train the Random Forest model using the dataset. + Args: + test_size: The percent of the dataset to use as test data (rest will be used as train data) + random_state: The random state to generate train/test split with Returns: RandomForestRegressor: Trained model for predicting success rates @@ -64,47 +77,99 @@ def prepare_models(): features = np.array(data[all_features]) # Changed from X to features targets = np.array(data['success_rate']) # Changed from y to targets # Split the dataset - features_train, _, targets_train, _ = train_test_split( # Removed unused variables + X_train, x_test, Y_train, y_test = train_test_split( # Removed unused variables features, targets, - test_size=0.2, - random_state=42 + test_size=test_size, + random_state=random_state ) - # Initialize and train the model - model = RandomForestRegressor(n_estimators=100, random_state=42) - model.fit(features_train, targets_train) + + return X_train, x_test, Y_train, y_test + + +def train_model(X_train, Y_train, model_type, n_estimators=100, random_state=42): + """ + Trains the model + Args: + X_train: Training features + targets_train: Target features + Y_train: Which model to create + n_estimators: Number estimators (for random forest) + random_state: Random state to train with (for random forest) + + Returns: A trained model of the type specified + + """ + # TODO the way model is selected should be improved + # Instantiate machine learning model + if model_type == MLModels.available_models[0]: # Linear regression + model = LinearRegression() + + elif model_type == MLModels.available_models[1]: # Random forest + model = RandomForestRegressor(n_estimators=n_estimators, random_state=random_state) + + elif model_type == MLModels.available_models[2]: # Support Vector Machine + model = svm.SVR() + + else: # Invalid model passed in + print("ERROR! Invalid model passed in. Exiting...") + sys.exit(-1) + + # Fit model with training data + model.fit(X_train, Y_train) return model -def save_model(model, filename="model.pkl"): +def get_true_file_name(model_type, filename): + """ + Takes a model type and file name, formats model type, and replaces spaces with underscores + Args: + model_type: The model type as a String + filename: The file name (should follow 'model_{}.pkl' format) + + Returns: The clean file name + """ + return filename.format(model_type).replace(" ", "_") + + +def save_model(model, model_type, filename=default_unformatted_model_path): """ Save the trained model to a file. Args: model: Trained model to save + model_type: The type of model being saved filename (str): Name of the file to save the model to """ - with open(filename, "wb") as model_file: + true_file_name = get_true_file_name(model_type, filename) + with open(true_file_name, "wb") as model_file: pickle.dump(model, model_file) -def load_model(filename="model.pkl"): +def load_model(model_type, filename=default_unformatted_model_path): """ Load a trained model from a file. Args: + model_type: The type of model being loaded filename (str): Name of the file to load the model from Returns: The loaded model """ - with open(filename, "rb") as model_file: + true_file_name = get_true_file_name(model_type, filename) + with open(true_file_name, "rb") as model_file: return pickle.load(model_file) -def main(): +def main(argv): """Main function to train and save the model.""" - print("Starting model training...") - model = prepare_models() - save_model(model) + # Get the model type from the command line arguments + model_type = argv[1] + + # Train and save the model + print("Starting model training for {} model...".format(model_type)) + X_train, x_test, Y_train, y_test = prepare_model_data() + model = train_model(X_train, Y_train, model_type) + save_model(model, model_type) print("Model training completed and saved successfully.") if __name__ == "__main__": - main() + main(sys.argv) diff --git a/app/clients/service/pretrained_models/model_Linear_Regression.pkl b/app/clients/service/pretrained_models/model_Linear_Regression.pkl new file mode 100644 index 0000000000000000000000000000000000000000..d264f1b9ca1b479c78f1402a65ee1b1c7988be85 GIT binary patch literal 899 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 OckQ0Kd)kp53;qvv3hZ$J literal 0 HcmV?d00001 diff --git a/app/clients/service/pretrained_models/model_Support_Vector_Machine.pkl b/app/clients/service/pretrained_models/model_Support_Vector_Machine.pkl new file mode 100644 index 0000000000000000000000000000000000000000..999b824c1113e91c153c6333a6813dd438685c89 GIT binary patch literal 29888 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 P)2ALi_3-S*#Z&(aZOEUp literal 0 HcmV?d00001 From 36445eeb8205cd4166345f068dc4e241b1d0ecc4 Mon Sep 17 00:00:00 2001 From: Fran Date: Sun, 23 Mar 2025 11:19:24 -0700 Subject: [PATCH 03/18] fixed client service --- app/clients/service/client_service.py | 193 ++++++++++++++++++++---- app/clients/service/ml_models.py | 39 ++--- app/clients/service/ml_models_router.py | 19 ++- 3 files changed, 197 insertions(+), 54 deletions(-) diff --git a/app/clients/service/client_service.py b/app/clients/service/client_service.py index 86c3ef4a..c743acb4 100644 --- a/app/clients/service/client_service.py +++ b/app/clients/service/client_service.py @@ -3,6 +3,7 @@ Provides CRUD operations and business logic for client management. """ +from abc import ABC, abstractmethod from sqlalchemy.orm import Session from sqlalchemy import and_ from fastapi import HTTPException, status @@ -10,7 +11,76 @@ from app.models import Client, ClientCase, User from app.clients.schema import ClientUpdate, ServiceUpdate, ServiceResponse -class ClientService: + +class InterfaceClientQueryService(ABC): + """Interface for client query operations""" + @abstractmethod + def get_client(self, db: Session, client_id: int) -> Client: + """Get a specific client by ID""" + pass + + @abstractmethod + def get_clients(self, db: Session, skip: int, limit: int) -> Dict[str, Any]: + """ Get clients with optional pagination.""" + pass + + @abstractmethod + def get_clients_by_criteria(self, db: Session, **criteria) -> List[Client]: + """Get clients filtered by any combination of criteria""" + pass + + @abstractmethod + def get_clients_by_services(self, db: Session, **service_filters) -> \ + List[Client]: + """ Get clients filtered by multiple service statuses.""" + + pass + + @abstractmethod + def get_client_services(self, db: Session, client_id: int) -> List[ + ClientCase]: + pass + + @abstractmethod + def get_clients_by_success_rate(self, db: Session, min_rate: int) -> List[ + Client]: + pass + + @abstractmethod + def get_clients_by_case_worker(self, db: Session, case_worker_id: int) -> \ + List[Client]: + pass + + +class InterfaceClientManagementService(ABC): + """Interface for client management operations""" + @abstractmethod + def update_client(self, db: Session, client_id: int, + client_update: ClientUpdate) -> ClientUpdate: + """Update a client's information""" + pass + + @abstractmethod + def update_client_services(self, db: Session, client_id: int, user_id: int, + service_update: ServiceUpdate) -> ClientCase: + """Update a client's services and outcomes for a specific caseworker""" + pass + + @abstractmethod + def create_case_assignment(self, db: Session, client_id: int, + case_worker_id: int) -> ClientCase: + """Create a new case assignment""" + pass + + @abstractmethod + def delete_client(self, db: Session, client_id: int) -> None: + """Delete a client and their associated records""" + pass + + +class ClientQueryService(InterfaceClientQueryService): + """Implementation of client query service""" + @staticmethod def get_client(db: Session, client_id: int): """Get a specific client by ID""" @@ -38,7 +108,7 @@ def get_clients(db: Session, skip: int = 0, limit: int = 50): status_code=status.HTTP_400_BAD_REQUEST, detail="Limit must be greater than 0" ) - + clients = db.query(Client).offset(skip).limit(limit).all() total = db.query(Client).count() return {"clients": clients, "total": total} @@ -73,13 +143,13 @@ def get_clients_by_criteria( ): """Get clients filtered by any combination of criteria""" query = db.query(Client) - + if education_level is not None and not (1 <= education_level <= 14): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Education level must be between 1 and 14" ) - + if age_min is not None and age_min < 18: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -114,17 +184,21 @@ def get_clients_by_criteria( if fluent_english is not None: query = query.filter(Client.fluent_english == fluent_english) if reading_english_scale is not None: - query = query.filter(Client.reading_english_scale == reading_english_scale) + query = query.filter( + Client.reading_english_scale == reading_english_scale) if speaking_english_scale is not None: - query = query.filter(Client.speaking_english_scale == speaking_english_scale) + query = query.filter( + Client.speaking_english_scale == speaking_english_scale) if writing_english_scale is not None: - query = query.filter(Client.writing_english_scale == writing_english_scale) + query = query.filter( + Client.writing_english_scale == writing_english_scale) if numeracy_scale is not None: query = query.filter(Client.numeracy_scale == numeracy_scale) if computer_scale is not None: query = query.filter(Client.computer_scale == computer_scale) if transportation_bool is not None: - query = query.filter(Client.transportation_bool == transportation_bool) + query = query.filter( + Client.transportation_bool == transportation_bool) if caregiver_bool is not None: query = query.filter(Client.caregiver_bool == caregiver_bool) if housing is not None: @@ -140,7 +214,8 @@ def get_clients_by_criteria( if time_unemployed is not None: query = query.filter(Client.time_unemployed == time_unemployed) if need_mental_health_support_bool is not None: - query = query.filter(Client.need_mental_health_support_bool == need_mental_health_support_bool) + query = query.filter( + Client.need_mental_health_support_bool == need_mental_health_support_bool) try: return query.all() @@ -159,12 +234,12 @@ def get_clients_by_services( Get clients filtered by multiple service statuses. """ query = db.query(Client).join(ClientCase) - + for service_name, status in service_filters.items(): if status is not None: filter_criteria = getattr(ClientCase, service_name) == status query = query.filter(filter_criteria) - + try: return query.all() except Exception as e: @@ -175,8 +250,9 @@ def get_clients_by_services( @staticmethod def get_client_services(db: Session, client_id: int): - """Get all services for a specific client with case worker info""" - client_cases = db.query(ClientCase).filter(ClientCase.client_id == client_id).all() + """Get all services for a specific client with caseworker info""" + client_cases = db.query(ClientCase).filter( + ClientCase.client_id == client_id).all() if not client_cases: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -192,7 +268,7 @@ def get_clients_by_success_rate(db: Session, min_rate: int = 70): status_code=status.HTTP_400_BAD_REQUEST, detail="Success rate must be between 0 and 100" ) - + return db.query(Client).join(ClientCase).filter( ClientCase.success_rate >= min_rate ).all() @@ -206,11 +282,15 @@ def get_clients_by_case_worker(db: Session, case_worker_id: int): status_code=status.HTTP_404_NOT_FOUND, detail=f"Case worker with id {case_worker_id} not found" ) - + return db.query(Client).join(ClientCase).filter( ClientCase.user_id == case_worker_id ).all() + +class ClientManagementService(InterfaceClientManagementService): + """Implementation of client management service""" + @staticmethod def update_client(db: Session, client_id: int, client_update: ClientUpdate): """Update a client's information""" @@ -235,10 +315,10 @@ def update_client(db: Session, client_id: int, client_update: ClientUpdate): status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to update client: {str(e)}" ) - + @staticmethod def update_client_services( - db: Session, + db: Session, client_id: int, user_id: int, service_update: ServiceUpdate @@ -248,12 +328,12 @@ def update_client_services( ClientCase.client_id == client_id, ClientCase.user_id == user_id ).first() - + if not client_case: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"No case found for client {client_id} with case worker {user_id}. " - f"Cannot update services for a non-existent case assignment." + f"Cannot update services for a non-existent case assignment." ) update_data = service_update.dict(exclude_unset=True) @@ -270,10 +350,10 @@ def update_client_services( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to update client services: {str(e)}" ) - + @staticmethod def create_case_assignment( - db: Session, + db: Session, client_id: int, case_worker_id: int ): @@ -286,7 +366,7 @@ def create_case_assignment( detail=f"Client with id {client_id} not found" ) - # Check if case worker exists + # Check if caseworker exists case_worker = db.query(User).filter(User.id == case_worker_id).first() if not case_worker: raise HTTPException( @@ -299,12 +379,12 @@ def create_case_assignment( ClientCase.client_id == client_id, ClientCase.user_id == case_worker_id ).first() - + if existing_case: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Client {client_id} already has a case assigned to case worker {case_worker_id}" - ) + ) try: # Create new case assignment with default service values @@ -331,7 +411,7 @@ def create_case_assignment( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to create case assignment: {str(e)}" ) - + @staticmethod def delete_client(db: Session, client_id: int): """Delete a client and their associated records""" @@ -348,14 +428,73 @@ def delete_client(db: Session, client_id: int): db.query(ClientCase).filter( ClientCase.client_id == client_id ).delete() - + # Delete the client db.delete(client) db.commit() - + except Exception as e: db.rollback() raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to delete client: {str(e)}" ) + + +class ClientService: + """ + Facade that maintains backward compatibility with the existing router. + Delegates to specialized service classes. + """ + + # Query methods + @staticmethod + def get_client(db: Session, client_id: int): + return ClientQueryService.get_client(db, client_id) + + @staticmethod + def get_clients(db: Session, skip: int = 0, limit: int = 50): + return ClientQueryService.get_clients(db, skip, limit) + + @staticmethod + def get_clients_by_criteria(db: Session, **criteria): + return ClientQueryService.get_clients_by_criteria(db, **criteria) + + @staticmethod + def get_clients_by_services(db: Session, **service_filters): + return ClientQueryService.get_clients_by_services(db, **service_filters) + + @staticmethod + def get_client_services(db: Session, client_id: int): + return ClientQueryService.get_client_services(db, client_id) + + @staticmethod + def get_clients_by_success_rate(db: Session, min_rate: int = 70): + return ClientQueryService.get_clients_by_success_rate(db, min_rate) + + @staticmethod + def get_clients_by_case_worker(db: Session, case_worker_id: int): + return ClientQueryService.get_clients_by_case_worker(db, case_worker_id) + + # Modification methods + @staticmethod + def update_client(db: Session, client_id: int, client_update: ClientUpdate): + return ClientManagementService.update_client(db, client_id, + client_update) + + @staticmethod + def update_client_services(db: Session, client_id: int, user_id: int, + service_update: ServiceUpdate): + return ClientManagementService.update_client_services(db, client_id, + user_id, + service_update) + + @staticmethod + def create_case_assignment(db: Session, client_id: int, + case_worker_id: int): + return ClientManagementService.create_case_assignment(db, client_id, + case_worker_id) + + @staticmethod + def delete_client(db: Session, client_id: int): + return ClientManagementService.delete_client(db, client_id) diff --git a/app/clients/service/ml_models.py b/app/clients/service/ml_models.py index 129ab6e2..7fb16fa2 100644 --- a/app/clients/service/ml_models.py +++ b/app/clients/service/ml_models.py @@ -1,21 +1,22 @@ class MLModels: - current_model = "Random Forest" - """List of available ml models""" - available_models = ["Linear Regression", "Random Forest Regressor", "Support Vector Machine"] + current_model = "Random Forest" + """List of available ml models""" + available_models = ["Linear Regression", "Random Forest Regressor", + "Support Vector Machine"] - @staticmethod - def get_current_model(): - """Get the current active ml model""" - return MLModels.current_model - - @staticmethod - def list_available_models(): - return MLModels.available_models - - @staticmethod - def switch_model(model_name: str): - """Switch the current ml model""" - if model_name in MLModels.available_models: - MLModels.current_model = model_name - return True - return False \ No newline at end of file + @staticmethod + def get_current_model(): + """Get the current active ml model""" + return MLModels.current_model + + @staticmethod + def list_available_models(): + return MLModels.available_models + + @staticmethod + def switch_model(model_name: str): + """Switch the current ml model""" + if model_name in MLModels.available_models: + MLModels.current_model = model_name + return True + return False diff --git a/app/clients/service/ml_models_router.py b/app/clients/service/ml_models_router.py index e96de76d..476f2b1e 100644 --- a/app/clients/service/ml_models_router.py +++ b/app/clients/service/ml_models_router.py @@ -3,19 +3,22 @@ router = APIRouter(prefix="/ml_models") + @router.get("/list") def list_models(): - models = MLModels.list_available_models() - return {"models": models} + models = MLModels.list_available_models() + return {"models": models} + @router.post("/switch/{model_name}") def switch_models(model_name: str): - success = MLModels.switch_model(model_name) - if not success: - raise HTTPException(status_code=400, detail="Model switch failed") - return {"message": f"Model switched to {model_name}"} + success = MLModels.switch_model(model_name) + if not success: + raise HTTPException(status_code=400, detail="Model switch failed") + return {"message": f"Model switched to {model_name}"} + @router.get("/current") def current_model(): - model = MLModels.get_current_model() - return {"current_model": model} + model = MLModels.get_current_model() + return {"current_model": model} From 5afc84e0f0d8953c6f4a72a71c8e333909a362c9 Mon Sep 17 00:00:00 2001 From: Fran Date: Sun, 23 Mar 2025 11:47:42 -0700 Subject: [PATCH 04/18] fixed ml model solid principle --- app/clients/service/ml_models.py | 72 +++++++++++++++++++------ app/clients/service/ml_models_router.py | 17 +++--- 2 files changed, 64 insertions(+), 25 deletions(-) diff --git a/app/clients/service/ml_models.py b/app/clients/service/ml_models.py index 7fb16fa2..7b19d661 100644 --- a/app/clients/service/ml_models.py +++ b/app/clients/service/ml_models.py @@ -1,22 +1,60 @@ -class MLModels: - current_model = "Random Forest" - """List of available ml models""" - available_models = ["Linear Regression", "Random Forest Regressor", - "Support Vector Machine"] - - @staticmethod - def get_current_model(): +from abc import ABC, abstractmethod +from typing import List + + +class InterfaceMLModelRepository(ABC): + """Interface for ML Models storage""" + + @abstractmethod + def list_models(self) -> List[str]: + """Get list of all available models""" + pass + + @abstractmethod + def is_model_available(self, model_name: str) -> bool: + """Check if a model is valid""" + pass + + +class InterfaceMLModelManager(ABC): + """Interface for ML model management""" + + @abstractmethod + def get_current_model(self) -> str: """Get the current active ml model""" - return MLModels.current_model + pass - @staticmethod - def list_available_models(): - return MLModels.available_models + @abstractmethod + def switch_model(self, model_name: str) -> bool: + """Switch between models""" + pass - @staticmethod - def switch_model(model_name: str): - """Switch the current ml model""" - if model_name in MLModels.available_models: - MLModels.current_model = model_name + +class MLModelRepository(InterfaceMLModelRepository): + def __init__(self): + self._available_models = [ + "Linear Regression", + "Random Forest Regressor", + "Support Vector Machine" + ] + + def list_models(self) -> List[str]: + return self._available_models + + def is_model_available(self, model_name: str) -> bool: + return model_name in self._available_models + +class MLModelManager(InterfaceMLModelManager): + def __init__(self, repository: InterfaceMLModelRepository): + self._repository = repository + self._current_model = "Random Forest Regressor" + + def get_current_model(self) -> str: + return self._current_model + + def switch_model(self, model_name: str) -> bool: + if self._repository.is_model_available(model_name): + self._current_model = model_name return True return False + diff --git a/app/clients/service/ml_models_router.py b/app/clients/service/ml_models_router.py index 476f2b1e..aa3ec3ec 100644 --- a/app/clients/service/ml_models_router.py +++ b/app/clients/service/ml_models_router.py @@ -1,18 +1,19 @@ from fastapi import APIRouter, HTTPException -from app.clients.service.ml_models import MLModels +from app.clients.service.ml_models import MLModelRepository, MLModelManager router = APIRouter(prefix="/ml_models") - - +model_repository = MLModelRepository() +model_manager = MLModelManager(model_repository) @router.get("/list") def list_models(): - models = MLModels.list_available_models() - return {"models": models} + """List all available ML models""" + return {"models": model_repository.list_models()} @router.post("/switch/{model_name}") def switch_models(model_name: str): - success = MLModels.switch_model(model_name) + """Switch between ML models""" + success = model_manager.switch_model(model_name) if not success: raise HTTPException(status_code=400, detail="Model switch failed") return {"message": f"Model switched to {model_name}"} @@ -20,5 +21,5 @@ def switch_models(model_name: str): @router.get("/current") def current_model(): - model = MLModels.get_current_model() - return {"current_model": model} + """Get the current ML model""" + return {"current_model": model_manager.get_current_model()} From 32cbfbc224df0a049027017e3ffe521ffe11c1dc Mon Sep 17 00:00:00 2001 From: Fran Date: Sun, 23 Mar 2025 12:26:32 -0700 Subject: [PATCH 05/18] fixed ml base models --- app/clients/service/ml_models.py | 115 +++++++++++++++++++++--- app/clients/service/ml_models_router.py | 7 +- app/clients/service/model.py | 97 +++++++++++--------- requirements.txt | 2 +- 4 files changed, 161 insertions(+), 60 deletions(-) diff --git a/app/clients/service/ml_models.py b/app/clients/service/ml_models.py index 7b19d661..4f75209c 100644 --- a/app/clients/service/ml_models.py +++ b/app/clients/service/ml_models.py @@ -1,13 +1,88 @@ from abc import ABC, abstractmethod from typing import List +import numpy as np +from sklearn.linear_model import LinearRegression +from sklearn.ensemble import RandomForestRegressor +from sklearn.svm import SVR + + +class InterfaceBaseMLModel(ABC): + """Interface of a base ML Model""" + + @abstractmethod + def fit(self, X: np.ndarray, y: np.ndarray): + pass + + @abstractmethod + def predict(self, X: np.ndarray) -> np.ndarray: + pass + + def save(self, path: str): + import pickle + with open(path, "wb") as f: + pickle.dump(self, f) + + @staticmethod + def load(path: str): + import pickle + with open(path, "rb") as f: + return pickle.load(f) + + @abstractmethod + def __str__(self) -> str: + """Return the name of the model""" + pass + + +class LinearRegressionModel(InterfaceBaseMLModel): + def __init__(self): + self.model = LinearRegression() + + def fit(self, X, y): + self.model.fit(X, y) + + def predict(self, X): + return self.model.predict(X) + + def __str__(self): + return "Linear Regression" + + +class RandomForestModel(InterfaceBaseMLModel): + def __init__(self, n_estimators=100, random_state=42): + self.model = RandomForestRegressor(n_estimators=n_estimators, + random_state=random_state) + + def fit(self, X, y): + self.model.fit(X, y) + + def predict(self, X): + return self.model.predict(X) + + def __str__(self): + return "Random Forest Regressor" + + +class SVMModel(InterfaceBaseMLModel): + def __init__(self): + self.model = SVR() + + def fit(self, X, y): + self.model.fit(X, y) + + def predict(self, X): + return self.model.predict(X) + + def __str__(self): + return "Support Vector Machine" class InterfaceMLModelRepository(ABC): """Interface for ML Models storage""" @abstractmethod - def list_models(self) -> List[str]: - """Get list of all available models""" + def list_models(self) -> List[InterfaceBaseMLModel]: + """Get list of all available models instances""" pass @abstractmethod @@ -15,12 +90,17 @@ def is_model_available(self, model_name: str) -> bool: """Check if a model is valid""" pass + @abstractmethod + def get_model_instance(self, model_name: str) -> InterfaceBaseMLModel: + """Return an instance of the requested model""" + pass + class InterfaceMLModelManager(ABC): """Interface for ML model management""" @abstractmethod - def get_current_model(self) -> str: + def get_current_model(self) -> InterfaceBaseMLModel: """Get the current active ml model""" pass @@ -32,29 +112,36 @@ def switch_model(self, model_name: str) -> bool: class MLModelRepository(InterfaceMLModelRepository): def __init__(self): - self._available_models = [ - "Linear Regression", - "Random Forest Regressor", - "Support Vector Machine" - ] + self._model_map = { + "Linear Regression": LinearRegressionModel, + "Random Forest Regressor": RandomForestModel, + "Support Vector Machine": SVMModel + } - def list_models(self) -> List[str]: - return self._available_models + def list_models(self) -> List[InterfaceBaseMLModel]: + return [model_class() for model_class in self._model_map.values()] def is_model_available(self, model_name: str) -> bool: - return model_name in self._available_models + return model_name in self._model_map + + def get_model_instance(self, model_name: str) -> InterfaceBaseMLModel: + if not self.is_model_available(model_name): + raise ValueError(f"Model '{model_name}' is not available.") + return self._model_map[model_name]() + class MLModelManager(InterfaceMLModelManager): def __init__(self, repository: InterfaceMLModelRepository): self._repository = repository - self._current_model = "Random Forest Regressor" + self._current_model = repository.get_model_instance( + "Random Forest Regressor") def get_current_model(self) -> str: return self._current_model def switch_model(self, model_name: str) -> bool: if self._repository.is_model_available(model_name): - self._current_model = model_name + self._current_model = self._repository.get_model_instance( + model_name) return True return False - diff --git a/app/clients/service/ml_models_router.py b/app/clients/service/ml_models_router.py index aa3ec3ec..f9ffe659 100644 --- a/app/clients/service/ml_models_router.py +++ b/app/clients/service/ml_models_router.py @@ -7,8 +7,8 @@ @router.get("/list") def list_models(): """List all available ML models""" - return {"models": model_repository.list_models()} - + # return {"models": model_repository.list_models()} + return {"models": [str(model) for model in model_repository.list_models()]} @router.post("/switch/{model_name}") def switch_models(model_name: str): @@ -22,4 +22,5 @@ def switch_models(model_name: str): @router.get("/current") def current_model(): """Get the current ML model""" - return {"current_model": model_manager.get_current_model()} + # return {"current_model": model_manager.get_current_model()} + return {"current_model": str(model_manager.get_current_model())} \ No newline at end of file diff --git a/app/clients/service/model.py b/app/clients/service/model.py index 16097276..973c03d5 100644 --- a/app/clients/service/model.py +++ b/app/clients/service/model.py @@ -18,10 +18,32 @@ from sklearn import svm # Local imports -from ml_models import MLModels +from ml_models import InterfaceBaseMLModel, MLModelRepository, \ + LinearRegressionModel, RandomForestModel, SVMModel + +repo = MLModelRepository() default_unformatted_model_path = "pretrained_models" + os.sep + "model_{}.pkl" + +def get_model_by_name(model_type: str, n_estimators=100, + random_state=42) -> InterfaceBaseMLModel: + model_map = { + "Linear Regression": LinearRegressionModel, + "Random Forest Regressor": lambda: RandomForestModel(n_estimators, + random_state), + "Support Vector Machine": SVMModel + } + + if model_type not in model_map: + print(f"ERROR! Invalid model type '{model_type}' passed in.") + print(f"Available models: {repo.list_models()}") + sys.exit(-1) + + constructor = model_map[model_type] + return constructor() if callable(constructor) else constructor() + + def prepare_model_data(test_size=0.2, random_state=42): """ Prepare and train the Random Forest model using the dataset. @@ -36,29 +58,29 @@ def prepare_model_data(test_size=0.2, random_state=42): data = pd.read_csv('data_commontool.csv') # Define feature columns feature_columns = [ - 'age', # Client's age - 'gender', # Client's gender (bool) - 'work_experience', # Years of work experience - 'canada_workex', # Years of work experience in Canada - 'dep_num', # Number of dependents - 'canada_born', # Born in Canada - 'citizen_status', # Citizenship status - 'level_of_schooling', # Highest level achieved (1-14) - 'fluent_english', # English fluency scale (1-10) - 'reading_english_scale', # Reading ability scale (1-10) - 'speaking_english_scale',# Speaking ability scale (1-10) - 'writing_english_scale', # Writing ability scale (1-10) - 'numeracy_scale', # Numeracy ability scale (1-10) - 'computer_scale', # Computer proficiency scale (1-10) - 'transportation_bool', # Needs transportation support (bool) - 'caregiver_bool', # Is primary caregiver (bool) - 'housing', # Housing situation (1-10) - 'income_source', # Source of income (1-10) - 'felony_bool', # Has a felony (bool) - 'attending_school', # Currently a student (bool) - 'currently_employed', # Currently employed (bool) - 'substance_use', # Substance use disorder (bool) - 'time_unemployed', # Years unemployed + 'age', # Client's age + 'gender', # Client's gender (bool) + 'work_experience', # Years of work experience + 'canada_workex', # Years of work experience in Canada + 'dep_num', # Number of dependents + 'canada_born', # Born in Canada + 'citizen_status', # Citizenship status + 'level_of_schooling', # Highest level achieved (1-14) + 'fluent_english', # English fluency scale (1-10) + 'reading_english_scale', # Reading ability scale (1-10) + 'speaking_english_scale', # Speaking ability scale (1-10) + 'writing_english_scale', # Writing ability scale (1-10) + 'numeracy_scale', # Numeracy ability scale (1-10) + 'computer_scale', # Computer proficiency scale (1-10) + 'transportation_bool', # Needs transportation support (bool) + 'caregiver_bool', # Is primary caregiver (bool) + 'housing', # Housing situation (1-10) + 'income_source', # Source of income (1-10) + 'felony_bool', # Has a felony (bool) + 'attending_school', # Currently a student (bool) + 'currently_employed', # Currently employed (bool) + 'substance_use', # Substance use disorder (bool) + 'time_unemployed', # Years unemployed 'need_mental_health_support_bool' # Needs mental health support (bool) ] # Define intervention columns @@ -77,7 +99,8 @@ def prepare_model_data(test_size=0.2, random_state=42): features = np.array(data[all_features]) # Changed from X to features targets = np.array(data['success_rate']) # Changed from y to targets # Split the dataset - X_train, x_test, Y_train, y_test = train_test_split( # Removed unused variables + X_train, x_test, Y_train, y_test = train_test_split( + # Removed unused variables features, targets, test_size=test_size, @@ -87,7 +110,8 @@ def prepare_model_data(test_size=0.2, random_state=42): return X_train, x_test, Y_train, y_test -def train_model(X_train, Y_train, model_type, n_estimators=100, random_state=42): +def train_model(X_train, Y_train, model_type, n_estimators=100, + random_state=42) -> InterfaceBaseMLModel: """ Trains the model Args: @@ -100,25 +124,11 @@ def train_model(X_train, Y_train, model_type, n_estimators=100, random_state=42) Returns: A trained model of the type specified """ - # TODO the way model is selected should be improved - # Instantiate machine learning model - if model_type == MLModels.available_models[0]: # Linear regression - model = LinearRegression() - - elif model_type == MLModels.available_models[1]: # Random forest - model = RandomForestRegressor(n_estimators=n_estimators, random_state=random_state) - - elif model_type == MLModels.available_models[2]: # Support Vector Machine - model = svm.SVR() - - else: # Invalid model passed in - print("ERROR! Invalid model passed in. Exiting...") - sys.exit(-1) - - # Fit model with training data + model = get_model_by_name(model_type, n_estimators, random_state) model.fit(X_train, Y_train) return model + def get_true_file_name(model_type, filename): """ Takes a model type and file name, formats model type, and replaces spaces with underscores @@ -144,6 +154,7 @@ def save_model(model, model_type, filename=default_unformatted_model_path): with open(true_file_name, "wb") as model_file: pickle.dump(model, model_file) + def load_model(model_type, filename=default_unformatted_model_path): """ Load a trained model from a file. @@ -159,6 +170,7 @@ def load_model(model_type, filename=default_unformatted_model_path): with open(true_file_name, "rb") as model_file: return pickle.load(model_file) + def main(argv): """Main function to train and save the model.""" # Get the model type from the command line arguments @@ -171,5 +183,6 @@ def main(argv): save_model(model, model_type) print("Model training completed and saved successfully.") + if __name__ == "__main__": main(sys.argv) diff --git a/requirements.txt b/requirements.txt index 93d35fbf..c5910a3b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -141,4 +141,4 @@ webencodings==0.5.1 websocket-client==1.5.1 websockets==11.0.3 widgetsnbextension==4.0.7 - +mypy=1.15.1 From 9e3567f081f8148dc92ef7b78bbb1d7ed4ed0222 Mon Sep 17 00:00:00 2001 From: Fran Date: Sun, 23 Mar 2025 12:29:50 -0700 Subject: [PATCH 06/18] added black, isort, mypy, pylint --- .pylintrc | 637 ++++++++++++++++++++++++ app/auth/router.py | 50 +- app/clients/router.py | 68 +-- app/clients/schema.py | 17 +- app/clients/service/client_service.py | 177 +++---- app/clients/service/logic.py | 159 ++++-- app/clients/service/ml_models.py | 16 +- app/clients/service/ml_models_router.py | 8 +- app/clients/service/model.py | 111 +++-- app/database.py | 13 +- app/main.py | 17 +- app/models.py | 54 +- mypy.ini | 40 ++ pyproject.toml | 18 + 14 files changed, 1081 insertions(+), 304 deletions(-) create mode 100644 .pylintrc create mode 100644 mypy.ini create mode 100644 pyproject.toml diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 00000000..dee81e31 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,637 @@ +[MAIN] + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Clear in-memory caches upon conclusion of linting. Useful if running pylint +# in a server-like mode. +clear-cache-post-run=no + +# Load and enable all available extensions. Use --list-extensions to see a list +# all available extensions. +#enable-all-extensions= + +# In error mode, messages with a category besides ERROR or FATAL are +# suppressed, and no reports are done by default. Error mode is compatible with +# disabling specific errors. +#errors-only= + +# Always return a 0 (non-error) status code, even if lint errors are found. +# This is primarily useful in continuous integration scripts. +#exit-zero= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-allow-list= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) +extension-pkg-whitelist= + +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +fail-on= + +# Specify a score threshold under which the program will exit with error. +fail-under=10 + +# Interpret the stdin as a python script, whose filename needs to be passed as +# the module_or_package argument. +#from-stdin= + +# Files or directories to be skipped. They should be base names, not paths. +ignore=CVS + +# Add files or directories matching the regular expressions patterns to the +# ignore-list. The regex matches against paths and can be in Posix or Windows +# format. Because '\\' represents the directory delimiter on Windows systems, +# it can't be used as an escape character. +ignore-paths= + +# Files or directories matching the regular expression patterns are skipped. +# The regex matches against base names, not paths. The default value ignores +# Emacs file locks +ignore-patterns=^\.# + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use, and will cap the count on Windows to +# avoid hangs. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Minimum Python version to use for version dependent checks. Will default to +# the version used to run pylint. +py-version=3.10 + +# Discover python modules and packages in the file system subtree. +recursive=no + +# Add paths to the list of the source roots. Supports globbing patterns. The +# source root is an absolute path or a path relative to the current working +# directory used to determine a package namespace for modules located under the +# source root. +source-roots= + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# In verbose mode, extra non-checker-related info will be displayed. +#verbose= + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. If left empty, argument names will be checked with the set +# naming style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. If left empty, attribute names will be checked with the set naming +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. If left empty, class attribute names will be checked +# with the set naming style. +#class-attribute-rgx= + +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. If left empty, class constant names will be checked with +# the set naming style. +#class-const-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. If left empty, class names will be checked with the set naming style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. If left empty, constant names will be checked with the set naming +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. If left empty, function names will be checked with the set +# naming style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. If left empty, inline iteration names will be checked +# with the set naming style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. If left empty, method names will be checked with the set naming style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. If left empty, module names will be checked with the set naming style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Regular expression matching correct type alias names. If left empty, type +# alias names will be checked with the set naming style. +#typealias-rgx= + +# Regular expression matching correct type variable names. If left empty, type +# variable names will be checked with the set naming style. +#typevar-rgx= + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. If left empty, variable names will be checked with the set +# naming style. +#variable-rgx= + + +[CLASSES] + +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + asyncSetUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[DESIGN] + +# List of regular expressions of class ancestor names to ignore when counting +# public methods (see R0903) +exclude-too-few-public-methods= + +# List of qualified class names to ignore when counting class parents (see +# R0901) +ignored-parents= + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when caught. +overgeneral-exceptions=builtins.BaseException,builtins.Exception + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow explicit reexports by alias from a package __init__. +allow-reexport-from-package=no + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules= + +# Output a graph (.gv or any supported image format) of external dependencies +# to the given file (report RP0402 must not be disabled). +ext-import-graph= + +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be +# disabled). +import-graph= + +# Output a graph (.gv or any supported image format) of internal dependencies +# to the given file (report RP0402 must not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, +# UNDEFINED. +confidence=HIGH, + CONTROL_FLOW, + INFERENCE, + INFERENCE_FAILURE, + UNDEFINED + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then re-enable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + use-implicit-booleaness-not-comparison-to-string, + use-implicit-booleaness-not-comparison-to-zero, + missing-module-docstring, + missing-class-docstring, + missing-function-docstring + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable= + + +[METHOD_ARGS] + +# List of qualified names (i.e., library.method) which require a timeout +# parameter e.g. 'requests.api.get,requests.api.post' +timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +notes-rgx= + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'fatal', 'error', 'warning', 'refactor', +# 'convention', and 'info' which contain the number of messages in each +# category, as well as 'statement' which is the total number of statements +# analyzed. This score is used by the global evaluation report (RP0004). +evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +msg-template= + +# Set the output format. Available formats are: text, parseable, colorized, +# json2 (improved json format), json (old json format) and msvs (visual +# studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +#output-format=colorized + +# Tells whether to display a full report or only the messages. +reports=yes + +# Activate the evaluation score. +score=yes + + +[SIMILARITIES] + +# Comments are removed from the similarity computation +ignore-comments=yes + +# Docstrings are removed from the similarity computation +ignore-docstrings=yes + +# Imports are removed from the similarity computation +ignore-imports=yes + +# Signatures are removed from the similarity computation +ignore-signatures=yes + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. No available dictionaries : You need to install +# both the python package and the system dependency for enchant to work. +spelling-dict= + +# List of comma separated words that should be considered directives if they +# appear at the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of symbolic message names to ignore for Mixin members. +ignored-checks-for-mixins=no-member, + not-async-context-manager, + not-context-manager, + attribute-defined-outside-init + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# Regex pattern to define which classes are considered mixins. +mixin-class-rgx=.*[Mm]ixin + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of names allowed to shadow builtins +allowed-redefined-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io diff --git a/app/auth/router.py b/app/auth/router.py index 229ee71d..3f5455ee 100644 --- a/app/auth/router.py +++ b/app/auth/router.py @@ -1,28 +1,32 @@ from datetime import datetime, timedelta from typing import Optional + from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from jose import JWTError, jwt +from passlib.context import CryptContext +from pydantic import BaseModel, Field, validator from sqlalchemy.orm import Session + from app.database import get_db from app.models import User, UserRole -from passlib.context import CryptContext -from pydantic import BaseModel, Field, validator router = APIRouter(prefix="/auth", tags=["authentication"]) + class UserCreate(BaseModel): username: str = Field(..., min_length=3, max_length=50) email: str password: str role: UserRole - @validator('role') + @validator("role") def validate_role(cls, v): if v not in [UserRole.admin, UserRole.case_worker]: - raise ValueError('Role must be either admin or case_worker') + raise ValueError("Role must be either admin or case_worker") return v + class UserResponse(BaseModel): username: str email: str @@ -31,6 +35,7 @@ class UserResponse(BaseModel): class Config: from_attributes = True + # Configuration SECRET_KEY = "your-secret-key-here" ALGORITHM = "HS256" @@ -39,18 +44,22 @@ class Config: pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/token") + def verify_password(plain_password: str, hashed_password: str) -> bool: return pwd_context.verify(plain_password, hashed_password) + def get_password_hash(password: str) -> str: return pwd_context.hash(password) + def authenticate_user(db: Session, username: str, password: str) -> Optional[User]: user = db.query(User).filter(User.username == username).first() if not user or not verify_password(password, user.hashed_password): return None return user + def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): to_encode = data.copy() if expires_delta: @@ -61,9 +70,9 @@ def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt + async def get_current_user( - token: str = Depends(oauth2_scheme), - db: Session = Depends(get_db) + token: str = Depends(oauth2_scheme), db: Session = Depends(get_db) ) -> User: credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -77,24 +86,25 @@ async def get_current_user( raise credentials_exception except JWTError: raise credentials_exception - + user = db.query(User).filter(User.username == username).first() if user is None: raise credentials_exception return user + def get_admin_user(current_user: User = Depends(get_current_user)): if current_user.role != UserRole.admin: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail="Only admin users can perform this operation" + detail="Only admin users can perform this operation", ) return current_user + @router.post("/token") async def login_for_access_token( - form_data: OAuth2PasswordRequestForm = Depends(), - db: Session = Depends(get_db) + form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db) ): user = authenticate_user(db, form_data.username, form_data.password) if not user: @@ -109,25 +119,24 @@ async def login_for_access_token( ) return {"access_token": access_token, "token_type": "bearer"} + @router.post("/users", response_model=UserResponse) async def create_user( user_data: UserCreate, current_user: User = Depends(get_admin_user), - db: Session = Depends(get_db) + db: Session = Depends(get_db), ): """Create a new user (admin only)""" # Check if username exists if db.query(User).filter(User.username == user_data.username).first(): raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Username already registered" + status_code=status.HTTP_400_BAD_REQUEST, detail="Username already registered" ) - + # Check if email exists if db.query(User).filter(User.email == user_data.email).first(): raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Email already registered" + status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered" ) # Create new user @@ -135,9 +144,9 @@ async def create_user( username=user_data.username, email=user_data.email, hashed_password=get_password_hash(user_data.password), - role=user_data.role + role=user_data.role, ) - + try: db.add(db_user) db.commit() @@ -145,7 +154,4 @@ async def create_user( return db_user except Exception as e: db.rollback() - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=str(e) - ) + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)) diff --git a/app/clients/router.py b/app/clients/router.py index 4ecc83e4..45c385e5 100644 --- a/app/clients/router.py +++ b/app/clients/router.py @@ -3,42 +3,44 @@ Handles all HTTP requests for client operations including create, read, update, and delete. """ -from fastapi import APIRouter, Depends, HTTPException, status, Query -from sqlalchemy.orm import Session from typing import List, Optional -from app.auth.router import get_current_user, get_admin_user -from app.models import User, UserRole -from app.database import get_db -from app.clients.service.client_service import ClientService +from fastapi import APIRouter, Depends, HTTPException, Query, status +from sqlalchemy.orm import Session + +from app.auth.router import get_admin_user, get_current_user from app.clients.schema import ( - ClientResponse, - ClientUpdate, ClientListResponse, + ClientResponse, + ClientUpdate, ServiceResponse, - ServiceUpdate + ServiceUpdate, ) +from app.clients.service.client_service import ClientService +from app.database import get_db +from app.models import User, UserRole router = APIRouter(prefix="/clients", tags=["clients"]) + @router.get("/", response_model=ClientListResponse) async def get_clients( - current_user: User = Depends(get_admin_user), + current_user: User = Depends(get_admin_user), skip: int = Query(default=0, ge=0, description="Number of records to skip"), limit: int = Query(default=50, ge=1, le=150, description="Maximum number of records to return"), - db: Session = Depends(get_db) + db: Session = Depends(get_db), ): return ClientService.get_clients(db, skip, limit) + @router.get("/{client_id}", response_model=ClientResponse) async def get_client( - client_id: int, - current_user: User = Depends(get_admin_user), - db: Session = Depends(get_db) + client_id: int, current_user: User = Depends(get_admin_user), db: Session = Depends(get_db) ): """Get a specific client by ID""" return ClientService.get_client(db, client_id) + @router.get("/search/by-criteria", response_model=List[ClientResponse]) async def get_clients_by_criteria( employment_status: Optional[bool] = None, @@ -66,7 +68,7 @@ async def get_clients_by_criteria( time_unemployed: Optional[int] = Query(None, ge=0), need_mental_health_support_bool: Optional[bool] = None, current_user: User = Depends(get_admin_user), - db: Session = Depends(get_db) + db: Session = Depends(get_db), ): """Search clients by any combination of criteria""" return ClientService.get_clients_by_criteria( @@ -94,9 +96,10 @@ async def get_clients_by_criteria( attending_school=attending_school, substance_use=substance_use, time_unemployed=time_unemployed, - need_mental_health_support_bool=need_mental_health_support_bool + need_mental_health_support_bool=need_mental_health_support_bool, ) + @router.get("/search/by-services", response_model=List[ClientResponse]) async def get_clients_by_services( employment_assistance: Optional[bool] = None, @@ -107,7 +110,7 @@ async def get_clients_by_services( employer_financial_supports: Optional[bool] = None, enhanced_referrals: Optional[bool] = None, current_user: User = Depends(get_admin_user), - db: Session = Depends(get_db) + db: Session = Depends(get_db), ): """Get clients filtered by multiple service statuses""" return ClientService.get_clients_by_services( @@ -118,70 +121,73 @@ async def get_clients_by_services( specialized_services=specialized_services, employment_related_financial_supports=employment_related_financial_supports, employer_financial_supports=employer_financial_supports, - enhanced_referrals=enhanced_referrals + enhanced_referrals=enhanced_referrals, ) + @router.get("/{client_id}/services", response_model=List[ServiceResponse]) async def get_client_services( - client_id: int, - current_user: User = Depends(get_admin_user), - db: Session = Depends(get_db) + client_id: int, current_user: User = Depends(get_admin_user), db: Session = Depends(get_db) ): """Get all services and their status for a specific client, including case worker info""" return ClientService.get_client_services(db, client_id) + @router.get("/search/success-rate", response_model=List[ClientResponse]) async def get_clients_by_success_rate( min_rate: int = Query(70, ge=0, le=100, description="Minimum success rate percentage"), current_user: User = Depends(get_admin_user), - db: Session = Depends(get_db) + db: Session = Depends(get_db), ): """Get clients with success rate above specified threshold""" return ClientService.get_clients_by_success_rate(db, min_rate) + @router.get("/case-worker/{case_worker_id}", response_model=List[ClientResponse]) async def get_clients_by_case_worker( case_worker_id: int, - current_user: User = Depends(get_current_user), - db: Session = Depends(get_db) + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), ): return ClientService.get_clients_by_case_worker(db, case_worker_id) + @router.put("/{client_id}", response_model=ClientResponse) async def update_client( client_id: int, client_data: ClientUpdate, current_user: User = Depends(get_admin_user), - db: Session = Depends(get_db) + db: Session = Depends(get_db), ): """Update a client's information""" return ClientService.update_client(db, client_id, client_data) + @router.put("/{client_id}/services/{user_id}", response_model=ServiceResponse) async def update_client_services( client_id: int, user_id: int, service_update: ServiceUpdate, - current_user: User = Depends(get_current_user), - db: Session = Depends(get_db) + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), ): return ClientService.update_client_services(db, client_id, user_id, service_update) + @router.post("/{client_id}/case-assignment", response_model=ServiceResponse) async def create_case_assignment( client_id: int, case_worker_id: int = Query(..., description="Case worker ID to assign"), current_user: User = Depends(get_admin_user), - db: Session = Depends(get_db) + db: Session = Depends(get_db), ): """Create a new case assignment for a client with a case worker""" return ClientService.create_case_assignment(db, client_id, case_worker_id) + @router.delete("/{client_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_client( - client_id: int, - current_user: User = Depends(get_admin_user), - db: Session = Depends(get_db) + client_id: int, current_user: User = Depends(get_admin_user), db: Session = Depends(get_db) ): """Delete a client""" ClientService.delete_client(db, client_id) diff --git a/app/clients/schema.py b/app/clients/schema.py index cff28897..0120d1cf 100644 --- a/app/clients/schema.py +++ b/app/clients/schema.py @@ -3,22 +3,27 @@ Defines schemas for client data, predictions, and API responses. """ +from enum import IntEnum +from typing import List, Optional + # Standard library imports from pydantic import BaseModel, Field, validator -from typing import Optional, List -from enum import IntEnum + from app.models import UserRole + # Enums for validation class Gender(IntEnum): MALE = 1 FEMALE = 2 + class PredictionInput(BaseModel): """ Schema for prediction input data containing all client assessment fields. Used for making predictions about client outcomes. """ + age: int gender: str work_experience: int @@ -44,6 +49,7 @@ class PredictionInput(BaseModel): time_unemployed: int need_mental_health_support_bool: str + class ClientBase(BaseModel): age: int = Field(ge=18, description="Age of client, must be 18 or older") gender: Gender = Field(description="Gender: 1 for male, 2 for female") @@ -96,16 +102,18 @@ class Config: "currently_employed": False, "substance_use": False, "time_unemployed": 6, - "need_mental_health_support_bool": False + "need_mental_health_support_bool": False, } } + class ClientResponse(ClientBase): id: int class Config: from_attributes = True + class ClientUpdate(BaseModel): age: Optional[int] = Field(None, ge=18) gender: Optional[Gender] = None @@ -132,6 +140,7 @@ class ClientUpdate(BaseModel): time_unemployed: Optional[int] = Field(None, ge=0) need_mental_health_support_bool: Optional[bool] = None + class ServiceResponse(BaseModel): client_id: int user_id: int @@ -147,6 +156,7 @@ class ServiceResponse(BaseModel): class Config: from_attributes = True + class ServiceUpdate(BaseModel): employment_assistance: Optional[bool] = None life_stabilization: Optional[bool] = None @@ -157,6 +167,7 @@ class ServiceUpdate(BaseModel): enhanced_referrals: Optional[bool] = None success_rate: Optional[int] = Field(None, ge=0, le=100) + class ClientListResponse(BaseModel): clients: List[ClientResponse] total: int diff --git a/app/clients/service/client_service.py b/app/clients/service/client_service.py index c743acb4..16115ec1 100644 --- a/app/clients/service/client_service.py +++ b/app/clients/service/client_service.py @@ -4,16 +4,19 @@ """ from abc import ABC, abstractmethod -from sqlalchemy.orm import Session -from sqlalchemy import and_ +from typing import Any, Dict, List, Optional + from fastapi import HTTPException, status -from typing import List, Optional, Dict, Any +from sqlalchemy import and_ +from sqlalchemy.orm import Session + +from app.clients.schema import ClientUpdate, ServiceResponse, ServiceUpdate from app.models import Client, ClientCase, User -from app.clients.schema import ClientUpdate, ServiceUpdate, ServiceResponse class InterfaceClientQueryService(ABC): """Interface for client query operations""" + @abstractmethod def get_client(self, db: Session, client_id: int) -> Client: """Get a specific client by ID""" @@ -21,7 +24,7 @@ def get_client(self, db: Session, client_id: int) -> Client: @abstractmethod def get_clients(self, db: Session, skip: int, limit: int) -> Dict[str, Any]: - """ Get clients with optional pagination.""" + """Get clients with optional pagination.""" pass @abstractmethod @@ -30,45 +33,45 @@ def get_clients_by_criteria(self, db: Session, **criteria) -> List[Client]: pass @abstractmethod - def get_clients_by_services(self, db: Session, **service_filters) -> \ - List[Client]: - """ Get clients filtered by multiple service statuses.""" + def get_clients_by_services(self, db: Session, **service_filters) -> List[Client]: + """Get clients filtered by multiple service statuses.""" pass @abstractmethod - def get_client_services(self, db: Session, client_id: int) -> List[ - ClientCase]: + def get_client_services(self, db: Session, client_id: int) -> List[ClientCase]: pass @abstractmethod - def get_clients_by_success_rate(self, db: Session, min_rate: int) -> List[ - Client]: + def get_clients_by_success_rate(self, db: Session, min_rate: int) -> List[Client]: pass @abstractmethod - def get_clients_by_case_worker(self, db: Session, case_worker_id: int) -> \ - List[Client]: + def get_clients_by_case_worker(self, db: Session, case_worker_id: int) -> List[Client]: pass class InterfaceClientManagementService(ABC): """Interface for client management operations""" + @abstractmethod - def update_client(self, db: Session, client_id: int, - client_update: ClientUpdate) -> ClientUpdate: + def update_client( + self, db: Session, client_id: int, client_update: ClientUpdate + ) -> ClientUpdate: """Update a client's information""" pass @abstractmethod - def update_client_services(self, db: Session, client_id: int, user_id: int, - service_update: ServiceUpdate) -> ClientCase: + def update_client_services( + self, db: Session, client_id: int, user_id: int, service_update: ServiceUpdate + ) -> ClientCase: """Update a client's services and outcomes for a specific caseworker""" pass @abstractmethod - def create_case_assignment(self, db: Session, client_id: int, - case_worker_id: int) -> ClientCase: + def create_case_assignment( + self, db: Session, client_id: int, case_worker_id: int + ) -> ClientCase: """Create a new case assignment""" pass @@ -88,7 +91,7 @@ def get_client(db: Session, client_id: int): if not client: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail=f"Client with id {client_id} not found" + detail=f"Client with id {client_id} not found", ) return client @@ -100,13 +103,11 @@ def get_clients(db: Session, skip: int = 0, limit: int = 50): """ if skip < 0: raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Skip value cannot be negative" + status_code=status.HTTP_400_BAD_REQUEST, detail="Skip value cannot be negative" ) if limit < 1: raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Limit must be greater than 0" + status_code=status.HTTP_400_BAD_REQUEST, detail="Limit must be greater than 0" ) clients = db.query(Client).offset(skip).limit(limit).all() @@ -139,7 +140,7 @@ def get_clients_by_criteria( attending_school: Optional[bool] = None, substance_use: Optional[bool] = None, time_unemployed: Optional[int] = None, - need_mental_health_support_bool: Optional[bool] = None + need_mental_health_support_bool: Optional[bool] = None, ): """Get clients filtered by any combination of criteria""" query = db.query(Client) @@ -147,19 +148,17 @@ def get_clients_by_criteria( if education_level is not None and not (1 <= education_level <= 14): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="Education level must be between 1 and 14" + detail="Education level must be between 1 and 14", ) if age_min is not None and age_min < 18: raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Minimum age must be at least 18" + status_code=status.HTTP_400_BAD_REQUEST, detail="Minimum age must be at least 18" ) if gender is not None and gender not in [1, 2]: raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Gender must be 1 or 2" + status_code=status.HTTP_400_BAD_REQUEST, detail="Gender must be 1 or 2" ) # Apply filters for non-None values @@ -184,21 +183,17 @@ def get_clients_by_criteria( if fluent_english is not None: query = query.filter(Client.fluent_english == fluent_english) if reading_english_scale is not None: - query = query.filter( - Client.reading_english_scale == reading_english_scale) + query = query.filter(Client.reading_english_scale == reading_english_scale) if speaking_english_scale is not None: - query = query.filter( - Client.speaking_english_scale == speaking_english_scale) + query = query.filter(Client.speaking_english_scale == speaking_english_scale) if writing_english_scale is not None: - query = query.filter( - Client.writing_english_scale == writing_english_scale) + query = query.filter(Client.writing_english_scale == writing_english_scale) if numeracy_scale is not None: query = query.filter(Client.numeracy_scale == numeracy_scale) if computer_scale is not None: query = query.filter(Client.computer_scale == computer_scale) if transportation_bool is not None: - query = query.filter( - Client.transportation_bool == transportation_bool) + query = query.filter(Client.transportation_bool == transportation_bool) if caregiver_bool is not None: query = query.filter(Client.caregiver_bool == caregiver_bool) if housing is not None: @@ -215,21 +210,19 @@ def get_clients_by_criteria( query = query.filter(Client.time_unemployed == time_unemployed) if need_mental_health_support_bool is not None: query = query.filter( - Client.need_mental_health_support_bool == need_mental_health_support_bool) + Client.need_mental_health_support_bool == need_mental_health_support_bool + ) try: return query.all() except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Error retrieving clients: {str(e)}" + detail=f"Error retrieving clients: {str(e)}", ) @staticmethod - def get_clients_by_services( - db: Session, - **service_filters: Optional[bool] - ): + def get_clients_by_services(db: Session, **service_filters: Optional[bool]): """ Get clients filtered by multiple service statuses. """ @@ -245,18 +238,17 @@ def get_clients_by_services( except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Error retrieving clients: {str(e)}" + detail=f"Error retrieving clients: {str(e)}", ) @staticmethod def get_client_services(db: Session, client_id: int): """Get all services for a specific client with caseworker info""" - client_cases = db.query(ClientCase).filter( - ClientCase.client_id == client_id).all() + client_cases = db.query(ClientCase).filter(ClientCase.client_id == client_id).all() if not client_cases: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail=f"No services found for client with id {client_id}" + detail=f"No services found for client with id {client_id}", ) return client_cases @@ -266,12 +258,10 @@ def get_clients_by_success_rate(db: Session, min_rate: int = 70): if not (0 <= min_rate <= 100): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="Success rate must be between 0 and 100" + detail="Success rate must be between 0 and 100", ) - return db.query(Client).join(ClientCase).filter( - ClientCase.success_rate >= min_rate - ).all() + return db.query(Client).join(ClientCase).filter(ClientCase.success_rate >= min_rate).all() @staticmethod def get_clients_by_case_worker(db: Session, case_worker_id: int): @@ -280,12 +270,10 @@ def get_clients_by_case_worker(db: Session, case_worker_id: int): if not case_worker: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail=f"Case worker with id {case_worker_id} not found" + detail=f"Case worker with id {case_worker_id} not found", ) - return db.query(Client).join(ClientCase).filter( - ClientCase.user_id == case_worker_id - ).all() + return db.query(Client).join(ClientCase).filter(ClientCase.user_id == case_worker_id).all() class ClientManagementService(InterfaceClientManagementService): @@ -298,7 +286,7 @@ def update_client(db: Session, client_id: int, client_update: ClientUpdate): if not client: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail=f"Client with id {client_id} not found" + detail=f"Client with id {client_id} not found", ) update_data = client_update.dict(exclude_unset=True) @@ -313,27 +301,25 @@ def update_client(db: Session, client_id: int, client_update: ClientUpdate): db.rollback() raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to update client: {str(e)}" + detail=f"Failed to update client: {str(e)}", ) @staticmethod def update_client_services( - db: Session, - client_id: int, - user_id: int, - service_update: ServiceUpdate + db: Session, client_id: int, user_id: int, service_update: ServiceUpdate ): """Update a client's services and outcomes for a specific case worker""" - client_case = db.query(ClientCase).filter( - ClientCase.client_id == client_id, - ClientCase.user_id == user_id - ).first() + client_case = ( + db.query(ClientCase) + .filter(ClientCase.client_id == client_id, ClientCase.user_id == user_id) + .first() + ) if not client_case: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"No case found for client {client_id} with case worker {user_id}. " - f"Cannot update services for a non-existent case assignment." + f"Cannot update services for a non-existent case assignment.", ) update_data = service_update.dict(exclude_unset=True) @@ -348,22 +334,18 @@ def update_client_services( db.rollback() raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to update client services: {str(e)}" + detail=f"Failed to update client services: {str(e)}", ) @staticmethod - def create_case_assignment( - db: Session, - client_id: int, - case_worker_id: int - ): + def create_case_assignment(db: Session, client_id: int, case_worker_id: int): """Create a new case assignment""" # Check if client exists client = db.query(Client).filter(Client.id == client_id).first() if not client: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail=f"Client with id {client_id} not found" + detail=f"Client with id {client_id} not found", ) # Check if caseworker exists @@ -371,19 +353,20 @@ def create_case_assignment( if not case_worker: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail=f"Case worker with id {case_worker_id} not found" + detail=f"Case worker with id {case_worker_id} not found", ) # Check if assignment already exists - existing_case = db.query(ClientCase).filter( - ClientCase.client_id == client_id, - ClientCase.user_id == case_worker_id - ).first() + existing_case = ( + db.query(ClientCase) + .filter(ClientCase.client_id == client_id, ClientCase.user_id == case_worker_id) + .first() + ) if existing_case: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Client {client_id} already has a case assigned to case worker {case_worker_id}" + detail=f"Client {client_id} already has a case assigned to case worker {case_worker_id}", ) try: @@ -398,7 +381,7 @@ def create_case_assignment( employment_related_financial_supports=False, employer_financial_supports=False, enhanced_referrals=False, - success_rate=0 + success_rate=0, ) db.add(new_case) db.commit() @@ -409,7 +392,7 @@ def create_case_assignment( db.rollback() raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to create case assignment: {str(e)}" + detail=f"Failed to create case assignment: {str(e)}", ) @staticmethod @@ -420,14 +403,12 @@ def delete_client(db: Session, client_id: int): if not client: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail=f"Client with id {client_id} not found" + detail=f"Client with id {client_id} not found", ) try: # Delete associated client_cases - db.query(ClientCase).filter( - ClientCase.client_id == client_id - ).delete() + db.query(ClientCase).filter(ClientCase.client_id == client_id).delete() # Delete the client db.delete(client) @@ -437,7 +418,7 @@ def delete_client(db: Session, client_id: int): db.rollback() raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to delete client: {str(e)}" + detail=f"Failed to delete client: {str(e)}", ) @@ -479,21 +460,19 @@ def get_clients_by_case_worker(db: Session, case_worker_id: int): # Modification methods @staticmethod def update_client(db: Session, client_id: int, client_update: ClientUpdate): - return ClientManagementService.update_client(db, client_id, - client_update) + return ClientManagementService.update_client(db, client_id, client_update) @staticmethod - def update_client_services(db: Session, client_id: int, user_id: int, - service_update: ServiceUpdate): - return ClientManagementService.update_client_services(db, client_id, - user_id, - service_update) + def update_client_services( + db: Session, client_id: int, user_id: int, service_update: ServiceUpdate + ): + return ClientManagementService.update_client_services( + db, client_id, user_id, service_update + ) @staticmethod - def create_case_assignment(db: Session, client_id: int, - case_worker_id: int): - return ClientManagementService.create_case_assignment(db, client_id, - case_worker_id) + def create_case_assignment(db: Session, client_id: int, case_worker_id: int): + return ClientManagementService.create_case_assignment(db, client_id, case_worker_id) @staticmethod def delete_client(db: Session, client_id: int): diff --git a/app/clients/service/logic.py b/app/clients/service/logic.py index c25b4217..290e1dd9 100644 --- a/app/clients/service/logic.py +++ b/app/clients/service/logic.py @@ -5,30 +5,33 @@ # Standard library imports import os -#import json -from itertools import product # Third-party imports import pickle + +# import json +from itertools import product + import numpy as np # Constants COLUMN_INTERVENTIONS = [ - 'Life Stabilization', - 'General Employment Assistance Services', - 'Retention Services', - 'Specialized Services', - 'Employment-Related Financial Supports for Job Seekers and Employers', - 'Employer Financial Supports', - 'Enhanced Referrals for Skills Development' + "Life Stabilization", + "General Employment Assistance Services", + "Retention Services", + "Specialized Services", + "Employment-Related Financial Supports for Job Seekers and Employers", + "Employer Financial Supports", + "Enhanced Referrals for Skills Development", ] # Load model CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) -MODEL_PATH = os.path.join(CURRENT_DIR, 'model.pkl') +MODEL_PATH = os.path.join(CURRENT_DIR, "model.pkl") with open(MODEL_PATH, "rb") as model_file: MODEL = pickle.load(model_file) + def clean_input_data(input_data): """ Clean and transform input data into model-compatible format. @@ -40,13 +43,30 @@ def clean_input_data(input_data): list: Cleaned and formatted data ready for model input """ columns = [ - "age", "gender", "work_experience", "canada_workex", "dep_num", - "canada_born", "citizen_status", "level_of_schooling", "fluent_english", - "reading_english_scale", "speaking_english_scale", "writing_english_scale", - "numeracy_scale", "computer_scale", "transportation_bool", "caregiver_bool", - "housing", "income_source", "felony_bool", "attending_school", - "currently_employed", "substance_use", "time_unemployed", - "need_mental_health_support_bool" + "age", + "gender", + "work_experience", + "canada_workex", + "dep_num", + "canada_born", + "citizen_status", + "level_of_schooling", + "fluent_english", + "reading_english_scale", + "speaking_english_scale", + "writing_english_scale", + "numeracy_scale", + "computer_scale", + "transportation_bool", + "caregiver_bool", + "housing", + "income_source", + "felony_bool", + "attending_school", + "currently_employed", + "substance_use", + "time_unemployed", + "need_mental_health_support_bool", ] demographics = {key: input_data[key] for key in columns} output = [] @@ -57,6 +77,7 @@ def clean_input_data(input_data): output.append(value) return output + def convert_text(text_data: str): """ Convert text answers from front end into numerical values. @@ -68,33 +89,47 @@ def convert_text(text_data: str): int: Converted numerical value """ categorical_mappings = [ + {"": 0, "true": 1, "false": 0, "no": 0, "yes": 1, "No": 0, "Yes": 1}, { - "": 0, "true": 1, "false": 0, "no": 0, "yes": 1, - "No": 0, "Yes": 1 - }, - { - "Grade 0-8": 1, "Grade 9": 2, "Grade 10": 3, "Grade 11": 4, - "Grade 12 or equivalent": 5, "OAC or Grade 13": 6, - "Some college": 7, "Some university": 8, "Some apprenticeship": 9, - "Certificate of Apprenticeship": 10, "Journeyperson": 11, - "Certificate/Diploma": 12, "Bachelor's degree": 13, - "Post graduate": 14 + "Grade 0-8": 1, + "Grade 9": 2, + "Grade 10": 3, + "Grade 11": 4, + "Grade 12 or equivalent": 5, + "OAC or Grade 13": 6, + "Some college": 7, + "Some university": 8, + "Some apprenticeship": 9, + "Certificate of Apprenticeship": 10, + "Journeyperson": 11, + "Certificate/Diploma": 12, + "Bachelor's degree": 13, + "Post graduate": 14, }, { - "Renting-private": 1, "Renting-subsidized": 2, - "Boarding or lodging": 3, "Homeowner": 4, - "Living with family/friend": 5, "Institution": 6, - "Temporary second residence": 7, "Band-owned home": 8, - "Homeless or transient": 9, "Emergency hostel": 10 + "Renting-private": 1, + "Renting-subsidized": 2, + "Boarding or lodging": 3, + "Homeowner": 4, + "Living with family/friend": 5, + "Institution": 6, + "Temporary second residence": 7, + "Band-owned home": 8, + "Homeless or transient": 9, + "Emergency hostel": 10, }, { - "No Source of Income": 1, "Employment Insurance": 2, + "No Source of Income": 1, + "Employment Insurance": 2, "Workplace Safety and Insurance Board": 3, "Ontario Works applied or receiving": 4, "Ontario Disability Support Program applied or receiving": 5, - "Dependent of someone receiving OW or ODSP": 6, "Crown Ward": 7, - "Employment": 8, "Self-Employment": 9, "Other (specify)": 10 - } + "Dependent of someone receiving OW or ODSP": 6, + "Crown Ward": 7, + "Employment": 8, + "Self-Employment": 9, + "Other (specify)": 10, + }, ] for category in categorical_mappings: if text_data in category: @@ -102,6 +137,7 @@ def convert_text(text_data: str): return int(text_data) if text_data.isnumeric() else text_data + def create_matrix(row_data): """ Create matrix of all possible intervention combinations. @@ -116,6 +152,7 @@ def create_matrix(row_data): perms = intervention_permutations(7) return np.concatenate((np.array(data), np.array(perms)), axis=1) + def intervention_permutations(num): """ Generate all possible intervention combinations. @@ -128,6 +165,7 @@ def intervention_permutations(num): """ return np.array(list(product([0, 1], repeat=num))) + def get_baseline_row(row_data): """ Create baseline row with no interventions. @@ -141,6 +179,7 @@ def get_baseline_row(row_data): base_interventions = np.zeros(7) return np.concatenate((np.array(row_data), base_interventions)) + def intervention_row_to_names(row_data): """ Convert intervention row to list of intervention names. @@ -153,6 +192,7 @@ def intervention_row_to_names(row_data): """ return [COLUMN_INTERVENTIONS[i] for i, value in enumerate(row_data) if value == 1] + def process_results(baseline_pred, results_matrix): """ Process model results into structured output. @@ -164,14 +204,9 @@ def process_results(baseline_pred, results_matrix): Returns: dict: Processed results with baseline and interventions """ - result_list = [ - (row[-1], intervention_row_to_names(row[:-1])) - for row in results_matrix - ] - return { - "baseline": baseline_pred[-1], - "interventions": result_list - } + result_list = [(row[-1], intervention_row_to_names(row[:-1])) for row in results_matrix] + return {"baseline": baseline_pred[-1], "interventions": result_list} + def interpret_and_calculate(input_data): """ @@ -194,19 +229,33 @@ def interpret_and_calculate(input_data): top_results = result_matrix[-3:, -8:] return process_results(baseline_prediction, top_results) + if __name__ == "__main__": test_data = { - "age": "23", "gender": "1", "work_experience": "1", - "canada_workex": "1", "dep_num": "0", "canada_born": "1", - "citizen_status": "2", "level_of_schooling": "2", - "fluent_english": "3", "reading_english_scale": "2", - "speaking_english_scale": "2", "writing_english_scale": "3", - "numeracy_scale": "2", "computer_scale": "3", - "transportation_bool": "2", "caregiver_bool": "1", - "housing": "1", "income_source": "5", "felony_bool": "1", - "attending_school": "0", "currently_employed": "1", - "substance_use": "1", "time_unemployed": "1", - "need_mental_health_support_bool": "1" + "age": "23", + "gender": "1", + "work_experience": "1", + "canada_workex": "1", + "dep_num": "0", + "canada_born": "1", + "citizen_status": "2", + "level_of_schooling": "2", + "fluent_english": "3", + "reading_english_scale": "2", + "speaking_english_scale": "2", + "writing_english_scale": "3", + "numeracy_scale": "2", + "computer_scale": "3", + "transportation_bool": "2", + "caregiver_bool": "1", + "housing": "1", + "income_source": "5", + "felony_bool": "1", + "attending_school": "0", + "currently_employed": "1", + "substance_use": "1", + "time_unemployed": "1", + "need_mental_health_support_bool": "1", } results = interpret_and_calculate(test_data) print(results) diff --git a/app/clients/service/ml_models.py b/app/clients/service/ml_models.py index 4f75209c..50957926 100644 --- a/app/clients/service/ml_models.py +++ b/app/clients/service/ml_models.py @@ -1,8 +1,9 @@ from abc import ABC, abstractmethod from typing import List + import numpy as np -from sklearn.linear_model import LinearRegression from sklearn.ensemble import RandomForestRegressor +from sklearn.linear_model import LinearRegression from sklearn.svm import SVR @@ -19,12 +20,14 @@ def predict(self, X: np.ndarray) -> np.ndarray: def save(self, path: str): import pickle + with open(path, "wb") as f: pickle.dump(self, f) @staticmethod def load(path: str): import pickle + with open(path, "rb") as f: return pickle.load(f) @@ -50,8 +53,7 @@ def __str__(self): class RandomForestModel(InterfaceBaseMLModel): def __init__(self, n_estimators=100, random_state=42): - self.model = RandomForestRegressor(n_estimators=n_estimators, - random_state=random_state) + self.model = RandomForestRegressor(n_estimators=n_estimators, random_state=random_state) def fit(self, X, y): self.model.fit(X, y) @@ -115,7 +117,7 @@ def __init__(self): self._model_map = { "Linear Regression": LinearRegressionModel, "Random Forest Regressor": RandomForestModel, - "Support Vector Machine": SVMModel + "Support Vector Machine": SVMModel, } def list_models(self) -> List[InterfaceBaseMLModel]: @@ -133,15 +135,13 @@ def get_model_instance(self, model_name: str) -> InterfaceBaseMLModel: class MLModelManager(InterfaceMLModelManager): def __init__(self, repository: InterfaceMLModelRepository): self._repository = repository - self._current_model = repository.get_model_instance( - "Random Forest Regressor") + self._current_model = repository.get_model_instance("Random Forest Regressor") def get_current_model(self) -> str: return self._current_model def switch_model(self, model_name: str) -> bool: if self._repository.is_model_available(model_name): - self._current_model = self._repository.get_model_instance( - model_name) + self._current_model = self._repository.get_model_instance(model_name) return True return False diff --git a/app/clients/service/ml_models_router.py b/app/clients/service/ml_models_router.py index f9ffe659..d1070153 100644 --- a/app/clients/service/ml_models_router.py +++ b/app/clients/service/ml_models_router.py @@ -1,15 +1,19 @@ from fastapi import APIRouter, HTTPException -from app.clients.service.ml_models import MLModelRepository, MLModelManager + +from app.clients.service.ml_models import MLModelManager, MLModelRepository router = APIRouter(prefix="/ml_models") model_repository = MLModelRepository() model_manager = MLModelManager(model_repository) + + @router.get("/list") def list_models(): """List all available ML models""" # return {"models": model_repository.list_models()} return {"models": [str(model) for model in model_repository.list_models()]} + @router.post("/switch/{model_name}") def switch_models(model_name: str): """Switch between ML models""" @@ -23,4 +27,4 @@ def switch_models(model_name: str): def current_model(): """Get the current ML model""" # return {"current_model": model_manager.get_current_model()} - return {"current_model": str(model_manager.get_current_model())} \ No newline at end of file + return {"current_model": str(model_manager.get_current_model())} diff --git a/app/clients/service/model.py b/app/clients/service/model.py index 973c03d5..cba85686 100644 --- a/app/clients/service/model.py +++ b/app/clients/service/model.py @@ -4,35 +4,39 @@ Pass in model name via command line """ +import os + # Standard library imports import pickle +import sys # Third-party imports import numpy as np import pandas as pd -import sys -import os -from sklearn.model_selection import train_test_split -from sklearn.linear_model import LinearRegression -from sklearn.ensemble import RandomForestRegressor -from sklearn import svm # Local imports -from ml_models import InterfaceBaseMLModel, MLModelRepository, \ - LinearRegressionModel, RandomForestModel, SVMModel +from ml_models import ( + InterfaceBaseMLModel, + LinearRegressionModel, + MLModelRepository, + RandomForestModel, + SVMModel, +) +from sklearn import svm +from sklearn.ensemble import RandomForestRegressor +from sklearn.linear_model import LinearRegression +from sklearn.model_selection import train_test_split repo = MLModelRepository() default_unformatted_model_path = "pretrained_models" + os.sep + "model_{}.pkl" -def get_model_by_name(model_type: str, n_estimators=100, - random_state=42) -> InterfaceBaseMLModel: +def get_model_by_name(model_type: str, n_estimators=100, random_state=42) -> InterfaceBaseMLModel: model_map = { "Linear Regression": LinearRegressionModel, - "Random Forest Regressor": lambda: RandomForestModel(n_estimators, - random_state), - "Support Vector Machine": SVMModel + "Random Forest Regressor": lambda: RandomForestModel(n_estimators, random_state), + "Support Vector Machine": SVMModel, } if model_type not in model_map: @@ -50,68 +54,69 @@ def prepare_model_data(test_size=0.2, random_state=42): Args: test_size: The percent of the dataset to use as test data (rest will be used as train data) random_state: The random state to generate train/test split with - + Returns: RandomForestRegressor: Trained model for predicting success rates """ # Load dataset - data = pd.read_csv('data_commontool.csv') + data = pd.read_csv("data_commontool.csv") # Define feature columns feature_columns = [ - 'age', # Client's age - 'gender', # Client's gender (bool) - 'work_experience', # Years of work experience - 'canada_workex', # Years of work experience in Canada - 'dep_num', # Number of dependents - 'canada_born', # Born in Canada - 'citizen_status', # Citizenship status - 'level_of_schooling', # Highest level achieved (1-14) - 'fluent_english', # English fluency scale (1-10) - 'reading_english_scale', # Reading ability scale (1-10) - 'speaking_english_scale', # Speaking ability scale (1-10) - 'writing_english_scale', # Writing ability scale (1-10) - 'numeracy_scale', # Numeracy ability scale (1-10) - 'computer_scale', # Computer proficiency scale (1-10) - 'transportation_bool', # Needs transportation support (bool) - 'caregiver_bool', # Is primary caregiver (bool) - 'housing', # Housing situation (1-10) - 'income_source', # Source of income (1-10) - 'felony_bool', # Has a felony (bool) - 'attending_school', # Currently a student (bool) - 'currently_employed', # Currently employed (bool) - 'substance_use', # Substance use disorder (bool) - 'time_unemployed', # Years unemployed - 'need_mental_health_support_bool' # Needs mental health support (bool) + "age", # Client's age + "gender", # Client's gender (bool) + "work_experience", # Years of work experience + "canada_workex", # Years of work experience in Canada + "dep_num", # Number of dependents + "canada_born", # Born in Canada + "citizen_status", # Citizenship status + "level_of_schooling", # Highest level achieved (1-14) + "fluent_english", # English fluency scale (1-10) + "reading_english_scale", # Reading ability scale (1-10) + "speaking_english_scale", # Speaking ability scale (1-10) + "writing_english_scale", # Writing ability scale (1-10) + "numeracy_scale", # Numeracy ability scale (1-10) + "computer_scale", # Computer proficiency scale (1-10) + "transportation_bool", # Needs transportation support (bool) + "caregiver_bool", # Is primary caregiver (bool) + "housing", # Housing situation (1-10) + "income_source", # Source of income (1-10) + "felony_bool", # Has a felony (bool) + "attending_school", # Currently a student (bool) + "currently_employed", # Currently employed (bool) + "substance_use", # Substance use disorder (bool) + "time_unemployed", # Years unemployed + "need_mental_health_support_bool", # Needs mental health support (bool) ] # Define intervention columns intervention_columns = [ - 'employment_assistance', - 'life_stabilization', - 'retention_services', - 'specialized_services', - 'employment_related_financial_supports', - 'employer_financial_supports', - 'enhanced_referrals' + "employment_assistance", + "life_stabilization", + "retention_services", + "specialized_services", + "employment_related_financial_supports", + "employer_financial_supports", + "enhanced_referrals", ] # Combine all feature columns all_features = feature_columns + intervention_columns # Prepare training data features = np.array(data[all_features]) # Changed from X to features - targets = np.array(data['success_rate']) # Changed from y to targets + targets = np.array(data["success_rate"]) # Changed from y to targets # Split the dataset X_train, x_test, Y_train, y_test = train_test_split( # Removed unused variables features, targets, test_size=test_size, - random_state=random_state + random_state=random_state, ) return X_train, x_test, Y_train, y_test -def train_model(X_train, Y_train, model_type, n_estimators=100, - random_state=42) -> InterfaceBaseMLModel: +def train_model( + X_train, Y_train, model_type, n_estimators=100, random_state=42 +) -> InterfaceBaseMLModel: """ Trains the model Args: @@ -144,7 +149,7 @@ def get_true_file_name(model_type, filename): def save_model(model, model_type, filename=default_unformatted_model_path): """ Save the trained model to a file. - + Args: model: Trained model to save model_type: The type of model being saved @@ -158,11 +163,11 @@ def save_model(model, model_type, filename=default_unformatted_model_path): def load_model(model_type, filename=default_unformatted_model_path): """ Load a trained model from a file. - + Args: model_type: The type of model being loaded filename (str): Name of the file to load the model from - + Returns: The loaded model """ diff --git a/app/database.py b/app/database.py index 3a489f54..b5c8b948 100644 --- a/app/database.py +++ b/app/database.py @@ -7,22 +7,23 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker -#Here is where the database is located -SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db" +# Here is where the database is located +SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db" -#Open up a connection so that we are able to use the database +# Open up a connection so that we are able to use the database engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}) -#Bind the engine just created +# Bind the engine just created SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) -#Create an object of our database so as to control the database +# Create an object of our database so as to control the database Base = declarative_base() + def get_db(): """ Creates a database session and ensures it's closed after use. - + Yields: Session: SQLAlchemy database session """ diff --git a/app/main.py b/app/main.py index adf1af76..66746785 100644 --- a/app/main.py +++ b/app/main.py @@ -5,18 +5,21 @@ """ from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + from app import models -from app.database import engine -from app.clients.router import router as clients_router from app.auth.router import router as auth_router +from app.clients.router import router as clients_router from app.clients.service.ml_models_router import router as ml_models_router -from fastapi.middleware.cors import CORSMiddleware +from app.database import engine # Initialize database tables models.Base.metadata.create_all(bind=engine) # Create FastAPI application -app = FastAPI(title="Case Management API", description="API for managing client cases", version="1.0.0") +app = FastAPI( + title="Case Management API", description="API for managing client cases", version="1.0.0" +) # Include routers app.include_router(auth_router) @@ -26,8 +29,8 @@ # Configure CORS middleware app.add_middleware( CORSMiddleware, - allow_origins=["*"], # Allows all origins - allow_methods=["*"], # Allows all methods - allow_headers=["*"], # Allows all headers + allow_origins=["*"], # Allows all origins + allow_methods=["*"], # Allows all methods + allow_headers=["*"], # Allows all headers allow_credentials=True, ) diff --git a/app/models.py b/app/models.py index df778348..870210e5 100644 --- a/app/models.py +++ b/app/models.py @@ -3,11 +3,14 @@ Contains the Client model for storing client information in the database. """ -from app.database import Base -from sqlalchemy import Column, Integer, String, Boolean, ForeignKey, CheckConstraint, Enum -from sqlalchemy.orm import relationship import enum +from sqlalchemy import Boolean, CheckConstraint, Column, Enum, ForeignKey, Integer, String +from sqlalchemy.orm import relationship + +from app.database import Base + + class UserRole(str, enum.Enum): admin = "admin" case_worker = "case_worker" @@ -24,46 +27,61 @@ class User(Base): cases = relationship("ClientCase", back_populates="user") + class Client(Base): """ Client model representing client data in the database. """ + __tablename__ = "clients" id = Column(Integer, primary_key=True, autoincrement=True) - age = Column(Integer, CheckConstraint('age >= 18')) + age = Column(Integer, CheckConstraint("age >= 18")) gender = Column(Integer, CheckConstraint("gender = 1 OR gender = 2")) - work_experience = Column(Integer, CheckConstraint('work_experience >= 0')) - canada_workex = Column(Integer, CheckConstraint('canada_workex >= 0')) - dep_num = Column(Integer, CheckConstraint('dep_num >= 0')) + work_experience = Column(Integer, CheckConstraint("work_experience >= 0")) + canada_workex = Column(Integer, CheckConstraint("canada_workex >= 0")) + dep_num = Column(Integer, CheckConstraint("dep_num >= 0")) canada_born = Column(Boolean) citizen_status = Column(Boolean) - level_of_schooling = Column(Integer, CheckConstraint('level_of_schooling >= 1 AND level_of_schooling <= 14')) + level_of_schooling = Column( + Integer, CheckConstraint("level_of_schooling >= 1 AND level_of_schooling <= 14") + ) fluent_english = Column(Boolean) - reading_english_scale = Column(Integer, CheckConstraint('reading_english_scale >= 0 AND reading_english_scale <= 10')) - speaking_english_scale = Column(Integer, CheckConstraint('speaking_english_scale >= 0 AND speaking_english_scale <= 10')) - writing_english_scale = Column(Integer, CheckConstraint('writing_english_scale >= 0 AND writing_english_scale <= 10')) - numeracy_scale = Column(Integer, CheckConstraint('numeracy_scale >= 0 AND numeracy_scale <= 10')) - computer_scale = Column(Integer, CheckConstraint('computer_scale >= 0 AND computer_scale <= 10')) + reading_english_scale = Column( + Integer, CheckConstraint("reading_english_scale >= 0 AND reading_english_scale <= 10") + ) + speaking_english_scale = Column( + Integer, CheckConstraint("speaking_english_scale >= 0 AND speaking_english_scale <= 10") + ) + writing_english_scale = Column( + Integer, CheckConstraint("writing_english_scale >= 0 AND writing_english_scale <= 10") + ) + numeracy_scale = Column( + Integer, CheckConstraint("numeracy_scale >= 0 AND numeracy_scale <= 10") + ) + computer_scale = Column( + Integer, CheckConstraint("computer_scale >= 0 AND computer_scale <= 10") + ) transportation_bool = Column(Boolean) caregiver_bool = Column(Boolean) - housing = Column(Integer, CheckConstraint('housing >= 1 AND housing <= 10')) - income_source = Column(Integer, CheckConstraint('income_source >= 1 AND income_source <= 11')) + housing = Column(Integer, CheckConstraint("housing >= 1 AND housing <= 10")) + income_source = Column(Integer, CheckConstraint("income_source >= 1 AND income_source <= 11")) felony_bool = Column(Boolean) attending_school = Column(Boolean) currently_employed = Column(Boolean) substance_use = Column(Boolean) - time_unemployed = Column(Integer, CheckConstraint('time_unemployed >= 0')) + time_unemployed = Column(Integer, CheckConstraint("time_unemployed >= 0")) need_mental_health_support_bool = Column(Boolean) cases = relationship("ClientCase", back_populates="client") + class ClientCase(Base): __tablename__ = "client_cases" client_id = Column(Integer, ForeignKey("clients.id"), primary_key=True) user_id = Column(Integer, ForeignKey("users.id"), primary_key=True) - + employment_assistance = Column(Boolean) life_stabilization = Column(Boolean) retention_services = Column(Boolean) @@ -71,7 +89,7 @@ class ClientCase(Base): employment_related_financial_supports = Column(Boolean) employer_financial_supports = Column(Boolean) enhanced_referrals = Column(Boolean) - success_rate = Column(Integer, CheckConstraint('success_rate >= 0 AND success_rate <= 100')) + success_rate = Column(Integer, CheckConstraint("success_rate >= 0 AND success_rate <= 100")) client = relationship("Client", back_populates="cases") user = relationship("User", back_populates="cases") diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..6dd3a8a7 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,40 @@ +# Global options: +[mypy] +# Enable strict mode for better type checking +strict = false + +# Encourage but don't require type annotations +disallow_untyped_defs = false +# At least annotate return types +disallow_incomplete_defs = true +# Type check the body of functions without annotations +check_untyped_defs = true + +# Allow calling functions without type hints +disallow_untyped_calls = false + +# Be more permissive with 'Any' usage +disallow_any_unimported = false +disallow_any_explicit = false +disallow_any_generics = false +# Still warn about returning Any +warn_return_any = true + +# Don't enforce strict subclassing +disallow_subclassing_any = false + +# Still catch obvious issues +warn_redundant_casts = true +warn_unused_ignores = true +warn_no_return = false +warn_unreachable = true + +# Allow redefinitions in some contexts +allow_redefinition = true + +# Module import settings +ignore_missing_imports = true +follow_imports = silent + +# Performance improvements +cache_dir = .mypy_cache \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..0d0e2efa --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,18 @@ +# Black Configuration +[tool.black] +line-length = 100 +include = '\.pyi?$' +skip-magic-trailing-comma = true +target-version = ['py310'] + +# isort Configuration +[tool.isort] +profile = "black" +line_length = 100 +known_first_party = ["app"] +multi_line_output = 3 +force_grid_wrap = 0 +combine_as_imports = true +include_trailing_comma = true +force_single_line = false +skip = ["venv", ".venv", "migrations"] \ No newline at end of file From fc36fa89832a3d4fd839c1d415aa6885aa95c402 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 24 Mar 2025 17:43:51 -0700 Subject: [PATCH 07/18] Start on .toml file --- pyproject.toml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 0d0e2efa..3e7a4c41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,28 @@ +# Overall Project Configuration +[project] +name = "Common Assessment Tool" +version = "1.0.0" +authors = ["David Treadwell ", +"Steve Chen "] +readme = "README.md" +repository = "https://github.com/dtread4/CommonAssessmentTool" +license = "MIT" +dependencies = {file = ["requirements.txt"]} +requires-python = ">=3.10" + +# Build system +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +# Packages finder +[tool.setuptools.packages.find] +where = ["."] + +# Optional dependency configuration +[project.optional-dependencies] +dev = ["black", "isort"] + # Black Configuration [tool.black] line-length = 100 From 4251c688156a8849bf463975e5c2b5c941c6da8a Mon Sep 17 00:00:00 2001 From: David Date: Tue, 25 Mar 2025 08:46:36 -0700 Subject: [PATCH 08/18] Fixes to toml file --- pyproject.toml | 56 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3e7a4c41..755e2239 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,15 +1,60 @@ # Overall Project Configuration [project] -name = "Common Assessment Tool" +name = "Common_Assessment_Tool" version = "1.0.0" -authors = ["David Treadwell ", -"Steve Chen "] +authors = [{name = "David Treadwell", email = "treadwell.d@northeastern.edu"}, {name = "Fran Li", email = "li.fengr@northeastern.edu"}, {name = "Steve Chen", email = "chen.steve2@northeastern.edu"}] readme = "README.md" -repository = "https://github.com/dtread4/CommonAssessmentTool" license = "MIT" -dependencies = {file = ["requirements.txt"]} +dependencies = [ + 'annotated-types==0.7.0', + 'anyio==4.4.0', + 'certifi==2024.7.4', + 'click==8.1.7', + 'dnspython==2.6.1', + 'email_validator==2.2.0', + 'fastapi==0.112.2', + 'fastapi-cli==0.0.5', + 'h11==0.14.0', + 'httpcore==1.0.5', + 'httptools==0.6.1', + 'httpx==0.27.2', + 'idna==3.8', + 'Jinja2==3.1.4', + 'joblib==1.4.2', + 'markdown-it-py==3.0.0', + 'MarkupSafe==2.1.5', + 'mdurl==0.1.2', + 'numpy==2.1.0', + 'pandas==2.2.2', + 'pydantic==2.8.2', + 'pydantic_core==2.20.1', + 'Pygments==2.18.0', + 'python-dateutil==2.9.0.post0', + 'python-dotenv==1.0.1', + 'python-multipart==0.0.9', + 'pytz==2024.1', + 'PyYAML==6.0.2', + 'rich==13.8.0', + 'scikit-learn==1.5.1', + 'scipy==1.14.1', + 'shellingham==1.5.4', + 'six==1.16.0', + 'sniffio==1.3.1', + 'starlette==0.38.2', + 'threadpoolctl==3.5.0', + 'typer==0.12.5', + 'typing_extensions==4.12.2', + 'tzdata==2024.1', + 'uvicorn==0.30.6', + 'watchfiles==0.23.0', + 'websockets==13.0' +] requires-python = ">=3.10" +# Project urls +[project.urls] +Repository = "https://github.com/dtread4/CommonAssessmentTool" + # Build system [build-system] requires = ["setuptools"] @@ -22,6 +67,7 @@ where = ["."] # Optional dependency configuration [project.optional-dependencies] dev = ["black", "isort"] +extra = ['uvloop==0.20.0'] # Black Configuration [tool.black] From 6be82558f599af46c6dd852d5f80affa916561d8 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 25 Mar 2025 09:06:36 -0700 Subject: [PATCH 09/18] Update to use .env file --- README.md | 9 ++++++++- app/auth/router.py | 12 ++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b34d6d6b..69070348 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -Team TicTech +Team SuperSonics via TicTech Project -- Feature Development Backend: Create CRUD API's for Client @@ -22,6 +22,13 @@ This also has an API file to interact with the front end, and logic in order to -------------------------How to Use------------------------- 1. In the virtual environment you've created for this project, install all dependencies in requirements.txt (pip install -r requirements.txt) +2. Create a .env file with the following fields: +```markdown +SECRET_KEY = "your-secret-key-here" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 +``` + 2. Run the app (uvicorn app.main:app --reload) 3. Load data into database (python initialize_data.py) diff --git a/app/auth/router.py b/app/auth/router.py index 3f5455ee..edc48165 100644 --- a/app/auth/router.py +++ b/app/auth/router.py @@ -11,6 +11,9 @@ from app.database import get_db from app.models import User, UserRole +from dotenv import load_dotenv +import os + router = APIRouter(prefix="/auth", tags=["authentication"]) @@ -36,10 +39,11 @@ class Config: from_attributes = True -# Configuration -SECRET_KEY = "your-secret-key-here" -ALGORITHM = "HS256" -ACCESS_TOKEN_EXPIRE_MINUTES = 30 +# Load configuration from .env +load_dotenv() +SECRET_KEY = os.getenv("SECRET_KEY") +ALGORITHM = os.getenv("ALGORITHM") +ACCESS_TOKEN_EXPIRE_MINUTES = os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES") pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/token") From e8cf6e7aba2c66a527143140d32aae3e969be3c9 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 25 Mar 2025 09:06:54 -0700 Subject: [PATCH 10/18] Update README --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 69070348..9f3af03f 100644 --- a/README.md +++ b/README.md @@ -29,15 +29,15 @@ ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 30 ``` -2. Run the app (uvicorn app.main:app --reload) +3. Run the app (uvicorn app.main:app --reload) -3. Load data into database (python initialize_data.py) +4. Load data into database (python initialize_data.py) -4. Go to SwaggerUI (http://127.0.0.1:8000/docs) +5. Go to SwaggerUI (http://127.0.0.1:8000/docs) -4. Log in as admin (username: admin password: admin123) +6. Log in as admin (username: admin password: admin123) -5. Click on each endpoint to use +7. Click on each endpoint to use -Create User (Only users in admin role can create new users. The role field needs to be either "admin" or "case_worker") -Get clients (Display all the clients that are in the database) From 223da652b55e68dbfb5e099d309e1cb6ccdbc926 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 25 Mar 2025 09:18:35 -0700 Subject: [PATCH 11/18] Add .env file per requirements --- .env | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .env diff --git a/.env b/.env new file mode 100644 index 00000000..f292cb8d --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +SECRET_KEY = "your-secret-key-here" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 \ No newline at end of file From 9e1bd774feab4d03dbc8763c3bc745bcfd8c7fc8 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 28 Mar 2025 09:44:18 -0700 Subject: [PATCH 12/18] Create Docker compose file --- docker-compose.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..e69de29b From 862d44542776a8265895f46e94cfce3e64cbedc7 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 28 Mar 2025 09:44:57 -0700 Subject: [PATCH 13/18] Bug fix with .env loading --- app/auth/router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/auth/router.py b/app/auth/router.py index edc48165..50e07890 100644 --- a/app/auth/router.py +++ b/app/auth/router.py @@ -117,7 +117,7 @@ async def login_for_access_token( detail="Incorrect username or password", headers={"WWW-Authenticate": "Bearer"}, ) - access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token_expires = timedelta(minutes=int(ACCESS_TOKEN_EXPIRE_MINUTES)) access_token = create_access_token( data={"sub": user.username}, expires_delta=access_token_expires ) From 0ab65c9cc270d2b1bba84d6d59eef66dd8f475ed Mon Sep 17 00:00:00 2001 From: David Date: Fri, 28 Mar 2025 10:55:53 -0700 Subject: [PATCH 14/18] Update requirements and toml file --- app/requirements.txt | 1 - pyproject.toml | 49 +++++--------------------------------------- requirements.txt | 2 -- 3 files changed, 5 insertions(+), 47 deletions(-) diff --git a/app/requirements.txt b/app/requirements.txt index 57fc8e6e..ddcf118a 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -38,6 +38,5 @@ typer==0.12.5 typing_extensions==4.12.2 tzdata==2024.1 uvicorn==0.30.6 -uvloop==0.20.0 watchfiles==0.23.0 websockets==13.0 diff --git a/pyproject.toml b/pyproject.toml index 755e2239..820b4f6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,52 +5,13 @@ version = "1.0.0" authors = [{name = "David Treadwell", email = "treadwell.d@northeastern.edu"}, {name = "Fran Li", email = "li.fengr@northeastern.edu"}, {name = "Steve Chen", email = "chen.steve2@northeastern.edu"}] readme = "README.md" license = "MIT" -dependencies = [ - 'annotated-types==0.7.0', - 'anyio==4.4.0', - 'certifi==2024.7.4', - 'click==8.1.7', - 'dnspython==2.6.1', - 'email_validator==2.2.0', - 'fastapi==0.112.2', - 'fastapi-cli==0.0.5', - 'h11==0.14.0', - 'httpcore==1.0.5', - 'httptools==0.6.1', - 'httpx==0.27.2', - 'idna==3.8', - 'Jinja2==3.1.4', - 'joblib==1.4.2', - 'markdown-it-py==3.0.0', - 'MarkupSafe==2.1.5', - 'mdurl==0.1.2', - 'numpy==2.1.0', - 'pandas==2.2.2', - 'pydantic==2.8.2', - 'pydantic_core==2.20.1', - 'Pygments==2.18.0', - 'python-dateutil==2.9.0.post0', - 'python-dotenv==1.0.1', - 'python-multipart==0.0.9', - 'pytz==2024.1', - 'PyYAML==6.0.2', - 'rich==13.8.0', - 'scikit-learn==1.5.1', - 'scipy==1.14.1', - 'shellingham==1.5.4', - 'six==1.16.0', - 'sniffio==1.3.1', - 'starlette==0.38.2', - 'threadpoolctl==3.5.0', - 'typer==0.12.5', - 'typing_extensions==4.12.2', - 'tzdata==2024.1', - 'uvicorn==0.30.6', - 'watchfiles==0.23.0', - 'websockets==13.0' -] +dynamic = ["dependencies"] requires-python = ">=3.10" +# Set up dependencies from requirements.txt file +[tool.setuptools.dynamic] +dependencies = {file = ["requirements.txt"]} + # Project urls [project.urls] Repository = "https://github.com/dtread4/CommonAssessmentTool" diff --git a/requirements.txt b/requirements.txt index c5910a3b..d098b41c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -133,7 +133,6 @@ tzdata==2023.3 uri-template==1.2.0 urllib3==2.0.7 uvicorn==0.23.2 -uvloop==0.17.0 watchfiles==0.20.0 wcwidth==0.2.8 webcolors==1.13 @@ -141,4 +140,3 @@ webencodings==0.5.1 websocket-client==1.5.1 websockets==11.0.3 widgetsnbextension==4.0.7 -mypy=1.15.1 From 4a902b926ace0c326c81a47ef50fb99624c9a5ee Mon Sep 17 00:00:00 2001 From: David Date: Fri, 28 Mar 2025 10:57:31 -0700 Subject: [PATCH 15/18] Add Docker compose file --- docker-compose.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index e69de29b..d3b893b6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -0,0 +1,11 @@ +services: + web: + build: . + command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] + ports: + - "8000:8000" + develop: + watch: + - action: sync + path: . + target: /code \ No newline at end of file From 3d751165b7c231b5da3c6e52c8be8f62bd424dbb Mon Sep 17 00:00:00 2001 From: David Date: Fri, 28 Mar 2025 11:02:50 -0700 Subject: [PATCH 16/18] Update README to include Docker instructions --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 9f3af03f..791b889c 100644 --- a/README.md +++ b/README.md @@ -62,3 +62,17 @@ ACCESS_TOKEN_EXPIRE_MINUTES = 30 -Create case assignment (Allow authorized users to create a new case assignment.) +## Docker Instructions +1. Follow installation guide from Docker: https://www.docker.com/blog/how-to-dockerize-your-python-applications/ +2. WINDOWS-SPECIFIC: Ensure virtualization is enabled in your system BIOS, or Docker cannot run +3. Open the Docker Desktop application +4. In a command prompt, navigate to the CommonAssessmentTool repo's directory on your machine (assumes you already cloned from GitHub) and run the command below (make sure the period at the end is included!): +``` +docker build -t common_assessment_tool . +``` +5. Now run docker run --rm -p 8000:8000 common_assessment_tool +6. Follow the steps to run the Swagger UI as described above (clicking link in step 5 should take you to the UI) +7. To run using Docker-Compose, run the command below in the CommonAssessmentTool repo's directory +``` +docker compose up +``` \ No newline at end of file From 74b1e0018184135b117c3278484cf972c88b1900 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 28 Mar 2025 11:23:08 -0700 Subject: [PATCH 17/18] Better README --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 791b889c..3e831058 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,10 @@ ACCESS_TOKEN_EXPIRE_MINUTES = 30 ``` docker build -t common_assessment_tool . ``` -5. Now run docker run --rm -p 8000:8000 common_assessment_tool +5. Now run with the following Docker command: +``` +docker run --rm -p 8000:8000 common_assessment_tool +``` 6. Follow the steps to run the Swagger UI as described above (clicking link in step 5 should take you to the UI) 7. To run using Docker-Compose, run the command below in the CommonAssessmentTool repo's directory ``` From b4b9ef7938c7910763f7429d628f893bcb1f2f19 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 31 Mar 2025 11:56:00 -0700 Subject: [PATCH 18/18] Update README to prevent possible DB error --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3e831058..70b0c190 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,9 @@ ACCESS_TOKEN_EXPIRE_MINUTES = 30 3. Run the app (uvicorn app.main:app --reload) -4. Load data into database (python initialize_data.py) +4. Go to SwaggerUI (http://127.0.0.1:8000/docs) -5. Go to SwaggerUI (http://127.0.0.1:8000/docs) +5. Load data into database (python initialize_data.py) (if receiving an error, make sure the app is running and open, then try again) 6. Log in as admin (username: admin password: admin123)