Spaces:
Sleeping
Sleeping
Fixed code in file_processing file although date formatting needs tweaking
Browse files- app/.DS_Store +0 -0
- app/api/routers/file_upload.py +35 -12
- app/categorization/categorizer_list.py +8 -8
- app/categorization/file_processing.py +39 -12
- tests/test_file_upload.py +20 -0
app/.DS_Store
CHANGED
|
Binary files a/app/.DS_Store and b/app/.DS_Store differ
|
|
|
app/api/routers/file_upload.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
from typing import Annotated
|
| 2 |
-
from fastapi import APIRouter, UploadFile, Depends
|
| 3 |
from app.categorization.file_processing import process_file, save_results
|
| 4 |
from app.schema.index import FileUploadCreate
|
| 5 |
import asyncio
|
|
@@ -9,6 +9,7 @@ import csv
|
|
| 9 |
from app.engine.postgresdb import get_db_session
|
| 10 |
from sqlalchemy.ext.asyncio import AsyncSession
|
| 11 |
|
|
|
|
| 12 |
file_upload_router = r = APIRouter(prefix="/api/v1/file_upload", tags=["file_upload"])
|
| 13 |
|
| 14 |
@r.post(
|
|
@@ -21,23 +22,45 @@ file_upload_router = r = APIRouter(prefix="/api/v1/file_upload", tags=["file_upl
|
|
| 21 |
)
|
| 22 |
async def create_file(input_file: UploadFile, db: AsyncSession = Depends(get_db_session)):
|
| 23 |
try:
|
|
|
|
|
|
|
| 24 |
# Create directory to store all uploaded .csv files
|
| 25 |
file_upload_directory_path = "data/tx_data/input"
|
|
|
|
| 26 |
if not os.path.exists(file_upload_directory_path):
|
| 27 |
os.makedirs(file_upload_directory_path)
|
| 28 |
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
-
except Exception:
|
| 40 |
-
|
| 41 |
-
"\n transaction_date, type, category, name_description, amount"}
|
| 42 |
|
| 43 |
return {"message": f"Successfully uploaded {input_file.filename}"}
|
|
|
|
| 1 |
from typing import Annotated
|
| 2 |
+
from fastapi import APIRouter, UploadFile, Depends, HTTPException
|
| 3 |
from app.categorization.file_processing import process_file, save_results
|
| 4 |
from app.schema.index import FileUploadCreate
|
| 5 |
import asyncio
|
|
|
|
| 9 |
from app.engine.postgresdb import get_db_session
|
| 10 |
from sqlalchemy.ext.asyncio import AsyncSession
|
| 11 |
|
| 12 |
+
|
| 13 |
file_upload_router = r = APIRouter(prefix="/api/v1/file_upload", tags=["file_upload"])
|
| 14 |
|
| 15 |
@r.post(
|
|
|
|
| 22 |
)
|
| 23 |
async def create_file(input_file: UploadFile, db: AsyncSession = Depends(get_db_session)):
|
| 24 |
try:
|
| 25 |
+
result = {"file_name": input_file.filename, "output": "", "result": "", "processed_file": "", "error": ""}
|
| 26 |
+
|
| 27 |
# Create directory to store all uploaded .csv files
|
| 28 |
file_upload_directory_path = "data/tx_data/input"
|
| 29 |
+
output_csv_file_path = os.path.join(file_upload_directory_path, input_file.filename)
|
| 30 |
if not os.path.exists(file_upload_directory_path):
|
| 31 |
os.makedirs(file_upload_directory_path)
|
| 32 |
|
| 33 |
+
input_csv_file = open(output_csv_file_path, "a")
|
| 34 |
+
|
| 35 |
+
try:
|
| 36 |
+
if input_file.filename.endswith(".csv"):
|
| 37 |
+
read_csv = await input_file.read()
|
| 38 |
+
# result["output"] = read_csv
|
| 39 |
+
|
| 40 |
+
# Write items of .csv filte to directory
|
| 41 |
+
with open(output_csv_file_path, 'w') as csv_file:
|
| 42 |
+
result["processed_file"] = read_csv
|
| 43 |
+
[csv_file.write(" ".join(row)+'\n') for row in read_csv.decode("utf-8").splitlines()]
|
| 44 |
+
csv_file.close()
|
| 45 |
+
|
| 46 |
+
# with open(output_csv_file_path, 'r') as csv_file:
|
| 47 |
+
# result["output"] = csv_file.read()
|
| 48 |
+
# csv_file.close()
|
| 49 |
+
|
| 50 |
+
# print(f"create_file result: {result}")
|
| 51 |
|
| 52 |
+
# With the newly created file and it's path, process and save it for embedding
|
| 53 |
+
processed_file = process_file(output_csv_file_path)
|
| 54 |
+
result["processed_file"] = processed_file
|
| 55 |
+
result = await asyncio.gather(processed_file)
|
| 56 |
+
result["result"] = result
|
| 57 |
+
await save_results(db, result)
|
| 58 |
+
|
| 59 |
+
except Exception as e:
|
| 60 |
+
result["error"] = str(e)
|
| 61 |
+
raise HTTPException(status_code = 500, detail=f"create_file inner exception: {result} \n")
|
| 62 |
|
| 63 |
+
except Exception as e:
|
| 64 |
+
raise HTTPException(status_code = 500, detail=f"create_file outer exception: {result} \n")
|
|
|
|
| 65 |
|
| 66 |
return {"message": f"Successfully uploaded {input_file.filename}"}
|
app/categorization/categorizer_list.py
CHANGED
|
@@ -29,14 +29,14 @@ async def categorize_list(tx_list: pd.DataFrame) -> pd.DataFrame:
|
|
| 29 |
if os.path.exists(CATEGORY_REFERENCE_OUTPUT_FILE):
|
| 30 |
# Read description-category pairs from the reference file
|
| 31 |
description_category_pairs = pd.read_csv(
|
| 32 |
-
CATEGORY_REFERENCE_OUTPUT_FILE, header=None, names=['
|
| 33 |
)
|
| 34 |
|
| 35 |
# Extract only descriptions for faster matching
|
| 36 |
-
descriptions = description_category_pairs['
|
| 37 |
|
| 38 |
# Use fuzzy matching to find similar descriptions and assign the category
|
| 39 |
-
tx_list['category'] = tx_list['
|
| 40 |
fuzzy_match_list_categorizer,
|
| 41 |
args=(descriptions, description_category_pairs),
|
| 42 |
)
|
|
@@ -44,14 +44,14 @@ async def categorize_list(tx_list: pd.DataFrame) -> pd.DataFrame:
|
|
| 44 |
# Filter out uncategorized transactions, deduplicate, and sort by description
|
| 45 |
uncategorized_descriptions = (
|
| 46 |
tx_list[tx_list['category'].isnull()]
|
| 47 |
-
.drop_duplicates(subset=['
|
| 48 |
-
.sort_values(by=['
|
| 49 |
)
|
| 50 |
|
| 51 |
# Ask the language model to categorize the remaining descriptions
|
| 52 |
if not uncategorized_descriptions.empty:
|
| 53 |
categorized_descriptions = await llm_list_categorizer(
|
| 54 |
-
uncategorized_descriptions[['
|
| 55 |
)
|
| 56 |
|
| 57 |
categorized_descriptions.dropna(inplace=True)
|
|
@@ -59,8 +59,8 @@ async def categorize_list(tx_list: pd.DataFrame) -> pd.DataFrame:
|
|
| 59 |
# Update the category for uncategorized transactions based on the language model results
|
| 60 |
if not categorized_descriptions.empty:
|
| 61 |
tx_list['category'] = tx_list['category'].fillna(
|
| 62 |
-
tx_list['
|
| 63 |
-
categorized_descriptions.set_index('
|
| 64 |
)
|
| 65 |
)
|
| 66 |
|
|
|
|
| 29 |
if os.path.exists(CATEGORY_REFERENCE_OUTPUT_FILE):
|
| 30 |
# Read description-category pairs from the reference file
|
| 31 |
description_category_pairs = pd.read_csv(
|
| 32 |
+
CATEGORY_REFERENCE_OUTPUT_FILE, header=None, names=['name_description', 'category']
|
| 33 |
)
|
| 34 |
|
| 35 |
# Extract only descriptions for faster matching
|
| 36 |
+
descriptions = description_category_pairs['name_description'].values
|
| 37 |
|
| 38 |
# Use fuzzy matching to find similar descriptions and assign the category
|
| 39 |
+
tx_list['category'] = tx_list['name_description'].apply(
|
| 40 |
fuzzy_match_list_categorizer,
|
| 41 |
args=(descriptions, description_category_pairs),
|
| 42 |
)
|
|
|
|
| 44 |
# Filter out uncategorized transactions, deduplicate, and sort by description
|
| 45 |
uncategorized_descriptions = (
|
| 46 |
tx_list[tx_list['category'].isnull()]
|
| 47 |
+
.drop_duplicates(subset=['name_description'])
|
| 48 |
+
.sort_values(by=['name_description'])
|
| 49 |
)
|
| 50 |
|
| 51 |
# Ask the language model to categorize the remaining descriptions
|
| 52 |
if not uncategorized_descriptions.empty:
|
| 53 |
categorized_descriptions = await llm_list_categorizer(
|
| 54 |
+
uncategorized_descriptions[['name_description', 'category']]
|
| 55 |
)
|
| 56 |
|
| 57 |
categorized_descriptions.dropna(inplace=True)
|
|
|
|
| 59 |
# Update the category for uncategorized transactions based on the language model results
|
| 60 |
if not categorized_descriptions.empty:
|
| 61 |
tx_list['category'] = tx_list['category'].fillna(
|
| 62 |
+
tx_list['name_description'].map(
|
| 63 |
+
categorized_descriptions.set_index('name_description')['category']
|
| 64 |
)
|
| 65 |
)
|
| 66 |
|
app/categorization/file_processing.py
CHANGED
|
@@ -8,6 +8,8 @@ from datetime import datetime
|
|
| 8 |
|
| 9 |
import pandas as pd
|
| 10 |
from dateparser import parse
|
|
|
|
|
|
|
| 11 |
|
| 12 |
from app.categorization.categorizer_list import categorize_list
|
| 13 |
from app.categorization.config import RESULT_OUTPUT_FILE, CATEGORY_REFERENCE_OUTPUT_FILE
|
|
@@ -42,8 +44,9 @@ async def process_file(file_path: str) -> Dict[str, Union[str, pd.DataFrame]]:
|
|
| 42 |
except Exception as e:
|
| 43 |
# Return an error indicator and exception info
|
| 44 |
logging.debug(logging.ERROR, f"| File: {file_name} | Unexpected Error: {e}")
|
| 45 |
-
print(f"ERROR processing file {file_name}: {e}")
|
| 46 |
result["error"] = str(e)
|
|
|
|
| 47 |
|
| 48 |
return result
|
| 49 |
|
|
@@ -58,23 +61,42 @@ def standardize_csv_file(file_path: str) -> pd.DataFrame:
|
|
| 58 |
Returns:
|
| 59 |
pd.DataFrame: Prepared transaction data.
|
| 60 |
"""
|
|
|
|
| 61 |
try:
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
except Exception as e:
|
| 74 |
# Return an error indicator and exception info
|
|
|
|
| 75 |
logging.debug("standardize_csv_file Error: {e}")
|
|
|
|
|
|
|
| 76 |
|
| 77 |
-
return tx_list
|
| 78 |
|
| 79 |
|
| 80 |
async def save_results(db: AsyncSession, results: List) -> None:
|
|
@@ -98,6 +120,7 @@ async def save_results(db: AsyncSession, results: List) -> None:
|
|
| 98 |
col_list = ["transaction_date", "type", "category", "name_description", "amount"]
|
| 99 |
tx_list = pd.DataFrame(columns=col_list)
|
| 100 |
for result in results:
|
|
|
|
| 101 |
if not result["error"]:
|
| 102 |
ok_files.append(result["file_name"])
|
| 103 |
result_df = result["output"]
|
|
@@ -113,6 +136,10 @@ async def save_results(db: AsyncSession, results: List) -> None:
|
|
| 113 |
await Transaction.bulk_create(db, txn_list_to_save)
|
| 114 |
|
| 115 |
new_ref_data = tx_list[["name_description", "category"]]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
if os.path.exists(CATEGORY_REFERENCE_OUTPUT_FILE):
|
| 117 |
# If it exists, add master file to interim results
|
| 118 |
old_ref_data = pd.read_csv(CATEGORY_REFERENCE_OUTPUT_FILE, names=["name_description", "category"], header=0)
|
|
|
|
| 8 |
|
| 9 |
import pandas as pd
|
| 10 |
from dateparser import parse
|
| 11 |
+
from fastapi import HTTPException
|
| 12 |
+
import csv
|
| 13 |
|
| 14 |
from app.categorization.categorizer_list import categorize_list
|
| 15 |
from app.categorization.config import RESULT_OUTPUT_FILE, CATEGORY_REFERENCE_OUTPUT_FILE
|
|
|
|
| 44 |
except Exception as e:
|
| 45 |
# Return an error indicator and exception info
|
| 46 |
logging.debug(logging.ERROR, f"| File: {file_name} | Unexpected Error: {e}")
|
| 47 |
+
# print(f"ERROR processing file {file_name}: {e}")
|
| 48 |
result["error"] = str(e)
|
| 49 |
+
raise HTTPException(status_code = 500, detail=f"process_file result: {result}")
|
| 50 |
|
| 51 |
return result
|
| 52 |
|
|
|
|
| 61 |
Returns:
|
| 62 |
pd.DataFrame: Prepared transaction data.
|
| 63 |
"""
|
| 64 |
+
result = {"csv_file": "", "file_path": file_path, "tx_list_columns": {}, "transaction_date": "", "tx_list_new_columns": "", "error": ""}
|
| 65 |
try:
|
| 66 |
+
with open(file_path, 'r') as csv_file:
|
| 67 |
+
# result["csv_file"] = csv_file.read()
|
| 68 |
+
# reader = csv.reader(csv_file)
|
| 69 |
+
# data = list(reader)
|
| 70 |
+
# tx_list = pd.DataFrame(data, columns=data[0])
|
| 71 |
+
|
| 72 |
+
# result["csv_file"] = csv_file.read()
|
| 73 |
+
tx_list = pd.read_csv(file_path, parse_dates=['transaction_date'])
|
| 74 |
+
# tx_list.dropna(inplace=True)
|
| 75 |
+
|
| 76 |
+
# result["tx_list_columns"] = tx_list
|
| 77 |
+
tx_list.attrs["file_name"] = file_path
|
| 78 |
+
tx_list.columns = tx_list.columns.str.lower().str.strip()
|
| 79 |
+
|
| 80 |
+
# Standardize dates to YYYY/MM/DD format
|
| 81 |
+
result["transaction_date"] = pd.to_datetime(tx_list["transaction_date"])
|
| 82 |
+
print(f"transaction_date: {tx_list['transaction_date']}")
|
| 83 |
+
tx_list["transaction_date"] = pd.to_datetime(tx_list["transaction_date"]).dt.strftime("%Y/%m/%d")
|
| 84 |
+
|
| 85 |
+
# Add source and reindex to desired tx format; category column is new and therefore empty
|
| 86 |
+
tx_list.loc[:, "source"] = os.path.basename(file_path)
|
| 87 |
+
tx_list = tx_list.reindex(columns=["transaction_date", "type", "category", "name_description", "amount"])
|
| 88 |
+
# result["tx_list_new_columns"] = tx_list
|
| 89 |
+
|
| 90 |
+
return tx_list
|
| 91 |
|
| 92 |
except Exception as e:
|
| 93 |
# Return an error indicator and exception info
|
| 94 |
+
# print(f"standardize_csv_file exception: {e}")
|
| 95 |
logging.debug("standardize_csv_file Error: {e}")
|
| 96 |
+
result["error"] = str(e)
|
| 97 |
+
raise HTTPException(status_code = 401, detail=f"standardize_csv_file result: {result}")
|
| 98 |
|
| 99 |
+
# return tx_list
|
| 100 |
|
| 101 |
|
| 102 |
async def save_results(db: AsyncSession, results: List) -> None:
|
|
|
|
| 120 |
col_list = ["transaction_date", "type", "category", "name_description", "amount"]
|
| 121 |
tx_list = pd.DataFrame(columns=col_list)
|
| 122 |
for result in results:
|
| 123 |
+
print(f"save_results result: {result}")
|
| 124 |
if not result["error"]:
|
| 125 |
ok_files.append(result["file_name"])
|
| 126 |
result_df = result["output"]
|
|
|
|
| 136 |
await Transaction.bulk_create(db, txn_list_to_save)
|
| 137 |
|
| 138 |
new_ref_data = tx_list[["name_description", "category"]]
|
| 139 |
+
|
| 140 |
+
if not os.path.exists(CATEGORY_REFERENCE_OUTPUT_FILE):
|
| 141 |
+
os.makedirs(CATEGORY_REFERENCE_OUTPUT_FILE)
|
| 142 |
+
|
| 143 |
if os.path.exists(CATEGORY_REFERENCE_OUTPUT_FILE):
|
| 144 |
# If it exists, add master file to interim results
|
| 145 |
old_ref_data = pd.read_csv(CATEGORY_REFERENCE_OUTPUT_FILE, names=["name_description", "category"], header=0)
|
tests/test_file_upload.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime
|
| 2 |
+
from typing import List
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from fastapi import Depends
|
| 5 |
+
from fastapi.testclient import TestClient
|
| 6 |
+
import pytest
|
| 7 |
+
|
| 8 |
+
from app.model.transaction import Transaction
|
| 9 |
+
from app.schema.index import TransactionType, TransactionCreate
|
| 10 |
+
|
| 11 |
+
from sqlalchemy.ext.asyncio import AsyncSession
|
| 12 |
+
from app.engine.postgresdb import get_db_session
|
| 13 |
+
|
| 14 |
+
@pytest.mark.asyncio
|
| 15 |
+
async def test_file_upload(client: TestClient, get_db_session_fixture: AsyncSession) -> None:
|
| 16 |
+
_test_upload_file = Path('/Users/patrickalexis/Documents/codepath-ai-course/codepath-group-project/backend/app/transactions_rag/transactions_2024.csv', 'new-index.json')
|
| 17 |
+
_files = {'input_file': _test_upload_file.open('rb')}
|
| 18 |
+
|
| 19 |
+
response = client.post(("/api/v1/file_upload/"),files=_files)
|
| 20 |
+
assert response.status_code == 201
|