From 340aa7e3ff613192cc6c9d1140138edae1e51d27 Mon Sep 17 00:00:00 2001 From: Steve Chen Date: Wed, 19 Mar 2025 20:34:52 -0700 Subject: [PATCH 01/11] 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/11] 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 zcmcKD-H)WlRR-|h_1F4?!~_QtF!A_}6V@?V659~RS!c_;PV9L7v9b9GpUv!UuV?V? z%rZ0U#7IQLMG{+TBv2ah@fQ$r!wnZmNH$z?!%eQZL6H0bU@o{pNTFt`e$|=NQ|_?@ zBh@_bdCqfAol{kBPxnmk_U_Gp@;5u9{jcdg+pk`otZr?b+J5cYsqv+&tJ~X??b-CE zmw)-C*|%p8{=w|`XD6q(zB<|3m|V^D*2OEc>8+P1uWU^wv-3AiZ+T_)+O^f$^p@>Q zt5+u(FKte)d}{V|$vzrQZ`#?sI(z!eg^SOB_dmb$KhtC9vhhxx`}oF7#d%V6eo}IsMdh58h_`!`E*RH>Q>eA-cq@b zPPlgO^tO%5W#(_s&X2w^J9q5jtmHp-e)Ai%wU11XuRl7Ub8T3h@y)Y;cdTr|9vsi( zraW%WJoXz809#7@* zg*?8P$KF|eCXX-W@oXOF^LQ?g=ks_Wj~DZ}kjIzvcqxyU^Z3O)ekqS%$>S?|{AwP* zmdCH>@f&&kW*)zl$2gDQ&SN!?i+Nni<8mI8Jg(&NN*-%@tmpANd0froS{@sDZ02!2 zkKfH>E066w_U_sndCc;;ncnT7yf(SCv$?gM56pvW5AB_awNJJaasIw<&eraK^Xwb1 z=CJ**FQkI+>@7?{L~$_sk7}PzRasFTFz%E2BGOxr){MN@hh=<}JKGw;$ zPIQp}i1x0&#iH zp}P5@`PuZNn$P~>^+#W3&9Bv}KKkh(z5Mtd()awIuk-f&=8*?}yvDJ`tKYukdwHV2 z*%DX8$8(Mk@`dMn(f`F%nGW{y+V7UH#@j!B``)cL&RY}q=9MSm2ctjQOLV*O_4klU27SmU5A-~hO|L(X3;K`eUR>+9 z&TGDi^u2xP=_Aj!-|jzkK>g6ecz@HYPv82DdFaGJ`5|3I{)o$T)!$y1(5rJuHDg77 zs4q6X{kG5g#nad9?eRxE9!CAL{f>HhApi5JG8L^mbPniY?3;cFdBjJ3owfZO6OWH* zUiqRw+D#uF#eVxjDi?I-SLfkj^Pm6x^y2OFkcy(WPxRvDQ%^*B>0s2cd;TX<@z6P+ z5HG%Ajbqd6&$(8o{^;e!v-uUM)EAx} zuaC&nH%#@aqB{7at#3T+>3jaqg!$;8dE|vdjbqd6cU1j!y>pDGldp&7>G_Y!`}wU- zj~n{!&0}B9Lm&IF-)%i=fV}+Ta~me2eUAMu>$p z7ubm##n++qLn<@3xN57nc<0aW#aWjYM;I{rqaEb!^gp4ro2(hx%aC+c&oR zHtN-(UwVARI1fAO*h~Do@pY&=Vay|6M0&h_oMSwcm*0BfBeu8{J%`&(f4F-6Gk2W3#f4jf&?lXuN z51mgoy*}CMjCy&aUOxVa^3lPlV|V;-zm$rH&N+(t`QZ@an!fV<`j3x$^4MqkQTU>R z>I<@F(!u7a7nR@j_wP|E4tXJ79%!G~^!glXKK&H4(d3!z)_0b>akM=Ts&mZ64Vt?EHm_#1LA@KB2pRnBr z2h%L>4#JpeX7$s z&^L_w<|r5HL7O-7{5?MSlb>Vib3S1=Pm-x1y}DYTRTuYD-l#j6b^f{yFOD7YsDAgi zzU>pl%Ln{p^l^_k60JitaOLzvO}XWYZ5d4_^ENKdN6< zuYYtGtG?y$3g6wKZ+YB@uopd~gC2VO7S{zY%6!ne_56`%LwO(`;^E7wdMc{leNHDH z>WfY9eDmwKmq)+!@`S_!4-G)>m>DcteoBu6Y(BbJKT3SREgm|b zY;h~8FwU*VD?B|E2P4mh?jtsg@pu^hUVrOPeK5v}*AJV1NF{>p97MgmQIGGTd_DhB z`KMAtkQ@<)59dVNFtL=UY89Mv!Wh@+aH4n`fjzK2vI zpmi$j{JV95bI;*d*6(od(b?0fX!Sc(8{&&<{+%DgcHip*Z$0Sc<%f7Wh=-A9Lpq4B z*dEuMANvCF;(JIxq>|ab;GKsUAN{qi_WL(_e5?mg2k{UOg z_URmmk4RrN~ZTiSpd-rUrii#@%#Z^@UeIdlz4}Ic?L|IVY zp8ueFZpklx?s;!l{{Ah`kV-tn7k2(VzR!a`o?GjKmqA{B=K{YBW1eOD;BWulA8$RO z^JgAtpV;(`PsKA-jC~jH^8Z(X4yqGIo*gmj{n{QY^$ zn~I_n*IqZbd_k{I=U}M*rmq)qo%nn6sxSJZEe{O-_WHZ=sVJi@9y-75s2@`C*hlL? z->~Xhe08uP{j$EJUK}3&LYP-xNGBfhN85VXNB6aTrU!~b_xC6jM!gWP zPI$8F+4OAvS_k#=n;##~10AG~^WfnqlUPy(YQwP)!KeSJ5dc6IOJiRik6O8_7_s*YuQQx!U&oOn{ck{># zopUz5brNSCdiO6sy?pA8*vk`j>|VadQuTCDAM!x^$yWDJ^Qcc>bg<6aexGFBA|LhU zp@Vb~5AiU*F3}GZw}9y&LvNkb=lnWHqRcOj%?~3VZ8~^VzjLlmbwK$d+DAP7kP2gd zI(+fYzvJn6#7A_F=wQ^bdwzch)^|kvCr=N>(X&@lnK{O(r}a^FczSljs*C<;H+{Vb z{`NjibuM6t6R%#jIJ|SMZ#+F5Qkei2YySE>+U^@Xl<%mvpWot!R3f2%_@Tbo^!jw} z>G1SWe!4RI;`gXuR2*cQ{vge2??_+_Jcj75@8+0^Xt3W1QjD6?I3SI zA0p2dAD^cVsVHEqOPoH~^!8O;EFQszDKU5p4pC88O zsZq~%eyxXiX#MC%;ft=(sa+jTj=0^f#m;jCF?ag?~>(FTOnwsrmFTkNWg2 zKKj|Pci+iJ7cu(d|0Am2_VXwvsCKNg=~EF574@sX;+=nQd8(c#*2#u+F!D?6_V@3R zc>~6H^9*!|YU`6h&AN|I7dGyDoA5vlTVLf_z_pQ|ikART*@%GCm21wCGT^|yZ?DxdX-LEoMioJ_@I9{Qp3`q4k0 zzC_Reohd)Qhw{ZdJ$;XVDpgNK`xEy&#z#NfzFJ3h4mE$wV|}7uUr-zyF5`QC&(GBv zQ6J`kbkSD7e)Jvls7D?sU&IA{C8CZ^&yMKtV|ew#W$`gi{mu=){@L<;qHRBV{;2QR?f0qnNxka9n+L|nb9(XO z^dl}Jy?Av-6en-=N4x2xquTL(J?gYi!sLOmZ+iP`UxrjN=u150r!VUMcX|4Xqj|g@ zvyS?Py?H!-n}uFKZ0j&2%7cif=anbgk&pf(+Ph~{QOf7sL3tv^zUlGSYe+Q`RuhPW zc=fQMa}oW~riamgIu#FI-cjY%!w(}*Kh!w;7yB1qW?lZgCF;$yz}JL5-h9=+@c&`! zXGm39Q9gJw@I$pD(mU^LXr0B4!WUhmQ@L8;tDwh==YJ~X!Na(|c>BqQ`r{vJe&?Sq z)?uE=)5EA+s;>g^Fn(T5%bR2@NDn=Jv&9WH4!=_E$n%S9I6C;%bd`4B98}G%sGobv zRrR-jFG#OWeL461(D`K3i;r>60lj*LRIbG1Vbu55r>p0vD4+dis~3i_xtZpddsp<% zvwgwq`(V^Oj$a*7$Byg0OyBXhe<#T=FT|U-VQN?V7xe094|H%}J^y=%^`Wos262A6 z$P4K4bN~LQcil1G7}pQ*I^=V^j^_{YdgRS>C-y!BY;Tm1%Qsf9j`u!c@34>h${+t$ zKI-dhH?^tWDp&7x@re(7?1Asy`)_Beu7|z&o*pl*XP>$HgYSIw!V`bQzYOJD!0*of z^soN=5C2I#KRY7-Ti<{2zrOwtZ+xG>@tt*xI(9^Pdi-1e{`fzC{jcx)`<_4Y(SED5 zUq|KN`s4HW|H~r}{VP2@^B((-apGamA9?mNJQM5teEvAU@e}_MtV}R z?$h;+{ArVyCfD~q!*Ssw`HtmV`~G}uKX}jG=Z^p7^Jzci*74q_TRwS+eDCA@vb;P3 z6xrFXZ0#$19}IZsmGzzdPq)00KNIl$XnOlNe=cSD1kA(BIm%~7p1b+Ey!fTR?X}hG zlkwgxv-4lw`-H*io%!Q3-)qlzj(_;^?3w(*mism4oX<_~9@oz$9p}&S++NS03Oc>z 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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