File size: 8,674 Bytes
e612627
 
 
 
 
 
 
 
 
 
cd5e987
 
e612627
 
 
0dbb03c
 
 
b905bd6
 
e612627
 
52b3c48
e612627
 
 
 
 
c37c62e
e612627
 
 
0dbb03c
e612627
 
c37c62e
e612627
e506f4d
52b3c48
 
e612627
 
0dbb03c
c37c62e
e612627
 
 
bd3733d
0dbb03c
52b3c48
e612627
0dbb03c
e612627
 
52b3c48
e612627
 
 
 
 
 
 
 
 
52b3c48
 
bd3733d
52b3c48
e506f4d
 
 
52b3c48
 
 
 
 
 
 
 
 
cd5e987
52b3c48
 
 
 
 
 
 
 
 
e506f4d
52b3c48
 
 
 
e506f4d
52b3c48
 
 
 
 
 
 
 
 
c37c62e
52b3c48
 
bd3733d
 
 
 
52b3c48
 
 
 
e506f4d
e612627
 
c37c62e
e612627
 
 
 
 
 
 
 
 
 
c37c62e
e506f4d
 
 
 
 
 
c37c62e
e506f4d
 
 
c37c62e
e506f4d
 
 
c37c62e
e506f4d
c37c62e
e506f4d
c37c62e
e506f4d
c37c62e
e506f4d
 
c37c62e
e506f4d
c37c62e
 
 
 
 
e506f4d
 
 
 
c37c62e
 
e506f4d
 
 
 
 
 
 
 
 
 
 
c37c62e
 
e506f4d
 
 
 
 
 
c37c62e
 
 
 
 
e506f4d
c37c62e
e506f4d
 
 
 
 
 
e612627
e506f4d
 
 
 
 
c37c62e
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
import os
import glob
import shutil
import logging
import warnings
from typing import Optional, Union, Dict, List
from datetime import datetime

import pandas as pd
from dateparser import parse
from fastapi import  HTTPException
import csv

from app.categorization.categorizer_list import categorize_list
from app.categorization.config import RESULT_OUTPUT_FILE, CATEGORY_REFERENCE_OUTPUT_FILE
from app.model.transaction import Transaction
from app.schema.index import TransactionCreate

from sqlalchemy.ext.asyncio import AsyncSession


# Read file and process it (e.g. categorize transactions)
async def process_file(file_path: str, df: pd.DataFrame) -> Dict[str, Union[str, pd.DataFrame]]:
    """
    Process the input file by reading, cleaning, standardizing, and categorizing the transactions.

    Args:
        file_path (str): Path to the input file.
        df (dataframe): Dataframe representing input file

    Returns:
        Dict[str, Union[str, pd.DataFrame]]: Dictionary containing the file name, processed output, and error information if any
    """

    file_name = os.path.basename(file_path)
    result = {'file_path': file_name, 'output': pd.DataFrame(), 'error': ''}
    try:
        # Read file into standardized tx format: transaction_date, name_description, type, amount, category, source
        tx_list = standardize_csv_file(file_path, df)
        result["columns"] = tx_list.columns.tolist()

        # Categorize transactions
        result["output"] = await categorize_list(tx_list)
        # print(f"File processed sucessfully: {file_name}")

    except Exception as e:
        # Return an error indicator and exception info
        logging.debug(logging.ERROR, f"| File: {file_name} | Unexpected Error: {e}")
        result["error"] = str(e)
        raise HTTPException(status_code = 500, detail=f"\n\n process_file result: \n{result}")

    return result


def standardize_csv_file(file_path: str, d: pd.DataFrame) -> pd.DataFrame:
    """
    Read and prepare the data from the input file.

    Args:
        file_path (str): Path to the input file.

    Returns:
        pd.DataFrame: Prepared transaction data.
    """
    result = {"csv_file": "", "file_path": file_path, "new_csv_file": "", "transaction_date": "", "tx_list_new_columns": "", "error": ""}
    
    try:
        #1
        # result["csv_file"] = csv_file.read()
        # reader = csv.reader(csv_file)
        # data = list(reader)
        # df = pd.DataFrame(data, columns=data[0])

        #2
        # df = pd.read_csv(file_path)

        #3
        # with open(file_path, 'r', encoding='utf-8') as file:
        #     csv_reader = csv.reader(file)
        #     data = list(csv_reader)
        
        # df = pd.DataFrame(data)

        headers = d.columns
        df = pd.DataFrame(d.values[1:], columns=headers)

        result["csv_file"] = df
        df.attrs['file_name'] = file_path
        # print(f"\n\n standardize_csv_file columns: \n{df} \n date:\n{df['transaction_date']} \n")
        # tx_list.columns = ['transaction_date', 'name_description', 'type', 'amount', 'category']

        # # Standardize dates to YYYY/MM/DD format
        warnings.filterwarnings('ignore', 'Parsing dates', category=UserWarning)
        # df['transaction_date'] =  pd.to_datetime(df['transaction_date']).dt.strftime('%d/%m/%Y')
        
        # Add source and reindex to desired tx format; category column is new and therefore empty
        # tx_list.insert(0, "source", [os.path.basename(file_path) for i in range(len(tx_list.columns))])
        # tx_list.insert(0, "category", [])

        # df = df.reindex(columns=['transaction_date', 'name_description', 'type', 'amount', 'category'])
        # tx_list['transaction_date'] = pd.to_datetime(tx_list['transaction_date']).dt.strftime('%Y/%m/%d')
        # tx_list.set_index(['transaction_date', 'name_description', 'type', 'amount', 'category'])
        df['source'] = pd.Series([os.path.basename(file_path)] * len(df.index))
        df['category'] = pd.Series([])
        result["new_csv_file"] = df
        # print(f"\n\n standardize_csv_file result: \n{df} \n")
       
        return df
    
    except Exception as e:
        # Return an error indicator and exception info
        logging.debug("standardize_csv_file Error: {e}")
        if hasattr(e, 'message'):
            result["error"] = str(e.message)
        else:
            result["error"] = str(e)
        raise HTTPException(status_code = 401, detail=f"standardize_csv_file exception: {result}")


async def save_results(db: AsyncSession, results: List) -> Dict[str, Union[List[str], List[TransactionCreate], pd.DataFrame]]:
    """
    Merge all interim results in the input folder and write the merged results to the output file.

    Args:
        in_folder (str): Path to the input folder containing interim results.
        out_file (str): Path to the output file.

    Returns:
        None
    """
    result = {'ok_files': '', 'ko_files': '', 'error_messages': '', 'txn_list_to_save': '', 'old_ref_data': '', 'new_ref_data': ''}
    try:
        # Concatenate all (valid) results into a single DataFrame
        # Print errors to console
        ok_files = []
        ko_files = []
        error_messages = []
        # print(f"save_results results: {results}")

        col_list = ["transaction_date", "name_description", "type", "amount", "category"]
        tx_list = pd.DataFrame(columns=col_list)
        # print(f"save_results tx_list: {tx_list}")
        for result in results:
            if not result["error"]:
                ok_files.append(result["file_name"])
                print(f"save_results ok_files: {ok_files}\n")
                result_df = result["output"]
                print(f"save_results result_df: {result_df}\n")
                result_df.columns = col_list
                print(f"save_results result_df.columns: {result_df.columns}\n")
                tx_list = pd.concat([tx_list, result_df], ignore_index=True)
                print(f"save_results tx_list: {tx_list}\n")
            else:
                ko_files.append(result["file_name"])
                print(f"save_results ko_files: {ko_files}\n")
                error_messages.append(f"{result['file_name']}: {result['error']}")
                print(f"save_results error_messages: {error_messages}\n")
        
        result['ok_files'] = ok_files
        result['ko_files'] = ko_files
        result['error_messages'] = error_messages

        # Save to database
        # FIXME: get user_id from session
        txn_list_to_save = [TransactionCreate(**row.to_dict(), user_id=1) for _, row in tx_list.iterrows()]
        result['txn_list_to_save'] = txn_list_to_save
        print(f"save_results txn_list_to_save: {txn_list_to_save}\n")
        await Transaction.bulk_create(db, txn_list_to_save)

        new_ref_data = tx_list[["name_description", "category"]]

        if not os.path.exists(CATEGORY_REFERENCE_OUTPUT_FILE):
            os.makedirs(CATEGORY_REFERENCE_OUTPUT_FILE)

        if os.path.exists(CATEGORY_REFERENCE_OUTPUT_FILE):
            # If it exists, add master file to interim results
            old_ref_data = pd.read_csv(CATEGORY_REFERENCE_OUTPUT_FILE, names=["name_description", "category"], header=0)
            new_ref_data = pd.concat([old_ref_data, new_ref_data], ignore_index=True)
            print(f"save_results new_ref_data: {new_ref_data}\n")
            print(f"save_results old_ref_data: {old_ref_data}\n")

        # Drop duplicates, sort, and write to create new Master File
        new_ref_data.drop_duplicates(subset=["name_description"]).sort_values(by=["name_description"]).to_csv(
            CATEGORY_REFERENCE_OUTPUT_FILE, mode="w", index=False, header=True
        )

        result['old_ref_data'] = old_ref_data
        result['new_ref_data'] = new_ref_data

        print(f"\nsave_results result:\n {result}\n")

        # Summarize results
        # print(f"\nProcessed {len(results)} files: {len(ok_files)} successful, {len(ko_files)} with errors\n")
        logging.debug(f"\nProcessed {len(results)} files: {len(ok_files)} successful, {len(ko_files)} with errors\n")
        if len(ko_files):
            print(f"Errors in the following files:")
            for message in error_messages:
                print(f"  {message}")
            print("\n")

    except Exception as e:
        # Return an error indicator and exception info
        # print(f"standardize_csv_file exception: {e}")
        logging.debug("save_results Error: {e}")
        result["error"] = str(e)
        raise HTTPException(status_code = 401, detail=f"save_results exception: {error_messages}")

    return result