diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 00000000..6ad2a849 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,39 @@ +name: CD Pipeline + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + cd: + runs-on: ubuntu-latest + + steps: + # Checkout the code + - name: Checkout Code + uses: actions/checkout@v3 + + # Build Docker Image + - name: Build Docker Image + run: docker build -t my-api . + + # Run the Docker Container + - name: Run Docker Container + run: docker run -d --name my-container -p 8080:80 my-api + + # Test the Running Container + - name: Test API Endpoints + run: | + sleep 10 # Allow some time for the container to start + curl --fail http://0.0.0.0:8080/docs || exit 1 + echo "API is accessible and working!" + + # Stop and Remove the Container + - name: Cleanup + run: | + docker stop my-container + docker rm my-container diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..373d8788 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,51 @@ +name: CI Pipeline + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + ci: + runs-on: ubuntu-latest + + steps: + # Checkout the code + - name: Checkout Code + uses: actions/checkout@v4 + + # Set up Python + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.9' + + # Install dependencies + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pylint pytest + + # Run linter (Pylint) + - name: Run Pylint + run: | + echo "Running Pylint..." + pylint **/*.py + + # Run tests + # - name: Run Tests + # run: | + # echo "Running Tests..." + # pytest tests/ + + # Logs for debugging + - name: Debugging Logs + run: | + echo "Installed Python Version:" + python --version + echo "Installed Packages:" + pip list diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..58351e62 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.10 +# +WORKDIR /code +# +COPY ./requirements.txt /code/requirements.txt +# +RUN pip install --no-cache-dir -r /code/requirements.txt +# +COPY . /code +# +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] diff --git a/README.md b/README.md index 0a48dc37..bb1665c1 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,433 @@ This will contain the model used for the project that based on the input informa The model works off of dummy data of several combinations of clients alongside the interventions chosen for them as well as their success rate at finding a job afterward. The model will be updated by the case workers by inputing new data for clients with their updated outcome information, and it can be updated on a daily, weekly, or monthly basis. This also has an API file to interact with the front end, and logic in order to process the interventions coming from the front end. This includes functions to clean data, create a matrix of all possible combinations in order to get the ones with the highest increase of success, and output the results in a way the front end can interact with. + +# Documentation + +## POST `/clients` + +### Description +Creates a new client in the database with the provided information. + +--- + +### Request Body + +The fields for the new client must be passed as a JSON object and are encapsulated in the `ClientUpdateModel` located in `update_model.py`. All fields are optional. + +| Field | Type | Example | Description | +|--------------------------------|---------|------------------------|-----------------------------------------| +| `age` | integer | `32` | Age of the client. | +| `gender` | string | `"Male"` | Gender of the client. | +| `work_experience` | integer | `7` | Years of total work experience. | +| `canada_workex` | integer | `3` | Years of work experience in Canada. | +| `dep_num` | integer | `1` | Number of dependents. | +| `canada_born` | string | `"No"` | Whether the client was born in Canada. | +| `citizen_status` | string | `"Citizen"` | Citizenship status of the client. | +| `level_of_schooling` | string | `"Master's degree"` | Highest level of education completed. | +| `fluent_english` | string | `"Yes"` | Whether the client is fluent in English. | +| `reading_english_scale` | integer | `10` | English reading skill level (1-10). | +| `speaking_english_scale` | integer | `10` | English speaking skill level (1-10). | +| `writing_english_scale` | integer | `10` | English writing skill level (1-10). | +| `numeracy_scale` | integer | `10` | Numeracy skill level (1-10). | +| `computer_scale` | integer | `10` | Computer skill level (1-10). | +| `transportation_bool` | string | `"No"` | Whether transportation support is needed. | +| `caregiver_bool` | string | `"No"` | Whether the client is a primary caregiver. | +| `housing` | string | `"Homeowner"` | Housing status of the client. | +| `income_source` | string | `"Employment"` | Source of income. | +| `felony_bool` | string | `"No"` | Whether the client has a felony record. | +| `attending_school` | string | `"No"` | Whether the client is currently a student. | +| `currently_employed` | string | `"Yes"` | Employment status of the client. | +| `substance_use` | string | `"No"` | Whether the client has a substance use disorder. | +| `time_unemployed` | integer | `0` | Time unemployed in years. | +| `need_mental_health_support_bool` | string | `"No"` | Whether the client needs mental health support. | +| `employment_assistance` | integer | `1` | Employment assistance score (1-10). | +| `life_stabilization` | integer | `1` | Life stabilization score (1-10). | +| `retention_services` | integer | `1` | Retention services score (1-10). | +| `specialized_services` | integer | `1` | Specialized services score (1-10). | +| `employment_related_financial_supports` | integer | `1` | Employment-related financial support score (1-10). | +| `employer_financial_supports` | integer | `1` | Employer financial support score (1-10). | +| `enhanced_referrals` | integer | `1` | Enhanced referrals score (1-10). | +| `success_rate` | integer | `100` | Client's success rate (percentage). | + +--- + +### Request Example + +**URL:** + +POST /clients + +**Request Body:** + +```json +{ + "age": 30, + "gender": "Male", + "work_experience": 5, + "canada_workex": 3, + "level_of_schooling": "Bachelor's", + "fluent_english": "Yes", + "currently_employed": "Yes" +} +``` + +**Responses** +**200 Created** + +The client was successfully created. + +**Response Example:** +```json +{ + "success": true, + "message": "Client successfully created with ID 101", + "client_id": "101" +} +``` + +**400 Bad Request** + +The input data is invalid or improperly formatted. + +**Response Example:** +```json +{ + "detail": "Invalid input data." +} +``` + +**500 Internal Server Error** + +An unexpected error occurred during client creation. + +**Response Example:** +```json +{ + "detail": "An error message describing the issue." +} +``` + +Ensure all required fields are included and properly formatted in the request body. Missing or invalid fields will result in a 400 Bad Request response. + +### **PUT /clients/{client_id}** + +#### **Description** +Updates specific fields of an existing client record. + +--- + +### **Path Parameters** + +| Parameter | Type | Description | +|-------------|----------|----------------------------------------| +| `client_id` | `string` | The unique ID of the client to update. | + +--- + +### **Request Body** + +The fields to be updated must be passed as a JSON object and are encapsulated in the `ClientUpdateModel` located in `update_model.py`. Unspecified fields will remain unchanged. + +| Field | Type | Example | Description | +|--------------------------------|---------|------------------------|-----------------------------------------| +| `age` | integer | `32` | Age of the client. | +| `gender` | string | `"Male"` | Gender of the client. | +| `work_experience` | integer | `7` | Years of total work experience. | +| `canada_workex` | integer | `3` | Years of work experience in Canada. | +| `dep_num` | integer | `1` | Number of dependents. | +| `canada_born` | string | `"No"` | Whether the client was born in Canada. | +| `citizen_status` | string | `"Citizen"` | Citizenship status of the client. | +| `level_of_schooling` | string | `"Master's degree"` | Highest level of education completed. | +| `fluent_english` | string | `"Yes"` | Whether the client is fluent in English. | +| `reading_english_scale` | integer | `10` | English reading skill level (1-10). | +| `speaking_english_scale` | integer | `10` | English speaking skill level (1-10). | +| `writing_english_scale` | integer | `10` | English writing skill level (1-10). | +| `numeracy_scale` | integer | `10` | Numeracy skill level (1-10). | +| `computer_scale` | integer | `10` | Computer skill level (1-10). | +| `transportation_bool` | string | `"No"` | Whether transportation support is needed. | +| `caregiver_bool` | string | `"No"` | Whether the client is a primary caregiver. | +| `housing` | string | `"Homeowner"` | Housing status of the client. | +| `income_source` | string | `"Employment"` | Source of income. | +| `felony_bool` | string | `"No"` | Whether the client has a felony record. | +| `attending_school` | string | `"No"` | Whether the client is currently a student. | +| `currently_employed` | string | `"Yes"` | Employment status of the client. | +| `substance_use` | string | `"No"` | Whether the client has a substance use disorder. | +| `time_unemployed` | integer | `0` | Time unemployed in years. | +| `need_mental_health_support_bool` | string | `"No"` | Whether the client needs mental health support. | +| `employment_assistance` | integer | `1` | Employment assistance score (1-10). | +| `life_stabilization` | integer | `1` | Life stabilization score (1-10). | +| `retention_services` | integer | `1` | Retention services score (1-10). | +| `specialized_services` | integer | `1` | Specialized services score (1-10). | +| `employment_related_financial_supports` | integer | `1` | Employment-related financial support score (1-10). | +| `employer_financial_supports` | integer | `1` | Employer financial support score (1-10). | +| `enhanced_referrals` | integer | `1` | Enhanced referrals score (1-10). | +| `success_rate` | integer | `100` | Client's success rate (percentage). | + +--- + +### **Request Example** + +**URL**: +PUT /clients/61 + + +**Request Body**: +```json +{ + "age": 32, + "work_experience": 7, + "canada_workex": 3, + "level_of_schooling": "Master's degree", + "fluent_english": "Yes", + "currently_employed": "Yes", + "housing": "Homeowner" +} +``` + +**Responses** +**200 OK** + +The client information was successfully updated. + +**Response Example:** +```json +{ + "success": true, + "message": "Client 61 successfully updated", + "client_id": "61" +} +``` +**404 Not Found** + +No client with the specified client_id exists. + +**Response Example:** +```json +{ + "detail": "Client with ID 61 not found." +} +``` +**400 Bad Request** + +The client_id is invalid or improperly formatted. + +**Response Example:** +```json +{ + "detail": "Invalid client_id format." +} +``` +**500 Internal Server Error** + +An unexpected error occurred on the server. + +**Response Example:** +```json +{ + "detail": "An error message describing the issue." +} +``` + +## DELETE `/clients/{client_id}` + +### Description +Deletes a client with the specified `client_id` from the database. + +### Path Parameters + +| Parameter | Type | Description | +|-------------|----------|----------------------------------------| +| `client_id` | `string` | The unique ID of the client to delete. | + +### Request Example + +**URL:** + +``` +DELETE /clients/1 +``` + +``` + +**404 Not Found** + +No client with the specified `client_id` exists. + +**Response Example:** +```json +{ + "detail": "Client with ID 1 not found." +} +``` + +**500 Internal Server Error** + +An unexpected error occurred on the server. + +**Response Example:** +```json +{ + "detail": "An error message describing the issue." +} +``` + +### Notes + +This endpoint requires the `client_id` to be a valid **integer string**. + +## GET `/clients/{client_id}` + +### Description +Gets a client with the specified `client_id` from the database. + +### Path Parameters + +| Parameter | Type | Description | +|-------------|----------|----------------------------------------| +| `client_id` | `string` | The unique ID of the client to get. | + +### Request Example + +**URL:** + +``` +GET /clients/1 +``` + +``` + +**404 Not Found** + +No client with the specified `client_id` exists. + +**Response Example:** +```json +{ + "detail": "Client with ID 1 not found." +} +``` + +**500 Internal Server Error** + +An unexpected error occurred on the server. + +**Response Example:** +```json +{ + "detail": "An error message describing the issue." +} +``` + +### Notes + +This endpoint requires the `client_id` to be a valid **integer string**. + + +## POST `/search` + +### Description +Searches for a list of clients based on the provided JSON criteria. Returns all clients that match the specified conditions. +The criteria are encapsulated in the `ClientUpdateModel` defined in `update_model.py`. +Any combination of fields can be provided in the JSON request body for filtering clients. Fields left empty will not be used as filters. + +### Request Body + +| Field | Type | Example | Description | +|--------------------------------|---------|------------------------|-----------------------------------------| +| `age` | integer | `32` | Age of the client. | +| `gender` | string | `"Male"` | Gender of the client. | +| `work_experience` | integer | `7` | Years of total work experience. | +| `canada_workex` | integer | `3` | Years of work experience in Canada. | +| `dep_num` | integer | `1` | Number of dependents. | +| `canada_born` | string | `"No"` | Whether the client was born in Canada. | +| `citizen_status` | string | `"Citizen"` | Citizenship status of the client. | +| `level_of_schooling` | string | `"Master's degree"` | Highest level of education completed. | +| `fluent_english` | string | `"Yes"` | Whether the client is fluent in English. | +| `reading_english_scale` | integer | `10` | English reading skill level (1-10). | +| `speaking_english_scale` | integer | `10` | English speaking skill level (1-10). | +| `writing_english_scale` | integer | `10` | English writing skill level (1-10). | +| `numeracy_scale` | integer | `10` | Numeracy skill level (1-10). | +| `computer_scale` | integer | `10` | Computer skill level (1-10). | +| `transportation_bool` | string | `"No"` | Whether transportation support is needed. | +| `caregiver_bool` | string | `"No"` | Whether the client is a primary caregiver. | +| `housing` | string | `"Homeowner"` | Housing status of the client. | +| `income_source` | string | `"Employment"` | Source of income. | +| `felony_bool` | string | `"No"` | Whether the client has a felony record. | +| `attending_school` | string | `"No"` | Whether the client is currently a student. | +| `currently_employed` | string | `"Yes"` | Employment status of the client. | +| `substance_use` | string | `"No"` | Whether the client has a substance use disorder. | +| `time_unemployed` | integer | `0` | Time unemployed in years. | +| `need_mental_health_support_bool` | string | `"No"` | Whether the client needs mental health support. | +| `employment_assistance` | integer | `1` | Employment assistance score (1-10). | +| `life_stabilization` | integer | `1` | Life stabilization score (1-10). | +| `retention_services` | integer | `1` | Retention services score (1-10). | +| `specialized_services` | integer | `1` | Specialized services score (1-10). | +| `employment_related_financial_supports` | integer | `1` | Employment-related financial support score (1-10). | +| `employer_financial_supports` | integer | `1` | Employer financial support score (1-10). | +| `enhanced_referrals` | integer | `1` | Enhanced referrals score (1-10). | +| `success_rate` | integer | `100` | Client's success rate (percentage). | + + + +### Request Example + +**URL**: + +POST /search + + +**Request Body**: +```json +{ + "age": 30, + "housing": "None" +} + +**Responses** +**200 OK** + +The list of clients matching the criteria was successfully retrieved. + +**Response Example:** + +```json +[ + { + "client_id": "1", + "age": 30, + "housing": "None", + "work_experience": 5, + "success_rate": 80 + }, + { + "client_id": "2", + "age": 30, + "housing": "None", + "work_experience": 6, + "success_rate": 85 + } +] + +``` +**404 Not Found** + +No clients matched the given criteria. + +**Response Example:** +```json +{ + "detail": "No clients found matching the criteria." +} +``` + +**500 Internal Server Error** +An unexpected error occurred on the server. + +**Response Example:** +```json +{ + "detail": "An error message describing the issue." +} +``` + diff --git a/app/clients/mapper.py b/app/clients/mapper.py new file mode 100644 index 00000000..ac6dcf1e --- /dev/null +++ b/app/clients/mapper.py @@ -0,0 +1,135 @@ +import os +import sqlite3 +from typing import Optional +from app.clients.service.update_model import ClientUpdateModel + +DATABASE = os.path.join(os.path.dirname(__file__), '../../mydatabase.db') + +def get_client(client_id: str) -> Optional[tuple]: + """ + Retrieve a client record from the database by client_id. + """ + try: + conn = sqlite3.connect(DATABASE) + cursor = conn.cursor() + cursor.execute("SELECT * FROM CommonAssessmentTool_Table WHERE client_id = ?", (client_id,)) + return cursor.fetchone() + except sqlite3.Error as e: + raise Exception(f"Database error occurred: {str(e)}") from e + finally: + conn.close() + +def get_all_clients_with_info(criteria): + """ + Search for clients based on given criteria. + + :param criteria: A dictionary where keys are column names and values are the filter values. + :return: A list of matching clients as dictionaries. + """ + try: + conn = sqlite3.connect(DATABASE) + cursor = conn.cursor() + + if isinstance(criteria, ClientUpdateModel): + client_dict = criteria.model_dump(exclude_unset=True) + else: + validated_data = ClientUpdateModel(**criteria) + client_dict = validated_data.model_dump(exclude_unset=True) + + # Build the WHERE clause dynamically + where_clauses = [] + values = [] + for key, value in client_dict.items(): + if value is not None: + where_clauses.append(f"{key} = ?") + values.append(value) + + where_statement = " AND ".join(where_clauses) + if where_clauses: + query = f"SELECT * FROM CommonAssessmentTool_Table WHERE {where_statement}" + else: + query = "SELECT * FROM CommonAssessmentTool_Table" + + cursor.execute(query, tuple(values)) + rows = cursor.fetchall() + + # Map each row to a dictionary + 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", "employment_assistance", "life_stabilization", + "retention_services", "specialized_services", "employment_related_financial_supports", + "employer_financial_supports", "enhanced_referrals", "success_rate", "client_id" + ] + return [dict(zip(columns, row)) for row in rows] + except sqlite3.Error as e: + raise Exception(f"Database error occurred: {str(e)}") from e + finally: + conn.close() + +def delete_client_from_db(client_id: str): + """ + Delete a client record from the database by client_id. + """ + try: + conn = sqlite3.connect(DATABASE) + cursor = conn.cursor() + cursor.execute("DELETE FROM CommonAssessmentTool_Table WHERE client_id = ?", (client_id,)) + conn.commit() + except sqlite3.Error as e: + raise Exception(f"Database error occurred: {str(e)}") from e + finally: + conn.close() + +def update_client_in_db(client_id: str, update_data: dict): + """Update a client record in the database by client_id.""" + try: + conn = sqlite3.connect(DATABASE) + cursor = conn.cursor() + # If update_data is already a ClientUpdateModel, convert it to a dict + if isinstance(update_data, ClientUpdateModel): + update_dict = update_data.model_dump(exclude_unset=True) + else: + # If it's a dict, validate it first + validated_data = ClientUpdateModel(**update_data) + update_dict = validated_data.model_dump(exclude_unset=True) + # Assuming update_data is a dict with column names as keys + updates = ", ".join([f"{key} = ?" for key in update_dict.keys()]) + values = list(update_dict.values()) + values.append(client_id) + cursor.execute(f"UPDATE CommonAssessmentTool_Table SET {updates} WHERE client_id = ?", values) + conn.commit() + except sqlite3.Error as e: + raise Exception(f"Database error occurred: {str(e)}") from e + finally: + conn.close() + +def create_client_in_db(client_data: ClientUpdateModel) -> int: + try: + conn = sqlite3.connect(DATABASE) + cursor = conn.cursor() + if isinstance(client_data, ClientUpdateModel): + client_dict = client_data.model_dump(exclude_unset=True) + else: + validated_data = ClientUpdateModel(**client_data) + client_dict = validated_data.model_dump(exclude_unset=True) + + columns = ", ".join(client_dict.keys()) + placeholders = ", ".join(["?"] * len(client_dict)) + values = list(client_dict.values()) + + cursor.execute( + f"INSERT INTO CommonAssessmentTool_Table ({columns}) VALUES ({placeholders})", + values + ) + conn.commit() + + # Retrieve the last inserted row's ID (autoincremented client_id) + return cursor.lastrowid + except sqlite3.Error as e: + raise Exception(f"Database error occurred: {str(e)}") from e + finally: + conn.close() diff --git a/app/clients/router.py b/app/clients/router.py index f860c402..477156dd 100644 --- a/app/clients/router.py +++ b/app/clients/router.py @@ -1,8 +1,17 @@ -from fastapi import APIRouter + +from fastapi import APIRouter, HTTPException from fastapi.responses import HTMLResponse +from app.clients.service.create import create_client from app.clients.service.logic import interpret_and_calculate +from app.clients.service.delete import delete_client from app.clients.schema import PredictionInput +from app.clients.service.retrieve import retrieve_client, search_clients + +from app.clients.service.update import update_client +from app.clients.service.update_model import ClientUpdateModel +from fastapi import Body + router = APIRouter(prefix="/clients", tags=["clients"]) @@ -12,4 +21,30 @@ async def predict(data: PredictionInput): print(data.model_dump()) return interpret_and_calculate(data.model_dump()) +@router.delete("/{client_id}") +async def delete_client_endpoint(client_id: str): + print(f"Delete client: {client_id}") + return await delete_client(client_id) + +@router.put("/{client_id}") +async def update_client_endpoint(client_id: str, update_data: ClientUpdateModel = Body(...)): + print(f"Update client: {client_id}") + return await update_client(client_id, update_data) + +@router.get("/{client_id}") +async def query_client(client_id: str): + print(f"Query client: {client_id}") + client_data = await retrieve_client(client_id) + if not client_data: + raise HTTPException(status_code=404, detail="Client not found") + return client_data + +@router.post("/") +async def create_client_endpoint(client_data: ClientUpdateModel = Body(...)): + print("Create new client") + return await create_client(client_data) +@router.post("/search") +async def search_clients_endpoint(criteria: ClientUpdateModel = Body(...)): + print("Search clients with given info") + return await search_clients(criteria) diff --git a/app/clients/service/create.py b/app/clients/service/create.py new file mode 100644 index 00000000..78059d12 --- /dev/null +++ b/app/clients/service/create.py @@ -0,0 +1,18 @@ +from fastapi import HTTPException + +from app.clients.mapper import create_client_in_db +from app.clients.service.update_model import ClientUpdateModel + + +async def create_client(client_data: ClientUpdateModel): + validated_data = client_data + + try: + client_id = create_client_in_db(validated_data) + return { + "success": True, + "message": f"Client successfully created with ID {client_id}", + "client_id": client_id + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/app/clients/service/delete.py b/app/clients/service/delete.py new file mode 100644 index 00000000..2d5553b3 --- /dev/null +++ b/app/clients/service/delete.py @@ -0,0 +1,32 @@ +from fastapi import HTTPException +from app.clients.mapper import get_client, delete_client_from_db + +def check_valid_input(client_id): + """Check if the input is valid""" + if not client_id.isdigit(): + raise HTTPException(status_code=400, detail="Invalid client_id format.") + +async def delete_client(client_id: str): + """Deletion service logic""" + # Validate input + check_valid_input(client_id) + + # Retrieve the client + try: + client = get_client(client_id) + if not client: + raise HTTPException( + status_code=404, + detail=f"Client with ID {client_id} not found" + ) + + # Delete the client + delete_client_from_db(client_id) + + return { + "success": True, + "message": f"Client {client_id} successfully deleted", + "client_id": client_id + } + except Exception as e: + raise e diff --git a/app/clients/service/retrieve.py b/app/clients/service/retrieve.py new file mode 100644 index 00000000..94524e8b --- /dev/null +++ b/app/clients/service/retrieve.py @@ -0,0 +1,46 @@ +from fastapi import HTTPException +from app.clients.mapper import get_client, get_all_clients_with_info + +def check_valid_input(client_id): + if not client_id.isdigit(): + raise HTTPException(status_code=400, detail="Invalid client_id format.") + +async def search_clients(criteria): + try: + clients = get_all_clients_with_info(criteria) + if not clients: + raise HTTPException(status_code=404, detail="No clients found matching the criteria.") + return clients + except Exception as e: + raise e + +async def retrieve_client(client_id: str): + try: + # Query the client + client = get_client(client_id) + if not client: + raise HTTPException( + status_code=404, + detail=f"Client with ID {client_id} not found" + ) + + client_data = map_client_data(client) + + # Return the client data as JSON + return client_data + except Exception as e: + raise e + + +def map_client_data(client: tuple) -> dict: + 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", "employment_assistance", "life_stabilization", + "retention_services", "specialized_services", "employment_related_financial_supports", + "employer_financial_supports", "enhanced_referrals", "success_rate", "client_id" + ] + return dict(zip(columns, client)) diff --git a/app/clients/service/update.py b/app/clients/service/update.py new file mode 100644 index 00000000..c86d60b3 --- /dev/null +++ b/app/clients/service/update.py @@ -0,0 +1,31 @@ +from fastapi import HTTPException +from app.clients.mapper import get_client, update_client_in_db + +def check_valid_input(client_id): + """Check if the input is valid""" + if not client_id.isdigit(): + raise HTTPException(status_code=400, detail="Invalid client_id format.") + +async def update_client(client_id: str, update_data: dict): + """Update service logic""" + # Validate input + check_valid_input(client_id) + + # Check if client exists + client = get_client(client_id) + if not client: + raise HTTPException( + status_code=404, + detail=f"Client with ID {client_id} not found" + ) + + # Update the client + try: + update_client_in_db(client_id, update_data) + return { + "success": True, + "message": f"Client {client_id} successfully updated", + "client_id": client_id + } + except Exception as e: + raise e diff --git a/app/clients/service/update_model.py b/app/clients/service/update_model.py new file mode 100644 index 00000000..375a1b4b --- /dev/null +++ b/app/clients/service/update_model.py @@ -0,0 +1,39 @@ +from pydantic import BaseModel, Field + +class ClientUpdateModel(BaseModel): + + age: int = Field(None, example=30) + gender: str = Field(None, example="Male") + work_experience: int = Field(None, example=5) + canada_workex: int = Field(None, example=2) + dep_num: int = Field(None, example=1) + canada_born: str = Field(None, example="No") + citizen_status: str = Field(None, example="Citizen") + level_of_schooling: str = Field(None, example="Bachelor’s degree") + fluent_english: str = Field(None, example="Yes") + reading_english_scale: int = Field(None, example=10) + speaking_english_scale: int = Field(None, example=10) + writing_english_scale: int = Field(None, example=10) + numeracy_scale: int = Field(None, example=10) + computer_scale: int = Field(None, example=10) + transportation_bool: str = Field(None, example="No") + caregiver_bool: str = Field(None, example="No") + housing: str = Field(None, example="Renting") + income_source: str = Field(None, example="Employment") + felony_bool: str = Field(None, example="No") + attending_school: str = Field(None, example="No") + currently_employed: str = Field(None, example="Yes") + substance_use: str = Field(None, example="No") + time_unemployed: int = Field(None, example=0) + need_mental_health_support_bool: str = Field(None, example="No") + employment_assistance: int = Field(None, example=1) + life_stabilization: int = Field(None, example=1) + retention_services: int = Field(None, example=1) + specialized_services: int = Field(None, example=1) + employment_related_financial_supports: int = Field(None, example=1) + employer_financial_supports: int = Field(None, example=1) + enhanced_referrals: int = Field(None, example=1) + success_rate: int = Field(None, example=100) + + class Config: + orm_mode = True diff --git a/app/main.py b/app/main.py index 5b6bf162..9ca9ea0a 100644 --- a/app/main.py +++ b/app/main.py @@ -1,3 +1,4 @@ +"""Main function""" from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware @@ -16,5 +17,3 @@ allow_methods=["*"], # Allows all methods, including OPTIONS allow_headers=["*"], # Allows all headers ) - - diff --git a/db.sql b/db.sql new file mode 100644 index 00000000..989ff578 --- /dev/null +++ b/db.sql @@ -0,0 +1,35 @@ +CREATE TABLE CommonAssessmentTool_Table ( + age INTEGER, + gender TEXT, + work_experience INTEGER, + canada_workex INTEGER, + dep_num INTEGER, + canada_born TEXT, + citizen_status TEXT, + level_of_schooling TEXT, + fluent_english TEXT, + reading_english_scale INTEGER, + speaking_english_scale INTEGER, + writing_english_scale INTEGER, + numeracy_scale INTEGER, + computer_scale INTEGER, + transportation_bool TEXT, + caregiver_bool TEXT, + housing TEXT, + income_source TEXT, + felony_bool TEXT, + attending_school TEXT, + currently_employed TEXT, + substance_use TEXT, + time_unemployed INTEGER, + need_mental_health_support_bool TEXT, + employment_assistance INTEGER, + life_stabilization INTEGER, + retention_services INTEGER, + specialized_services INTEGER, + employment_related_financial_supports INTEGER, + employer_financial_supports INTEGER, + enhanced_referrals INTEGER, + success_rate INTEGER, + client_id INTEGER PRIMARY KEY AUTOINCREMENT +); \ No newline at end of file diff --git a/mydatabase.db b/mydatabase.db new file mode 100644 index 00000000..6919a41c Binary files /dev/null and b/mydatabase.db differ diff --git a/requirements.txt b/requirements.txt index 1ccf75b7..2c306757 100644 --- a/requirements.txt +++ b/requirements.txt @@ -97,6 +97,7 @@ Pygments==2.16.1 pylint==3.0.1 pyrsistent==0.19.3 pytest==7.2.0 +pytest-asyncio==0.21.0 python-dateutil==2.8.2 python-dotenv==1.0.0 python-jose==3.3.0 diff --git a/tests/test.py b/tests/test.py index a911f0a2..2aa8bd4b 100644 --- a/tests/test.py +++ b/tests/test.py @@ -1,4 +1,5 @@ -from logic import interpret_and_calculate +"""Test functions""" +# from logic import interpret_and_calculate from itertools import combinations_with_replacement # def test_interpret_and_calculate(): @@ -20,4 +21,4 @@ result = list(combinations_with_replacement([0, 1], 2)) # Output: [(0, 0), (0, 1), (1, 1)] -print(result) \ No newline at end of file +print(result) diff --git a/tests/test_create_client.py b/tests/test_create_client.py new file mode 100644 index 00000000..e4aa0bca --- /dev/null +++ b/tests/test_create_client.py @@ -0,0 +1,38 @@ +"""Test functions for the create api""" +import pytest +from fastapi import HTTPException +from app.clients.service.create import create_client +from app.clients.service.update_model import ClientUpdateModel + +# Create Client Tests +@pytest.mark.asyncio +async def test_create_client_success(): + """Test successful client creation.""" + client_data = ClientUpdateModel( + age=30, + gender="Male", + work_experience=5, + canada_workex=3, + fluent_english="Yes", + level_of_schooling="Bachelor's", + currently_employed="Yes" + ) + + result = await create_client(client_data) + + # Assertions + assert result["success"] is True + assert "Client successfully created with ID" in result["message"] + assert "client_id" in result + +@pytest.mark.asyncio +async def test_create_client_missing_required_fields(): + """Test client creation with insufficient data.""" + # Create a client model with minimal or missing required fields + client_data = ClientUpdateModel() + + with pytest.raises(HTTPException) as excinfo: + await create_client(client_data) + + # Assertions + assert excinfo.value.status_code == 500 diff --git a/tests/test_delete_client.py b/tests/test_delete_client.py new file mode 100644 index 00000000..9bfdf0f5 --- /dev/null +++ b/tests/test_delete_client.py @@ -0,0 +1,56 @@ +"""Test functions for the delete api""" +import pytest +from fastapi import HTTPException +from app.clients.service.delete import delete_client +from app.clients.service.create import create_client +from app.clients.service.update_model import ClientUpdateModel + +@pytest.mark.asyncio +async def test_delete_client_success(): + """ + Test successful deletion of a client. + """ + + client_data = ClientUpdateModel( + age=23, + gender="Female", + work_experience=2, + canada_workex=5, + ) + result = await create_client(client_data) + client_id = str(result['client_id']) + + # Delete the client created before + result = await delete_client(client_id) + + # Assertions + assert result["success"] is True + assert result["message"] == f"Client {client_id} successfully deleted" + assert result["client_id"] == client_id + +@pytest.mark.asyncio +async def test_delete_client_not_found(): + """ + Test deletion when the client is not found in the database. + """ + + client_id = "250" + with pytest.raises(HTTPException) as excinfo: + await delete_client(client_id) + + # Assertions + assert excinfo.value.status_code == 404 + assert excinfo.value.detail == f"Client with ID {client_id} not found" + +@pytest.mark.asyncio +async def test_delete_client_invalid_id(): + """ + Test deletion with an invalid client_id. + """ + client_id = "invalid_id" + with pytest.raises(HTTPException) as excinfo: + await delete_client(client_id) + + # Assertions + assert excinfo.value.status_code == 400 + assert excinfo.value.detail == "Invalid client_id format." diff --git a/tests/test_retrieve_client.py b/tests/test_retrieve_client.py new file mode 100644 index 00000000..71d8c3c7 --- /dev/null +++ b/tests/test_retrieve_client.py @@ -0,0 +1,58 @@ +"""Test functions for the retrieve api""" +import pytest +from fastapi import HTTPException +from app.clients.service.retrieve import retrieve_client, check_valid_input, search_clients +from app.clients.service.update_model import ClientUpdateModel + +# Retrieve Client Tests +@pytest.mark.asyncio +async def test_retrieve_client_success(): + """Test successful retrieval of a client.""" + client_id = "123" + + result = await retrieve_client(client_id) + + # Assertions + assert isinstance(result, dict) + assert "client_id" in result + assert result["client_id"] == int(client_id) + +# Retrieve Client Tests +@pytest.mark.asyncio +async def test_search_client_success(): + """Test successful retrieval of a client.""" + client_data = ClientUpdateModel( + age=30, + housing="None" + ) + + with pytest.raises(HTTPException) as excinfo: + await search_clients(client_data) + + # Assertions + assert excinfo.value.status_code == 404 + +@pytest.mark.asyncio +async def test_retrieve_client_not_found(): + """Test retrieval of a non-existent client.""" + client_id = "999999" # An ID that should not exist in the database + + with pytest.raises(HTTPException) as excinfo: + await retrieve_client(client_id) + + # Assertions + assert excinfo.value.status_code == 404 + assert f"Client with ID {client_id} not found" in excinfo.value.detail + +@pytest.mark.asyncio +async def test_retrieve_client_invalid_id(): + """Test retrieval with an invalid client ID format.""" + invalid_client_ids = ["abc", "12.3", "-45", ""] + + for client_id in invalid_client_ids: + with pytest.raises(HTTPException) as excinfo: + check_valid_input(client_id) + + # Assertions + assert excinfo.value.status_code == 400 + assert excinfo.value.detail == "Invalid client_id format." diff --git a/tests/test_update_client.py b/tests/test_update_client.py new file mode 100644 index 00000000..2e19ceb8 --- /dev/null +++ b/tests/test_update_client.py @@ -0,0 +1,52 @@ +"""Test functions for the update API""" +import pytest +from fastapi import HTTPException +from app.clients.service.update import update_client + +@pytest.mark.asyncio +async def test_update_client_success(): + """Test successful update of a client.""" + client_id = "45" + update_data = { + "age": 32, + "work_experience": 7, + "canada_workex": 3, + "level_of_schooling": "Master's degree", + "fluent_english": "Yes", + "currently_employed": "Yes", + "housing": "Homeowner" + } + result = await update_client(client_id, update_data) + + # Assertions + assert result["success"] is True + assert result["message"] == f"Client {client_id} successfully updated" + assert result["client_id"] == client_id + +@pytest.mark.asyncio +async def test_update_client_not_found(): + """Test update when the client is not found in the database.""" + client_id = "250" + update_data = { + "age": 45 + } + with pytest.raises(HTTPException) as excinfo: + await update_client(client_id, update_data) + + # Assertions + assert excinfo.value.status_code == 404 + assert excinfo.value.detail == f"Client with ID {client_id} not found" + +@pytest.mark.asyncio +async def test_update_client_invalid_id(): + """Test update with an invalid client_id.""" + client_id = "invalid_id" + update_data = { + "age": 29 + } + with pytest.raises(HTTPException) as excinfo: + await update_client(client_id, update_data) + + # Assertions + assert excinfo.value.status_code == 400 + assert excinfo.value.detail == "Invalid client_id format."