Spaces:
Sleeping
Sleeping
Fixed file processing and standardization logic
Browse files- app/api/routers/file_upload.py +47 -11
- app/categorization/categorizer.py +54 -36
- app/categorization/categorizer_list.py +11 -6
- app/categorization/file_processing.py +48 -35
- migration/versions/e8e988ebb7c7_.py +30 -0
- transactions_2024.csv +28 -28
app/api/routers/file_upload.py
CHANGED
|
@@ -6,6 +6,8 @@ import asyncio
|
|
| 6 |
import os
|
| 7 |
import csv
|
| 8 |
import codecs
|
|
|
|
|
|
|
| 9 |
|
| 10 |
from app.engine.postgresdb import get_db_session
|
| 11 |
from sqlalchemy.ext.asyncio import AsyncSession
|
|
@@ -31,31 +33,65 @@ async def create_file(input_file: UploadFile, db: AsyncSession = Depends(get_db_
|
|
| 31 |
if not os.path.exists(file_upload_directory_path):
|
| 32 |
os.makedirs(file_upload_directory_path)
|
| 33 |
|
| 34 |
-
input_csv_file = open(output_csv_file_path, "a")
|
| 35 |
|
| 36 |
try:
|
| 37 |
if input_file.filename.endswith(".csv"):
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
|
|
|
| 41 |
# Write items of .csv filte to directory
|
| 42 |
-
with open(output_csv_file_path, 'w', encoding=
|
| 43 |
-
|
| 44 |
-
[csv_file.write(" ".join(row)+'\n') for row in read_csv.decode("utf-8").strip().splitlines()]
|
| 45 |
csv_file.close()
|
|
|
|
|
|
|
| 46 |
|
| 47 |
-
with open(output_csv_file_path, 'r', encoding=
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
csv_file.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
-
print(f"create_file result: {result}")
|
| 52 |
|
| 53 |
# With the newly created file and it's path, process and save it for embedding
|
| 54 |
-
processed_file = await process_file(
|
|
|
|
| 55 |
result["processed_file"] = processed_file
|
| 56 |
result = await asyncio.gather(processed_file)
|
| 57 |
result["result"] = result
|
| 58 |
await save_results(db, result)
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
except Exception as e:
|
| 61 |
result["error"] = str(e)
|
|
|
|
| 6 |
import os
|
| 7 |
import csv
|
| 8 |
import codecs
|
| 9 |
+
from io import StringIO
|
| 10 |
+
import pandas as pd
|
| 11 |
|
| 12 |
from app.engine.postgresdb import get_db_session
|
| 13 |
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
| 33 |
if not os.path.exists(file_upload_directory_path):
|
| 34 |
os.makedirs(file_upload_directory_path)
|
| 35 |
|
| 36 |
+
# input_csv_file = open(output_csv_file_path, "a")
|
| 37 |
|
| 38 |
try:
|
| 39 |
if input_file.filename.endswith(".csv"):
|
| 40 |
+
input_contents = await input_file.read()
|
| 41 |
+
buffer = StringIO(input_contents.decode('utf-8'))
|
| 42 |
+
read_csv = csv.reader(buffer)
|
| 43 |
+
|
| 44 |
# Write items of .csv filte to directory
|
| 45 |
+
with codecs.open(output_csv_file_path, 'w+', encoding='utf-8') as csv_file:
|
| 46 |
+
[csv_file.write(" ".join(row)+'\n') for row in read_csv]
|
|
|
|
| 47 |
csv_file.close()
|
| 48 |
+
|
| 49 |
+
new_output_csv_file_path = os.path.join(file_upload_directory_path, f"new_{input_file.filename}")
|
| 50 |
|
| 51 |
+
with codecs.open(output_csv_file_path, 'r', encoding='utf-8') as csv_file:
|
| 52 |
+
with codecs.open(new_output_csv_file_path, 'w+', encoding='utf-8') as new_csv_file:
|
| 53 |
+
csvreader = csv.reader(csv_file)
|
| 54 |
+
header = next(csvreader)[0]
|
| 55 |
+
new_header = header.replace(" ", ",")
|
| 56 |
+
# print(f"header: \n{header} headerItem: {header[0]} new_header: {new_header}\n")
|
| 57 |
+
new_csv_file.write(" ".join(new_header)+'\n')
|
| 58 |
+
for row in csvreader:
|
| 59 |
+
new_row_items = row[0].split(' ')
|
| 60 |
+
new_row = ""
|
| 61 |
+
description = ""
|
| 62 |
+
for idx, element in enumerate(new_row_items):
|
| 63 |
+
# Since our string has been delimited by spaces, we want to get all the words of the description which comes after the 'date' index (0) and
|
| 64 |
+
# before the second to last 'type' index
|
| 65 |
+
if idx > 0 and idx < len(new_row_items) - 2:
|
| 66 |
+
description = description + f" {element}"
|
| 67 |
+
else:
|
| 68 |
+
delim = ", " if idx > 0 else ""
|
| 69 |
+
item = element if len(description) == 0 else f"{description}, {element}"
|
| 70 |
+
new_row = new_row + delim + item
|
| 71 |
+
description = ""
|
| 72 |
+
|
| 73 |
+
# print(f"row: {row} rowType: \n{type(row)} new_row: {new_row}\n")
|
| 74 |
+
new_csv_file.write(" ".join(new_row)+'\n')
|
| 75 |
+
new_csv_file.close()
|
| 76 |
csv_file.close()
|
| 77 |
+
|
| 78 |
+
df = pd.read_csv(new_output_csv_file_path, names=['transaction_date', 'name_description', 'type', 'amount'], header=0, dtype=str)
|
| 79 |
+
# df = df.reindex(columns=['transaction_date', 'name_description', 'type', 'amount'])
|
| 80 |
+
# transaction_date_index = df.columns.get_loc('transaction_date')
|
| 81 |
+
result["output"] = df
|
| 82 |
+
# print(f"column: \n{df['transaction_date']} \n")
|
| 83 |
|
|
|
|
| 84 |
|
| 85 |
# With the newly created file and it's path, process and save it for embedding
|
| 86 |
+
# processed_file = await process_file(new_output_csv_file_path)
|
| 87 |
+
processed_file = await process_file(new_output_csv_file_path, df)
|
| 88 |
result["processed_file"] = processed_file
|
| 89 |
result = await asyncio.gather(processed_file)
|
| 90 |
result["result"] = result
|
| 91 |
await save_results(db, result)
|
| 92 |
+
|
| 93 |
+
buffer.close()
|
| 94 |
+
input_file.close()
|
| 95 |
|
| 96 |
except Exception as e:
|
| 97 |
result["error"] = str(e)
|
app/categorization/categorizer.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
-
# Standard library imports
|
| 2 |
import re
|
| 3 |
import ast
|
| 4 |
import json
|
|
@@ -6,7 +5,6 @@ import logging
|
|
| 6 |
import os
|
| 7 |
from typing import Any, List, Tuple, Optional, Dict, Union
|
| 8 |
|
| 9 |
-
# Third-party library imports
|
| 10 |
import numpy as np
|
| 11 |
import pandas as pd
|
| 12 |
import asyncio
|
|
@@ -14,7 +12,8 @@ from rapidfuzz import process
|
|
| 14 |
from tenacity import retry, wait_random_exponential, stop_after_attempt
|
| 15 |
from pydantic import ValidationError
|
| 16 |
|
| 17 |
-
|
|
|
|
| 18 |
from langchain_openai import ChatOpenAI
|
| 19 |
from langchain.chains import LLMChain
|
| 20 |
from langchain.output_parsers import PydanticOutputParser, OutputFixingParser
|
|
@@ -44,16 +43,23 @@ def fuzzy_match_list_categorizer(
|
|
| 44 |
Returns:
|
| 45 |
str or None: Category of the matched description, or None if no match found.
|
| 46 |
"""
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
|
| 59 |
async def llm_list_categorizer(tx_list: pd.DataFrame) -> pd.DataFrame:
|
|
@@ -68,31 +74,42 @@ async def llm_list_categorizer(tx_list: pd.DataFrame) -> pd.DataFrame:
|
|
| 68 |
Returns:
|
| 69 |
pd.DataFrame: DataFrame mapping transaction descriptions to their inferred categories.
|
| 70 |
"""
|
|
|
|
|
|
|
| 71 |
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
-
# Return a DataFrame with the valid outputs
|
| 95 |
-
return pd.DataFrame(valid_outputs, columns=['name_description', 'category'])
|
| 96 |
|
| 97 |
|
| 98 |
@retry(wait=wait_random_exponential(min=1, max=20), stop=stop_after_attempt(6))
|
|
@@ -143,5 +160,6 @@ async def llm_sublist_categorizer(
|
|
| 143 |
logging.log(
|
| 144 |
logging.ERROR, f"| File: {file_name} | Unexpected Error: {e}\nRaw Result: {raw_result}")
|
| 145 |
result['valid'] = False
|
|
|
|
| 146 |
|
| 147 |
return result
|
|
|
|
|
|
|
| 1 |
import re
|
| 2 |
import ast
|
| 3 |
import json
|
|
|
|
| 5 |
import os
|
| 6 |
from typing import Any, List, Tuple, Optional, Dict, Union
|
| 7 |
|
|
|
|
| 8 |
import numpy as np
|
| 9 |
import pandas as pd
|
| 10 |
import asyncio
|
|
|
|
| 12 |
from tenacity import retry, wait_random_exponential, stop_after_attempt
|
| 13 |
from pydantic import ValidationError
|
| 14 |
|
| 15 |
+
from fastapi import HTTPException
|
| 16 |
+
|
| 17 |
from langchain_openai import ChatOpenAI
|
| 18 |
from langchain.chains import LLMChain
|
| 19 |
from langchain.output_parsers import PydanticOutputParser, OutputFixingParser
|
|
|
|
| 43 |
Returns:
|
| 44 |
str or None: Category of the matched description, or None if no match found.
|
| 45 |
"""
|
| 46 |
+
result = {"description": description, "descriptions": descriptions, "description_category_pairs": description_category_pairs, "match_results": "", "error": ""}
|
| 47 |
+
try:
|
| 48 |
+
# Fuzzy-match this description against the reference descriptions
|
| 49 |
+
match_results = process.extractOne(
|
| 50 |
+
description, descriptions, score_cutoff=threshold)
|
| 51 |
+
|
| 52 |
+
result["match_results"] = match_results
|
| 53 |
+
|
| 54 |
+
# If a match is found, return the category of the matched description
|
| 55 |
+
if match_results:
|
| 56 |
+
return description_category_pairs.at[match_results[2], 'category']
|
| 57 |
+
|
| 58 |
+
return None
|
| 59 |
+
|
| 60 |
+
except Exception as e:
|
| 61 |
+
result["error"] = str(e)
|
| 62 |
+
raise HTTPException(status_code = 500, detail=f"fuzzy_match_list_categorizer result: {result}")
|
| 63 |
|
| 64 |
|
| 65 |
async def llm_list_categorizer(tx_list: pd.DataFrame) -> pd.DataFrame:
|
|
|
|
| 74 |
Returns:
|
| 75 |
pd.DataFrame: DataFrame mapping transaction descriptions to their inferred categories.
|
| 76 |
"""
|
| 77 |
+
|
| 78 |
+
result = {"tx_list": tx_list, "results": "", "valid results": "", "valid outputs": "", "error": ""}
|
| 79 |
|
| 80 |
+
try:
|
| 81 |
+
# Initialize language model and prompt
|
| 82 |
+
openai_api_key = os.environ['OPENAI_API_KEY']
|
| 83 |
+
llm = ChatOpenAI(temperature=0, model="gpt-3.5-turbo-0125",
|
| 84 |
+
api_key=openai_api_key)
|
| 85 |
+
prompt = PromptTemplate.from_template(template=CATEGORY_TEMPLATE)
|
| 86 |
+
chain = LLMChain(llm=llm, prompt=prompt)
|
| 87 |
+
|
| 88 |
+
# Iterate over the DataFrame in batches of TX_PER_LLM_RUN transactions
|
| 89 |
+
tasks = [llm_sublist_categorizer(tx_list.attrs['file_name'], chain=chain, tx_descriptions="\n".join(chunk['name_description']).strip())
|
| 90 |
+
for chunk in np.array_split(tx_list, tx_list.shape[0] // TX_PER_LLM_RUN + 1)]
|
| 91 |
+
|
| 92 |
+
# Gather results and extract (valid) outputs
|
| 93 |
+
# The results variable is a list of 'results', each 'result' being the output of a single LLM run
|
| 94 |
+
results = await asyncio.gather(*tasks)
|
| 95 |
+
result["results"] = results
|
| 96 |
+
|
| 97 |
+
# Extract valid results (each valid result is a list of description-category pairs)
|
| 98 |
+
valid_results = [result['output'] for result in results if result['valid']]
|
| 99 |
+
result["valid results"] = valid_results
|
| 100 |
+
|
| 101 |
+
# Flatten the list of valid results to obtain a single list of description-category pairs
|
| 102 |
+
valid_outputs = [
|
| 103 |
+
output for valid_result in valid_results for output in valid_result]
|
| 104 |
+
result["valid outputs"] = valid_outputs
|
| 105 |
+
|
| 106 |
+
# Return a DataFrame with the valid outputs
|
| 107 |
+
return pd.DataFrame(valid_outputs, columns=['name_description', 'category'])
|
| 108 |
+
|
| 109 |
+
except Exception as e:
|
| 110 |
+
result["error"] = str(e)
|
| 111 |
+
raise HTTPException(status_code = 500, detail=f"llm_list_categorizer result: {result}")
|
| 112 |
|
|
|
|
|
|
|
| 113 |
|
| 114 |
|
| 115 |
@retry(wait=wait_random_exponential(min=1, max=20), stop=stop_after_attempt(6))
|
|
|
|
| 160 |
logging.log(
|
| 161 |
logging.ERROR, f"| File: {file_name} | Unexpected Error: {e}\nRaw Result: {raw_result}")
|
| 162 |
result['valid'] = False
|
| 163 |
+
raise HTTPException(status_code = 500, detail=f"llm_sublist_categorizer result: {result}")
|
| 164 |
|
| 165 |
return result
|
app/categorization/categorizer_list.py
CHANGED
|
@@ -24,7 +24,7 @@ async def categorize_list(tx_list: pd.DataFrame) -> pd.DataFrame:
|
|
| 24 |
Returns:
|
| 25 |
pd.DataFrame: The original DataFrame with an additional column for the category.
|
| 26 |
"""
|
| 27 |
-
result = {"tx_list":
|
| 28 |
|
| 29 |
try:
|
| 30 |
if not os.path.exists(CATEGORY_REFERENCE_OUTPUT_FILE):
|
|
@@ -33,17 +33,22 @@ async def categorize_list(tx_list: pd.DataFrame) -> pd.DataFrame:
|
|
| 33 |
if os.path.exists(CATEGORY_REFERENCE_OUTPUT_FILE):
|
| 34 |
# Read description-category pairs from the reference file
|
| 35 |
description_category_pairs = pd.read_csv(
|
| 36 |
-
CATEGORY_REFERENCE_OUTPUT_FILE, header=None, names=['name_description', 'category']
|
|
|
|
| 37 |
)
|
| 38 |
-
|
| 39 |
-
|
| 40 |
# Extract only descriptions for faster matching
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
# Use fuzzy matching to find similar descriptions and assign the category
|
| 44 |
tx_list['category'] = tx_list['name_description'].apply(
|
| 45 |
fuzzy_match_list_categorizer,
|
| 46 |
-
args=(descriptions, description_category_pairs)
|
| 47 |
)
|
| 48 |
|
| 49 |
# Filter out uncategorized transactions, deduplicate, and sort by description
|
|
|
|
| 24 |
Returns:
|
| 25 |
pd.DataFrame: The original DataFrame with an additional column for the category.
|
| 26 |
"""
|
| 27 |
+
result = {"tx_list": pd.DataFrame(), "description_category": "", "description_category_pairs": "", "uncategorized_descriptions": "", "categorized_descriptions": "", "error": ""}
|
| 28 |
|
| 29 |
try:
|
| 30 |
if not os.path.exists(CATEGORY_REFERENCE_OUTPUT_FILE):
|
|
|
|
| 33 |
if os.path.exists(CATEGORY_REFERENCE_OUTPUT_FILE):
|
| 34 |
# Read description-category pairs from the reference file
|
| 35 |
description_category_pairs = pd.read_csv(
|
| 36 |
+
CATEGORY_REFERENCE_OUTPUT_FILE, sep=r'\s*,\s*', header=None, names=['name_description', 'category'], encoding='utf-8',
|
| 37 |
+
engine='python'
|
| 38 |
)
|
| 39 |
+
|
|
|
|
| 40 |
# Extract only descriptions for faster matching
|
| 41 |
+
description_category_pairs.columns = description_category_pairs.columns.str.strip()
|
| 42 |
+
# descriptions = description_category_pairs['name_description'].values
|
| 43 |
+
descriptions = tx_list['name_description'].values
|
| 44 |
+
|
| 45 |
+
result['description_category'] = descriptions
|
| 46 |
+
result['tx_list'] = tx_list['name_description']
|
| 47 |
|
| 48 |
# Use fuzzy matching to find similar descriptions and assign the category
|
| 49 |
tx_list['category'] = tx_list['name_description'].apply(
|
| 50 |
fuzzy_match_list_categorizer,
|
| 51 |
+
args=(descriptions, description_category_pairs)
|
| 52 |
)
|
| 53 |
|
| 54 |
# Filter out uncategorized transactions, deduplicate, and sort by description
|
app/categorization/file_processing.py
CHANGED
|
@@ -20,7 +20,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|
| 20 |
|
| 21 |
|
| 22 |
# Read file and process it (e.g. categorize transactions)
|
| 23 |
-
async def process_file(file_path: str) -> Dict[str, Union[str, pd.DataFrame]]:
|
| 24 |
"""
|
| 25 |
Process the input file by reading, cleaning, standardizing, and categorizing the transactions.
|
| 26 |
|
|
@@ -32,11 +32,11 @@ async def process_file(file_path: str) -> Dict[str, Union[str, pd.DataFrame]]:
|
|
| 32 |
"""
|
| 33 |
|
| 34 |
file_name = os.path.basename(file_path)
|
| 35 |
-
result = {"
|
| 36 |
try:
|
| 37 |
# Read file into standardized tx format: transaction_date, name_description, type, amount, category, source
|
| 38 |
-
tx_list = standardize_csv_file(file_path)
|
| 39 |
-
|
| 40 |
|
| 41 |
# Categorize transactions
|
| 42 |
result["output"] = await categorize_list(tx_list)
|
|
@@ -45,14 +45,13 @@ async def process_file(file_path: str) -> Dict[str, Union[str, pd.DataFrame]]:
|
|
| 45 |
except Exception as e:
|
| 46 |
# Return an error indicator and exception info
|
| 47 |
logging.debug(logging.ERROR, f"| File: {file_name} | Unexpected Error: {e}")
|
| 48 |
-
print(f"ERROR processing file {file_name}: {e} errorType: {type(e)}")
|
| 49 |
result["error"] = str(e)
|
| 50 |
-
raise HTTPException(status_code = 500, detail=f"process_file result: {result}")
|
| 51 |
|
| 52 |
return result
|
| 53 |
|
| 54 |
|
| 55 |
-
def standardize_csv_file(file_path: str) -> pd.DataFrame:
|
| 56 |
"""
|
| 57 |
Read and prepare the data from the input file.
|
| 58 |
|
|
@@ -62,46 +61,60 @@ def standardize_csv_file(file_path: str) -> pd.DataFrame:
|
|
| 62 |
Returns:
|
| 63 |
pd.DataFrame: Prepared transaction data.
|
| 64 |
"""
|
| 65 |
-
result = {"csv_file": "", "file_path": file_path, "
|
|
|
|
| 66 |
try:
|
|
|
|
| 67 |
# result["csv_file"] = csv_file.read()
|
| 68 |
# reader = csv.reader(csv_file)
|
| 69 |
# data = list(reader)
|
| 70 |
-
#
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
#
|
| 78 |
-
|
| 79 |
-
tx_list.columns = tx_list.columns.str.lower().str.strip()
|
| 80 |
|
| 81 |
-
#
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
# Add source and reindex to desired tx format; category column is new and therefore empty
|
| 87 |
-
tx_list.
|
| 88 |
-
# tx_list
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
#
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
|
| 96 |
except Exception as e:
|
| 97 |
# Return an error indicator and exception info
|
| 98 |
-
# print(f"standardize_csv_file exception: {e}")
|
| 99 |
logging.debug("standardize_csv_file Error: {e}")
|
| 100 |
-
|
|
|
|
|
|
|
|
|
|
| 101 |
raise HTTPException(status_code = 401, detail=f"standardize_csv_file exception: {result}")
|
| 102 |
|
| 103 |
-
return tx_list
|
| 104 |
-
|
| 105 |
|
| 106 |
async def save_results(db: AsyncSession, results: List) -> None:
|
| 107 |
"""
|
|
@@ -168,4 +181,4 @@ async def save_results(db: AsyncSession, results: List) -> None:
|
|
| 168 |
# print(f"standardize_csv_file exception: {e}")
|
| 169 |
logging.debug("save_results Error: {e}")
|
| 170 |
result["error"] = str(e)
|
| 171 |
-
raise HTTPException(status_code = 401, detail=f"save_results exception: {
|
|
|
|
| 20 |
|
| 21 |
|
| 22 |
# Read file and process it (e.g. categorize transactions)
|
| 23 |
+
async def process_file(file_path: str, df: pd.DataFrame) -> Dict[str, Union[str, pd.DataFrame]]:
|
| 24 |
"""
|
| 25 |
Process the input file by reading, cleaning, standardizing, and categorizing the transactions.
|
| 26 |
|
|
|
|
| 32 |
"""
|
| 33 |
|
| 34 |
file_name = os.path.basename(file_path)
|
| 35 |
+
result = {"file_path": file_path, "output": pd.DataFrame(), "columns": [], "error": ""}
|
| 36 |
try:
|
| 37 |
# Read file into standardized tx format: transaction_date, name_description, type, amount, category, source
|
| 38 |
+
tx_list = standardize_csv_file(file_path, df)
|
| 39 |
+
result["columns"] = tx_list.columns.tolist()
|
| 40 |
|
| 41 |
# Categorize transactions
|
| 42 |
result["output"] = await categorize_list(tx_list)
|
|
|
|
| 45 |
except Exception as e:
|
| 46 |
# Return an error indicator and exception info
|
| 47 |
logging.debug(logging.ERROR, f"| File: {file_name} | Unexpected Error: {e}")
|
|
|
|
| 48 |
result["error"] = str(e)
|
| 49 |
+
raise HTTPException(status_code = 500, detail=f"\n\n process_file result: \n{result}")
|
| 50 |
|
| 51 |
return result
|
| 52 |
|
| 53 |
|
| 54 |
+
def standardize_csv_file(file_path: str, d: pd.DataFrame) -> pd.DataFrame:
|
| 55 |
"""
|
| 56 |
Read and prepare the data from the input file.
|
| 57 |
|
|
|
|
| 61 |
Returns:
|
| 62 |
pd.DataFrame: Prepared transaction data.
|
| 63 |
"""
|
| 64 |
+
result = {"csv_file": "", "file_path": file_path, "new_csv_file": "", "transaction_date": "", "tx_list_new_columns": "", "error": ""}
|
| 65 |
+
|
| 66 |
try:
|
| 67 |
+
#1
|
| 68 |
# result["csv_file"] = csv_file.read()
|
| 69 |
# reader = csv.reader(csv_file)
|
| 70 |
# data = list(reader)
|
| 71 |
+
# df = pd.DataFrame(data, columns=data[0])
|
| 72 |
+
|
| 73 |
+
#2
|
| 74 |
+
# df = pd.read_csv(file_path)
|
| 75 |
+
|
| 76 |
+
#3
|
| 77 |
+
# with open(file_path, 'r', encoding='utf-8') as file:
|
| 78 |
+
# csv_reader = csv.reader(file)
|
| 79 |
+
# data = list(csv_reader)
|
|
|
|
| 80 |
|
| 81 |
+
# df = pd.DataFrame(data)
|
| 82 |
+
|
| 83 |
+
headers = d.columns
|
| 84 |
+
df = pd.DataFrame(d.values[1:], columns=headers)
|
| 85 |
+
|
| 86 |
+
result["csv_file"] = df
|
| 87 |
+
df.attrs['file_name'] = file_path
|
| 88 |
+
# print(f"\n\n standardize_csv_file columns: \n{df} \n date:\n{df['transaction_date']} \n")
|
| 89 |
+
# tx_list.columns = ['transaction_date', 'name_description', 'type', 'amount', 'category']
|
| 90 |
|
| 91 |
+
# # Standardize dates to YYYY/MM/DD format
|
| 92 |
+
warnings.filterwarnings('ignore', 'Parsing dates', category=UserWarning)
|
| 93 |
+
# df['transaction_date'] = pd.to_datetime(df['transaction_date']).dt.strftime('%d/%m/%Y')
|
| 94 |
+
|
| 95 |
# Add source and reindex to desired tx format; category column is new and therefore empty
|
| 96 |
+
# tx_list.insert(0, "source", [os.path.basename(file_path) for i in range(len(tx_list.columns))])
|
| 97 |
+
# tx_list.insert(0, "category", [])
|
| 98 |
+
|
| 99 |
+
# df = df.reindex(columns=['transaction_date', 'name_description', 'type', 'amount', 'category'])
|
| 100 |
+
# tx_list['transaction_date'] = pd.to_datetime(tx_list['transaction_date']).dt.strftime('%Y/%m/%d')
|
| 101 |
+
# tx_list.set_index(['transaction_date', 'name_description', 'type', 'amount', 'category'])
|
| 102 |
+
df['source'] = pd.Series([os.path.basename(file_path)] * len(df.index))
|
| 103 |
+
df['category'] = pd.Series([])
|
| 104 |
+
result["new_csv_file"] = df
|
| 105 |
+
print(f"\n\n standardize_csv_file result: \n{df} \n")
|
| 106 |
+
|
| 107 |
+
return df
|
| 108 |
|
| 109 |
except Exception as e:
|
| 110 |
# Return an error indicator and exception info
|
|
|
|
| 111 |
logging.debug("standardize_csv_file Error: {e}")
|
| 112 |
+
if hasattr(e, 'message'):
|
| 113 |
+
result["error"] = str(e.message)
|
| 114 |
+
else:
|
| 115 |
+
result["error"] = str(e)
|
| 116 |
raise HTTPException(status_code = 401, detail=f"standardize_csv_file exception: {result}")
|
| 117 |
|
|
|
|
|
|
|
| 118 |
|
| 119 |
async def save_results(db: AsyncSession, results: List) -> None:
|
| 120 |
"""
|
|
|
|
| 181 |
# print(f"standardize_csv_file exception: {e}")
|
| 182 |
logging.debug("save_results Error: {e}")
|
| 183 |
result["error"] = str(e)
|
| 184 |
+
raise HTTPException(status_code = 401, detail=f"save_results exception: {error_messages}")
|
migration/versions/e8e988ebb7c7_.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
|
| 3 |
+
Revision ID: e8e988ebb7c7
|
| 4 |
+
Revises: 4e76691ab103
|
| 5 |
+
Create Date: 2024-06-08 18:13:46.160432
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
from typing import Sequence, Union
|
| 9 |
+
|
| 10 |
+
from alembic import op
|
| 11 |
+
import sqlalchemy as sa
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
# revision identifiers, used by Alembic.
|
| 15 |
+
revision: str = 'e8e988ebb7c7'
|
| 16 |
+
down_revision: Union[str, None] = '4e76691ab103'
|
| 17 |
+
branch_labels: Union[str, Sequence[str], None] = None
|
| 18 |
+
depends_on: Union[str, Sequence[str], None] = None
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def upgrade() -> None:
|
| 22 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
| 23 |
+
pass
|
| 24 |
+
# ### end Alembic commands ###
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def downgrade() -> None:
|
| 28 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
| 29 |
+
pass
|
| 30 |
+
# ### end Alembic commands ###
|
transactions_2024.csv
CHANGED
|
@@ -1,29 +1,29 @@
|
|
| 1 |
transaction_date,name_description,type,amount
|
| 2 |
-
2023
|
| 3 |
-
2023
|
| 4 |
-
2023
|
| 5 |
-
2023
|
| 6 |
-
2023
|
| 7 |
-
|
| 8 |
-
2023
|
| 9 |
-
2023
|
| 10 |
-
2023
|
| 11 |
-
2023
|
| 12 |
-
2023
|
| 13 |
-
|
| 14 |
-
2023
|
| 15 |
-
2023
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
|
|
|
| 1 |
transaction_date,name_description,type,amount
|
| 2 |
+
20/11/2023,Comcast Internet,Expense,9.96
|
| 3 |
+
21/04/2023,Lemonade Home Insurance,Expense,17.53
|
| 4 |
+
22/05/2023,Monthly Appartment Rent,Expense,2000.0
|
| 5 |
+
14/06/2023,Staples Office Supplies,Expense,12.46
|
| 6 |
+
17/07/2023,Selling Paintings,Income,13.63
|
| 7 |
+
19/12/2023,Spotify,Expense,12.19
|
| 8 |
+
05/10/2023,Target,Expense,27.08
|
| 9 |
+
03/08/2023,IT Consulting,Income,541.57
|
| 10 |
+
06/04/2023,Phone,Expense,10.7
|
| 11 |
+
02/01/2023,ML Consulting,Income,2641.93
|
| 12 |
+
05/04/2023,Chipotle,Expense,18.9
|
| 13 |
+
18/05/2023,Cold Brew Coffee,Expense,17.67
|
| 14 |
+
13/03/2023,Gas,Expense,8.80
|
| 15 |
+
19/07/2023,CA Property Tax,Expense,1670.34
|
| 16 |
+
20/08/2023,Salary,Income,4000.36
|
| 17 |
+
20/09/2023,Cellphone,Expense,19.27
|
| 18 |
+
06/10/2023,Gym Membership,Expense,24.71
|
| 19 |
+
02/01/2023,Wholefoods,Expense,17.35
|
| 20 |
+
11/06/2023,Freelancing,Income,2409.55
|
| 21 |
+
12/11/2023,Spotify,Expense,20.76
|
| 22 |
+
20/03/2023,Blogging,Income,4044.27
|
| 23 |
+
12/02/2023,Uber Taxi,Expense,18.9
|
| 24 |
+
23/07/2023,Uber Taxi,Expense,27.54
|
| 25 |
+
01/09/2023,Apple Services,Expense,41.25
|
| 26 |
+
07/05/2023,Netflix,Expense,22.8
|
| 27 |
+
09/08/2023,Amazon Lux,Expense,24.11
|
| 28 |
+
17/01/2023,Burger King,Expense,30.08
|
| 29 |
+
14/01/2023,Amazon,Expense,11.0
|