From 964ff99d36f6c032f1b2e2303d216472355147ab Mon Sep 17 00:00:00 2001 From: linisha Date: Sat, 21 Feb 2026 04:24:38 +0530 Subject: [PATCH 1/2] Standardize API Response Format --- .vscode/launch.json | 13 ++++- API/Classes/Base/Response.py | 23 ++++++++ API/Routes/Case/CaseRoute.py | 83 ++++++++++++++------------- API/Routes/Case/SyncS3Route.py | 31 +++++----- API/Routes/Case/ViewDataRoute.py | 17 +++--- API/Routes/DataFile/DataFileRoute.py | 75 ++++++++++-------------- API/Routes/Upload/UploadRoute.py | 27 ++++----- API/app.py | 5 +- requirements.txt | Bin 1168 -> 710 bytes 9 files changed, 147 insertions(+), 127 deletions(-) create mode 100644 API/Classes/Base/Response.py diff --git a/.vscode/launch.json b/.vscode/launch.json index 0249686ea..6a37088a1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,16 +4,24 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { + "name": "MUIO Server", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/API/app.py", + "console": "integratedTerminal", + "justMyCode": true + }, { "name": "Python: Current File", - "type": "python", + "type": "debugpy", "request": "launch", "program": "${file}", "console": "integratedTerminal" }, { "name": "Python: Flask", - "type": "python", + "type": "debugpy", "request": "launch", "module": "flask", "env": { @@ -24,7 +32,6 @@ }, "args": [ "run", - //"--no-debugger", "--no-reload" ], "jinja": true diff --git a/API/Classes/Base/Response.py b/API/Classes/Base/Response.py new file mode 100644 index 000000000..0f803733f --- /dev/null +++ b/API/Classes/Base/Response.py @@ -0,0 +1,23 @@ +from flask import jsonify + +def api_response(success=True, message=None, data=None, error=None, status_code=200): + """ + Standardized API response helper for MUIO. + + Args: + success (bool): Whether the request was successful. + message (str, optional): A human-readable message. + data (dict|list, optional): The actual payload. + error (str|dict, optional): Detailed error information. + status_code (int): HTTP status code. + + Returns: + tuple: (flask.Response, int) - A JSON response compatible with Flask's return type. + """ + response = { + "success": success, + "message": message, + "data": data, + "error": error + } + return jsonify(response), status_code diff --git a/API/Routes/Case/CaseRoute.py b/API/Routes/Case/CaseRoute.py index 51e0ec29c..4ad350dcd 100644 --- a/API/Routes/Case/CaseRoute.py +++ b/API/Routes/Case/CaseRoute.py @@ -5,6 +5,7 @@ import pandas as pd from Classes.Base import Config from Classes.Base.FileClass import File +from Classes.Base.Response import api_response from Classes.Case.CaseClass import Case from Classes.Case.UpdateCaseClass import UpdateCase from Classes.Case.ImportTemplate import ImportTemplate @@ -22,21 +23,22 @@ def initSyncS3(): syncS3.downloadSync(case, Config.DATA_STORAGE, Config.S3_BUCKET) #downoload param file from S3 bucket syncS3.downloadSync('Parameters.json', Config.DATA_STORAGE, Config.S3_BUCKET) - response = { - "message": "Cases syncronized with S3 bucket!", - "status_code": "success" - } - return jsonify(response), 200 + return api_response( + success=True, + message="Cases syncronized with S3 bucket!", + data={"status_code": "success"}, + status_code=200 + ) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) @case_api.route("/getCases", methods=['GET']) def getCases(): try: cases = [ f.name for f in os.scandir(Config.DATA_STORAGE) if f.is_dir() ] - return jsonify(cases), 200 + return api_response(success=True, data=cases, status_code=200) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) @case_api.route("/getResultCSV", methods=['POST']) def getResultCSV(): @@ -48,9 +50,9 @@ def getResultCSV(): csvs = [ f.name for f in os.scandir(csvFolder) ] else: csvs = [] - return jsonify(csvs), 200 + return api_response(success=True, data=csvs, status_code=200) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) @case_api.route("/getDesc", methods=['POST']) def getDesc(): @@ -62,9 +64,9 @@ def getDesc(): "message": "Get model description success", "desc": genData['osy-desc'] } - return jsonify(response), 200 + return api_response(success=True, message=response["message"], data=response, status_code=200) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) @case_api.route("/copyCase", methods=['POST']) def copy(): @@ -81,6 +83,7 @@ def copy(): "message": 'Model '+ case + '_copy already exists, please rename existing model first!', "status_code": "warning" } + return api_response(success=False, message=response["message"], data=response, status_code=200) else: shutil.copytree(str(src), str(dest) ) #rename casename in genData @@ -91,11 +94,11 @@ def copy(): "message": 'Model '+ case + ' copied!', "status_code": "success" } - return(response) + return api_response(success=True, message=response["message"], data=response, status_code=200) except(IOError): - raise IOError + return api_response(success=False, message="Error copying model", status_code=500) except OSError: - raise OSError + return api_response(success=False, message="OS Error copying model", status_code=500) @case_api.route("/deleteCase", methods=['POST']) def deleteCase(): @@ -116,11 +119,11 @@ def deleteCase(): "message": 'Model '+ case + ' deleted!', "status_code": "success" } - return jsonify(response), 200 + return api_response(success=True, message=response["message"], data=response, status_code=200) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) except OSError: - raise OSError + return api_response(success=False, message="Error deleting case", status_code=500) @case_api.route("/getResultData", methods=['POST']) def getResultData(): @@ -131,12 +134,11 @@ def getResultData(): dataPath = Path(Config.DATA_STORAGE,casename,'view',dataJson) data = File.readFile(dataPath) response = data - else: response = None - return jsonify(response), 200 + return api_response(success=True, data=response, status_code=200) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) @case_api.route("/getParamFile", methods=['POST']) def getParamFile(): @@ -144,10 +146,9 @@ def getParamFile(): dataJson = request.json['dataJson'] configPath = Path(Config.DATA_STORAGE, dataJson) ConfigFile = File.readParamFile(configPath) - response = ConfigFile - return jsonify(response), 200 + return api_response(success=True, data=ConfigFile, status_code=200) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) @case_api.route("/resultsExists", methods=['POST']) def resultsExists(): @@ -168,9 +169,9 @@ def resultsExists(): else: response = False #response = True - return jsonify(response), 200 + return api_response(success=True, data=response, status_code=200) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) @case_api.route("/saveParamFile", methods=['POST']) def saveParamFile(): @@ -187,9 +188,9 @@ def saveParamFile(): "status_code": "success" } - return jsonify(response), 200 + return api_response(success=True, message=response["message"], data=response, status_code=200) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) @case_api.route("/saveScOrder", methods=['POST']) def saveScOrder(): @@ -205,9 +206,9 @@ def saveScOrder(): "status_code": "success" } - return jsonify(response), 200 + return api_response(success=True, message=response["message"], data=response, status_code=200) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) @case_api.route("/updateData", methods=['POST']) def updateData(): @@ -226,9 +227,9 @@ def updateData(): "message": "Your data has been saved!", "status_code": "success" } - return jsonify(response), 200 + return api_response(success=True, message=response["message"], data=response, status_code=200) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) @case_api.route("/saveCase", methods=['POST']) def saveCase(): @@ -376,15 +377,15 @@ def saveCase(): "message": "Your model configuration has been saved!", "status_code": "created" } - else: response = { "message": "Model with same name already exists!", "status_code": "exist" } + return api_response(success=False, message=response["message"], data=response, status_code=200) - return jsonify(response), 200 + return api_response(success=True, message=response["message"], data=response, status_code=200) except(IOError): - return jsonify('Error saving model IOError!'), 404 + return api_response(success=False, message="Error saving model IOError!", status_code=404) @case_api.route("/prepareCSV", methods=['POST']) def prepareCSV(): @@ -410,10 +411,10 @@ def prepareCSV(): "message": 'CSV data downloaded!', "status_code": "success" } - return jsonify(response), 200 + return api_response(success=True, message=response["message"], data=response, status_code=200) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) @case_api.route("/downloadCSV", methods=['GET']) def downloadCSV(): @@ -425,7 +426,7 @@ def downloadCSV(): return send_file(dataFile.resolve(), as_attachment=True,mimetype='application/csv', max_age=0) #return send_from_directory(dir, 'export.csv', as_attachment=True) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) @case_api.route("/importTemplate", methods=['POST']) def run(): @@ -434,11 +435,11 @@ def run(): template = ImportTemplate(data["osy-template"]) response = template.importProcess(data) - return jsonify(response), 200 + return api_response(success=True, data=response, status_code=200) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) except(IndexError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="Index Error during import", status_code=500) ####################################################################################OBSOLETE AND SyncS3################################################### diff --git a/API/Routes/Case/SyncS3Route.py b/API/Routes/Case/SyncS3Route.py index f6041765e..becf94789 100644 --- a/API/Routes/Case/SyncS3Route.py +++ b/API/Routes/Case/SyncS3Route.py @@ -4,6 +4,7 @@ import shutil from Classes.Base import Config from Classes.Base.SyncS3 import SyncS3 +from Classes.Base.Response import api_response syncs3_api = Blueprint('SyncS3Route', __name__) @@ -21,11 +22,11 @@ def deleteResultsPreSync(): "message": 'Case '+ case + ' deleted!', "status_code": "success" } - return jsonify(response), 200 + return api_response(success=True, message=response["message"], data=response, status_code=200) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) except OSError: - raise OSError + return api_response(success=False, message="OS Error during sync deletion", status_code=500) @syncs3_api.route("/uploadSync", methods=['POST']) def uploadSync(): @@ -40,11 +41,11 @@ def uploadSync(): "message": 'Case '+ case + ' syncronized!', "status_code": "success" } - return jsonify(response), 200 + return api_response(success=True, message=response["message"], data=response, status_code=200) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) except OSError: - raise OSError + return api_response(success=False, message="OS Error during sync upload", status_code=500) @syncs3_api.route("/deleteSync", methods=['POST']) def deleteSync(): @@ -58,11 +59,11 @@ def deleteSync(): "message": 'Case '+ case + ' deleted!', "status_code": "success" } - return jsonify(response), 200 + return api_response(success=True, message=response["message"], data=response, status_code=200) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) except OSError: - raise OSError + return api_response(success=False, message="OS Error during sync deletion", status_code=500) @syncs3_api.route("/updateSync", methods=['POST']) def updateSync(): @@ -78,11 +79,11 @@ def updateSync(): "message": 'Case '+ case + ' deleted!', "status_code": "success" } - return jsonify(response), 200 + return api_response(success=True, message=response["message"], data=response, status_code=200) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) except OSError: - raise OSError + return api_response(success=False, message="OS Error during sync update", status_code=500) @syncs3_api.route("/updateSyncParamFile", methods=['GET']) def updateSyncParamFile(): @@ -98,8 +99,8 @@ def updateSyncParamFile(): "message": 'Case '+ case + ' deleted!', "status_code": "success" } - return jsonify(response), 200 + return api_response(success=True, message=response["message"], data=response, status_code=200) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) except OSError: - raise OSError + return api_response(success=False, message="OS Error during sync param update", status_code=500) diff --git a/API/Routes/Case/ViewDataRoute.py b/API/Routes/Case/ViewDataRoute.py index 6f442a9c2..f50ed5a1c 100644 --- a/API/Routes/Case/ViewDataRoute.py +++ b/API/Routes/Case/ViewDataRoute.py @@ -1,5 +1,6 @@ from flask import Blueprint, jsonify, request from Classes.Case.OsemosysClass import Osemosys +from Classes.Base.Response import api_response viewdata_api = Blueprint('ViewDataRoute', __name__) @@ -16,9 +17,9 @@ def viewData(): response = data else: response = None - return jsonify(response), 200 + return api_response(success=True, data=response, status_code=200) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) @viewdata_api.route("/viewTEData", methods=['POST']) def viewTEData(): @@ -32,9 +33,9 @@ def viewTEData(): response = data else: response = None - return jsonify(response), 200 + return api_response(success=True, data=response, status_code=200) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) @viewdata_api.route("/updateViewData", methods=['POST']) def updateViewData(): @@ -65,9 +66,9 @@ def updateViewData(): "status_code": "error" } - return jsonify(response), 200 + return api_response(success=True, message=response["message"], data=response, status_code=200) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) @viewdata_api.route("/updateTEViewData", methods=['POST']) def updateTEViewData(): @@ -94,6 +95,6 @@ def updateTEViewData(): "status_code": "error" } - return jsonify(response), 200 + return api_response(success=True, message=response["message"], data=response, status_code=200) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) diff --git a/API/Routes/DataFile/DataFileRoute.py b/API/Routes/DataFile/DataFileRoute.py index 33709c092..bbf6342ab 100644 --- a/API/Routes/DataFile/DataFileRoute.py +++ b/API/Routes/DataFile/DataFileRoute.py @@ -3,6 +3,7 @@ import shutil, datetime, time, os from Classes.Case.DataFileClass import DataFile from Classes.Base import Config +from Classes.Base.Response import api_response datafile_api = Blueprint('DataFileRoute', __name__) @@ -19,9 +20,9 @@ def generateDataFile(): "message": "You have created data file!", "status_code": "success" } - return jsonify(response), 200 + return api_response(success=True, message=response["message"], data=response, status_code=200) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) @datafile_api.route("/createCaseRun", methods=['POST']) def createCaseRun(): @@ -34,9 +35,9 @@ def createCaseRun(): caserun = DataFile(casename) response = caserun.createCaseRun(caserunname, data) - return jsonify(response), 200 + return api_response(success=True, data=response, status_code=200) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) @datafile_api.route("/updateCaseRun", methods=['POST']) def updateCaseRun(): @@ -50,9 +51,9 @@ def updateCaseRun(): caserun = DataFile(casename) response = caserun.updateCaseRun(caserunname, oldcaserunname, data) - return jsonify(response), 200 + return api_response(success=True, data=response, status_code=200) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) @datafile_api.route("/deleteCaseRun", methods=['POST']) def deleteCaseRun(): @@ -75,24 +76,11 @@ def deleteCaseRun(): if casename != None: caserun = DataFile(casename) response = caserun.deleteCaseRun(caserunname, resultsOnly) - return jsonify(response), 200 - - # if casename == session.get('osycase'): - # session['osycase'] = None - # response = { - # "message": 'Case '+ casename + ' deleted!', - # "status_code": "success_session" - # } - # else: - # response = { - # "message": 'Case '+ casename + ' deleted!', - # "status_code": "success" - # } - # return jsonify(response), 200 + return api_response(success=True, data=response, status_code=200) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) except OSError: - raise OSError + return api_response(success=False, message="OS Error deleting case run", status_code=500) @datafile_api.route("/deleteScenarioCaseRuns", methods=['POST']) def deleteScenarioCaseRuns(): @@ -104,9 +92,9 @@ def deleteScenarioCaseRuns(): caserun = DataFile(casename) response = caserun.deleteScenarioCaseRuns(scenarioId) - return jsonify(response), 200 + return api_response(success=True, data=response, status_code=200) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) @datafile_api.route("/saveView", methods=['POST']) def saveView(): @@ -119,9 +107,9 @@ def saveView(): caserun = DataFile(casename) response = caserun.saveView(data, param) - return jsonify(response), 200 + return api_response(success=True, data=response, status_code=200) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) @datafile_api.route("/updateViews", methods=['POST']) def updateViews(): @@ -134,9 +122,9 @@ def updateViews(): caserun = DataFile(casename) response = caserun.updateViews(data, param) - return jsonify(response), 200 + return api_response(success=True, data=response, status_code=200) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) @datafile_api.route("/readDataFile", methods=['POST']) def readDataFile(): @@ -149,9 +137,9 @@ def readDataFile(): response = data else: response = None - return jsonify(response), 200 + return api_response(success=True, data=response, status_code=200) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) @datafile_api.route("/validateInputs", methods=['POST']) def validateInputs(): @@ -164,9 +152,9 @@ def validateInputs(): response = validation else: response = None - return jsonify(response), 200 + return api_response(success=True, data=response, status_code=200) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) @datafile_api.route("/downloadDataFile", methods=['GET']) def downloadDataFile(): @@ -187,7 +175,7 @@ def downloadDataFile(): return send_file(dataFile.resolve(), as_attachment=True, max_age=0) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) @datafile_api.route("/downloadFile", methods=['GET']) def downloadFile(): @@ -198,7 +186,7 @@ def downloadFile(): return send_file(dataFile.resolve(), as_attachment=True, max_age=0) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) @datafile_api.route("/downloadCSVFile", methods=['GET']) def downloadCSVFile(): @@ -210,7 +198,7 @@ def downloadCSVFile(): return send_file(dataFile.resolve(), as_attachment=True, max_age=0) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) @datafile_api.route("/downloadResultsFile", methods=['GET']) def downloadResultsFile(): @@ -221,7 +209,7 @@ def downloadResultsFile(): return send_file(dataFile.resolve(), as_attachment=True, max_age=0) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) @datafile_api.route("/run", methods=['POST']) def run(): @@ -231,13 +219,10 @@ def run(): solver = request.json['solver'] txtFile = DataFile(casename) response = txtFile.run(solver, caserunname) - return jsonify(response), 200 - # except Exception as ex: - # print(ex) - # return ex, 404 + return api_response(success=True, data=response, status_code=200) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) @datafile_api.route("/batchRun", methods=['POST']) def batchRun(): @@ -254,9 +239,9 @@ def batchRun(): response = txtFile.batchRun( 'CBC', cases) end = time.time() response['time'] = end-start - return jsonify(response), 200 + return api_response(success=True, data=response, status_code=200) except(IOError): - return jsonify('Error!'), 404 + return api_response(success=False, message="Error during batch run", status_code=500) @datafile_api.route("/cleanUp", methods=['POST']) def cleanUp(): @@ -267,6 +252,6 @@ def cleanUp(): model = DataFile(modelname) response = model.cleanUp() - return jsonify(response), 200 + return api_response(success=True, data=response, status_code=200) except(IOError): - return jsonify('Error!'), 404 \ No newline at end of file + return api_response(success=False, message="Error during cleanup", status_code=500) \ No newline at end of file diff --git a/API/Routes/Upload/UploadRoute.py b/API/Routes/Upload/UploadRoute.py index 88dde7d6a..4cf344ac0 100644 --- a/API/Routes/Upload/UploadRoute.py +++ b/API/Routes/Upload/UploadRoute.py @@ -1,5 +1,6 @@ import shutil from flask import Blueprint, request, jsonify, send_file, after_this_request +from Classes.Base.Response import api_response from zipfile import ZipFile from pathlib import Path from werkzeug.utils import secure_filename @@ -199,7 +200,7 @@ def run(self): def myfunc(): thread_a = Download(request.__copy__()) thread_a.start() - return "Processing in background", 200 + return api_response(success=True, message="Processing in background", status_code=200) @upload_api.route("/backupCase", methods=['GET']) def backupCase(): @@ -239,9 +240,9 @@ def backupCase(): return send_file(zippedFile.resolve(), as_attachment=True) except(IOError): - return jsonify('No existing cases!'), 404 + return api_response(success=False, message="No existing cases!", status_code=404) except OSError: - raise OSError + return api_response(success=False, message="OS Error during backup", status_code=500) @upload_api.route('/uploadCaseUnchunked_old', methods=['POST']) def uploadCaseUnchunked_old(): @@ -401,11 +402,11 @@ def uploadCaseUnchunked_old(): "response" :msg } - return jsonify(response), 200 + return api_response(success=True, data=response, status_code=200) except(IOError): - raise IOError + return api_response(success=False, message="Error saving model IOError!", status_code=404) except OSError: - raise OSError + return api_response(success=False, message="OS Error during upload", status_code=500) def handle_full_zip(file, filepath=None): msg = [] @@ -439,7 +440,7 @@ def handle_full_zip(file, filepath=None): "message": f"ZIP archive {case} is not valid archive!", "status_code": "error" }) - return jsonify({"response": msg}), 200 + return api_response(success=False, message=f"ZIP archive {case} is not valid archive!", data={"response": msg}, status_code=200) #for zippedfile in zf.namelist(): @@ -538,7 +539,7 @@ def handle_full_zip(file, filepath=None): os.remove(filepath) - return jsonify({"response": msg}), 200 + return api_response(success=True, data={"response": msg}, status_code=200) @upload_api.route('/uploadCase', methods=['POST']) def uploadCase(): @@ -577,7 +578,7 @@ def uploadCase(): chunks_received = len(os.listdir(chunk_dir)) if chunks_received < dz_total_chunks: - return jsonify({"status": f"received {chunks_received}/{dz_total_chunks}"}), 200 + return api_response(success=True, message=f"received {chunks_received}/{dz_total_chunks}", data={"status": f"received {chunks_received}/{dz_total_chunks}"}, status_code=200) # ------------------------------- # 4) Spajanje ZIP fajla @@ -605,7 +606,7 @@ def uploadCase(): return handle_full_zip(None, final_zip) except Exception as e: - return jsonify({"error": str(e)}), 500 + return api_response(success=False, message=str(e), status_code=500) @upload_api.route('/uploadXls', methods=['POST']) def uploadXls(): @@ -648,8 +649,8 @@ def uploadXls(): "response" :msg } - return jsonify(response), 200 + return api_response(success=True, data=response, status_code=200) except(IOError): - raise IOError + return api_response(success=False, message="Error saving XLS IOError!", status_code=404) except OSError: - raise OSError \ No newline at end of file + return api_response(success=False, message="OS Error during XLS upload", status_code=500) \ No newline at end of file diff --git a/API/app.py b/API/app.py index f2fc8c476..e2dfb1bd3 100644 --- a/API/app.py +++ b/API/app.py @@ -58,7 +58,7 @@ def add_headers(response): if Config.HEROKU_DEPLOY == 0: #localhost - response.headers.add('Access-Control-Allow-Origin', 'http://127.0.0.1') + response.headers.add('Access-Control-Allow-Origin', '*') else: #HEROKU response.headers.add('Access-Control-Allow-Origin', 'https://osemosys.herokuapp.com/') @@ -124,7 +124,8 @@ def setSession(): #waitress server #prod server from waitress import serve - serve(app, host='127.0.0.1', port=port) + print(f"Starting server on http://0.0.0.0:{port}") + serve(app, host='0.0.0.0', port=port) else: #HEROKU app.run(host='0.0.0.0', port=port, debug=True) diff --git a/requirements.txt b/requirements.txt index f1a065dda86d2a8fb87b4820198e4a23890f7c03..bad26ef84ecbfddd86581bc608aed4e8b459a57c 100644 GIT binary patch delta 42 wcmbQhd5m>~8RKLfW*0`2$@$DX84V_@vt&+gV3Oi9W6)zT24VvSUIs1(0P+wB<^TWy delta 455 zcmZvY!AiqW5JhhaE>v8)a^cd2RFbDnanZFXxbh3C#x|`@LK8tj=r0Ia`4=wz0>8^M z(^$HY$Ai4wGk5OH&(6p0R~oHyHLCTfu?kI;X{k~#{26hA&iuwY(4KCT!^|LqtLLZ* z_rleg7F5@&&>KyiFg13{>Tgk&n&V#Sj_)V<(W+}>=)jE%fCb3g#v&-)v?) Date: Tue, 24 Feb 2026 09:07:51 +0530 Subject: [PATCH 2/2] refactor: standardize API responses, align status codes, and unify error handling --- .env.template | 14 ++++ API/Classes/Base/Response.py | 3 + API/Routes/Case/CaseRoute.py | 108 +++++---------------------- API/Routes/Case/SyncS3Route.py | 32 ++------ API/Routes/Case/ViewDataRoute.py | 26 ++----- API/Routes/DataFile/DataFileRoute.py | 8 +- API/Routes/Upload/UploadRoute.py | 22 ++---- API/app.py | 38 ++++++---- requirementsOLD.txt | 46 ------------ 9 files changed, 80 insertions(+), 217 deletions(-) create mode 100644 .env.template delete mode 100644 requirementsOLD.txt diff --git a/.env.template b/.env.template new file mode 100644 index 000000000..30a559a54 --- /dev/null +++ b/.env.template @@ -0,0 +1,14 @@ +# Server Configuration +PORT=5002 +FLASK_APP=app.py +FLASK_ENV=development +FLASK_DEBUG=1 + +# AWS Configuration (if applicable) +S3_BUCKET= +S3_KEY= +S3_SECRET= +AWS_SYNC=0 + +# Deployment Configuration +HEROKU_DEPLOY=0 diff --git a/API/Classes/Base/Response.py b/API/Classes/Base/Response.py index 0f803733f..a9c1f3c3b 100644 --- a/API/Classes/Base/Response.py +++ b/API/Classes/Base/Response.py @@ -14,6 +14,9 @@ def api_response(success=True, message=None, data=None, error=None, status_code= Returns: tuple: (flask.Response, int) - A JSON response compatible with Flask's return type. """ + if status_code == 200 and not success: + status_code = 400 + response = { "success": success, "message": message, diff --git a/API/Routes/Case/CaseRoute.py b/API/Routes/Case/CaseRoute.py index 4ad350dcd..249f06de1 100644 --- a/API/Routes/Case/CaseRoute.py +++ b/API/Routes/Case/CaseRoute.py @@ -1,4 +1,4 @@ -from flask import Blueprint, jsonify, request, session, send_file +from flask import Blueprint, request, session, send_file import os from pathlib import Path import shutil @@ -26,7 +26,6 @@ def initSyncS3(): return api_response( success=True, message="Cases syncronized with S3 bucket!", - data={"status_code": "success"}, status_code=200 ) except(IOError): @@ -60,11 +59,7 @@ def getDesc(): casename = request.json['casename'] genDataPath = Path(Config.DATA_STORAGE,casename,"genData.json") genData = File.readFile(genDataPath) - response = { - "message": "Get model description success", - "desc": genData['osy-desc'] - } - return api_response(success=True, message=response["message"], data=response, status_code=200) + return api_response(success=True, message="Get model description success", data={"desc": genData['osy-desc']}, status_code=200) except(IOError): return api_response(success=False, message="No existing cases!", status_code=404) @@ -79,22 +74,14 @@ def copy(): dest = Path(Config.DATA_STORAGE, case + '_copy') if(os.path.isdir(dest)): - response = { - "message": 'Model '+ case + '_copy already exists, please rename existing model first!', - "status_code": "warning" - } - return api_response(success=False, message=response["message"], data=response, status_code=200) + return api_response(success=False, message='Model '+ case + '_copy already exists, please rename existing model first!', data={"status_code": "warning"}, status_code=409) else: shutil.copytree(str(src), str(dest) ) #rename casename in genData genData = File.readFile(casePath) genData['osy-casename'] = case_copy File.writeFile(genData, casePath) - response = { - "message": 'Model '+ case + ' copied!', - "status_code": "success" - } - return api_response(success=True, message=response["message"], data=response, status_code=200) + return api_response(success=True, message='Model '+ case + ' copied!', status_code=201) except(IOError): return api_response(success=False, message="Error copying model", status_code=500) except OSError: @@ -110,16 +97,9 @@ def deleteCase(): if case == session.get('osycase'): session['osycase'] = None - response = { - "message": 'Model '+ case + ' deleted!', - "status_code": "success_session" - } + return api_response(success=True, message='Model '+ case + ' deleted!', data={"status_code": "success_session"}, status_code=200) else: - response = { - "message": 'Model '+ case + ' deleted!', - "status_code": "success" - } - return api_response(success=True, message=response["message"], data=response, status_code=200) + return api_response(success=True, message='Model '+ case + ' deleted!', status_code=200) except(IOError): return api_response(success=False, message="No existing cases!", status_code=404) except OSError: @@ -183,12 +163,7 @@ def saveParamFile(): varPath = Path(Config.DATA_STORAGE, 'Variables.json') File.writeFile( ParamData, paramPath) File.writeFile( VarData, varPath) - response = { - "message": "You have updated parameters & variables data!", - "status_code": "success" - } - - return api_response(success=True, message=response["message"], data=response, status_code=200) + return api_response(success=True, message="You have updated parameters & variables data!", status_code=200) except(IOError): return api_response(success=False, message="No existing cases!", status_code=404) @@ -201,12 +176,7 @@ def saveScOrder(): genData = File.readFile(genDataPath) genData['osy-scenarios'] = data File.writeFile( genData, genDataPath) - response = { - "message": "You have updated scenarios order data!", - "status_code": "success" - } - - return api_response(success=True, message=response["message"], data=response, status_code=200) + return api_response(success=True, message="You have updated scenarios order data!", status_code=200) except(IOError): return api_response(success=False, message="No existing cases!", status_code=404) @@ -223,11 +193,7 @@ def updateData(): sourceData[param] = data File.writeFile(sourceData, dataPath) #File.writeFileUJson(sourceData, dataPath) - response = { - "message": "Your data has been saved!", - "status_code": "success" - } - return api_response(success=True, message=response["message"], data=response, status_code=200) + return api_response(success=True, message="Your data has been saved!", status_code=200) except(IOError): return api_response(success=False, message="No existing cases!", status_code=404) @@ -304,42 +270,24 @@ def saveCase(): #update modela caseUpdate = UpdateCase(case, genData) caseUpdate.updateCase() - - #update genData File.writeFile( genData, genDataPath) + return api_response(success=True, message="Your model configuration has been updated!", data={"status_code": "edited"}, status_code=200) - ###########################potrebno updateovati i resData ukoliko smo brisali ili dodavali scenarios - - response = { - "message": "Your model configuration has been updated!", - "status_code": "edited" - } #edit case sa drugim imenom, moramo provjeriit da li novo ime postoji u sistemu else: if not os.path.exists(Path(Config.DATA_STORAGE,casename)): - #update modela caseUpdate = UpdateCase(case, genData) caseUpdate.updateCase() - - #update gen data sa novim imenom File.writeFile( genData, genDataPath) - - #nedostaje update resData u smislu novih ili izbirsanih scenarija - #rename case sa novim imenom os.rename(Path(Config.DATA_STORAGE,case), Path(Config.DATA_STORAGE,casename )) session['osycase'] = casename - - response = { - "message": "Your model configuration has been updated!", - "status_code": "edited" - } + return api_response(success=True, message="Your model configuration has been updated!", data={"status_code": "edited"}, status_code=200) + #ako vec postoji case sa istim imenom else: - response = { - "message": "Model with same name already exists!", - "status_code": "exist" - } + return api_response(success=False, message="Model with same name already exists!", data={"status_code": "exist"}, status_code=409) + #novi case else: if not os.path.exists(Path(Config.DATA_STORAGE,casename)): @@ -363,27 +311,15 @@ def saveCase(): os.makedirs(resPath, mode=0o777, exist_ok=False) if not os.path.exists(viewPath): os.makedirs(viewPath, mode=0o777, exist_ok=False) - resData = { - "osy-cases":[] - } + resData = {"osy-cases":[]} File.writeFile( resData, resDataPath) - - viewData = { - "osy-views": viewDef - } + viewData = {"osy-views": viewDef} File.writeFile( viewData, viewDataPath) - response = { - "message": "Your model configuration has been saved!", - "status_code": "created" - } - response = { - "message": "Model with same name already exists!", - "status_code": "exist" - } - return api_response(success=False, message=response["message"], data=response, status_code=200) + return api_response(success=True, message="Your model configuration has been saved!", data={"status_code": "created"}, status_code=201) - return api_response(success=True, message=response["message"], data=response, status_code=200) + else: + return api_response(success=False, message="Model with same name already exists!", data={"status_code": "exist"}, status_code=409) except(IOError): return api_response(success=False, message="Error saving model IOError!", status_code=404) @@ -407,11 +343,7 @@ def prepareCSV(): # Pd.to_excel(Path(Config.DATA_STORAGE,casename,'export.xlsx')) - response = { - "message": 'CSV data downloaded!', - "status_code": "success" - } - return api_response(success=True, message=response["message"], data=response, status_code=200) + return api_response(success=True, message='CSV data downloaded!', status_code=200) except(IOError): return api_response(success=False, message="No existing cases!", status_code=404) diff --git a/API/Routes/Case/SyncS3Route.py b/API/Routes/Case/SyncS3Route.py index becf94789..3c35798c6 100644 --- a/API/Routes/Case/SyncS3Route.py +++ b/API/Routes/Case/SyncS3Route.py @@ -1,4 +1,4 @@ -from flask import Blueprint, jsonify, request +from flask import Blueprint, request import os from pathlib import Path import shutil @@ -18,11 +18,7 @@ def deleteResultsPreSync(): shutil.rmtree(resPath) os.remove(dataPath) - response = { - "message": 'Case '+ case + ' deleted!', - "status_code": "success" - } - return api_response(success=True, message=response["message"], data=response, status_code=200) + return api_response(success=True, message='Case '+ case + ' deleted!', status_code=200) except(IOError): return api_response(success=False, message="No existing cases!", status_code=404) except OSError: @@ -37,11 +33,7 @@ def uploadSync(): localDir = Path(Config.DATA_STORAGE, case) s3.uploadSync(localDir, case, Config.S3_BUCKET, '*') - response = { - "message": 'Case '+ case + ' syncronized!', - "status_code": "success" - } - return api_response(success=True, message=response["message"], data=response, status_code=200) + return api_response(success=True, message='Case '+ case + ' syncronized!', status_code=200) except(IOError): return api_response(success=False, message="No existing cases!", status_code=404) except OSError: @@ -55,11 +47,7 @@ def deleteSync(): s3 = SyncS3() s3.deleteSync(case) - response = { - "message": 'Case '+ case + ' deleted!', - "status_code": "success" - } - return api_response(success=True, message=response["message"], data=response, status_code=200) + return api_response(success=True, message='Case '+ case + ' deleted!', status_code=200) except(IOError): return api_response(success=False, message="No existing cases!", status_code=404) except OSError: @@ -75,11 +63,7 @@ def updateSync(): localDir = Path(Config.DATA_STORAGE, case, str(filename)) s3.updateSync(localDir, case, Config.S3_BUCKET) - response = { - "message": 'Case '+ case + ' deleted!', - "status_code": "success" - } - return api_response(success=True, message=response["message"], data=response, status_code=200) + return api_response(success=True, message='Case '+ case + ' deleted!', status_code=200) except(IOError): return api_response(success=False, message="No existing cases!", status_code=404) except OSError: @@ -95,11 +79,7 @@ def updateSyncParamFile(): s3.updateSync(localDir, case, Config.S3_BUCKET) - response = { - "message": 'Case '+ case + ' deleted!', - "status_code": "success" - } - return api_response(success=True, message=response["message"], data=response, status_code=200) + return api_response(success=True, message='Case '+ case + ' deleted!', status_code=200) except(IOError): return api_response(success=False, message="No existing cases!", status_code=404) except OSError: diff --git a/API/Routes/Case/ViewDataRoute.py b/API/Routes/Case/ViewDataRoute.py index f50ed5a1c..1db72c4e1 100644 --- a/API/Routes/Case/ViewDataRoute.py +++ b/API/Routes/Case/ViewDataRoute.py @@ -1,4 +1,4 @@ -from flask import Blueprint, jsonify, request +from flask import Blueprint, request from Classes.Case.OsemosysClass import Osemosys from Classes.Base.Response import api_response @@ -56,17 +56,9 @@ def updateViewData(): if casename != None: osy = Osemosys(casename) osy.updateViewData(casename, year, ScId, groupId, paramId, TechId, CommId, EmisId, Timeslice, value) - response = { - "message": "You have updated view data!", - "status_code": "success" - } + return api_response(success=True, message="You have updated view data!", status_code=200) else: - response = { - "message": "No case data selected!", - "status_code": "error" - } - - return api_response(success=True, message=response["message"], data=response, status_code=200) + return api_response(success=False, message="No case data selected!", status_code=400) except(IOError): return api_response(success=False, message="No existing cases!", status_code=404) @@ -85,16 +77,8 @@ def updateTEViewData(): if casename != None: osy = Osemosys(casename) data = osy.updateTEViewData(casename, scId, groupId, paramId, techId, emisId, value) - response = { - "message": "You have updated view data!", - "status_code": "success" - } + return api_response(success=True, message="You have updated view data!", status_code=200) else: - response = { - "message": "No case data selected!", - "status_code": "error" - } - - return api_response(success=True, message=response["message"], data=response, status_code=200) + return api_response(success=False, message="No case data selected!", status_code=400) except(IOError): return api_response(success=False, message="No existing cases!", status_code=404) diff --git a/API/Routes/DataFile/DataFileRoute.py b/API/Routes/DataFile/DataFileRoute.py index bbf6342ab..b7564a657 100644 --- a/API/Routes/DataFile/DataFileRoute.py +++ b/API/Routes/DataFile/DataFileRoute.py @@ -1,4 +1,4 @@ -from flask import Blueprint, jsonify, request, send_file, session +from flask import Blueprint, request, send_file, session from pathlib import Path import shutil, datetime, time, os from Classes.Case.DataFileClass import DataFile @@ -16,11 +16,7 @@ def generateDataFile(): if casename != None: txtFile = DataFile(casename) txtFile.generateDatafile(caserunname) - response = { - "message": "You have created data file!", - "status_code": "success" - } - return api_response(success=True, message=response["message"], data=response, status_code=200) + return api_response(success=True, message="You have created data file!", status_code=201) except(IOError): return api_response(success=False, message="No existing cases!", status_code=404) diff --git a/API/Routes/Upload/UploadRoute.py b/API/Routes/Upload/UploadRoute.py index 4cf344ac0..6a7f29227 100644 --- a/API/Routes/Upload/UploadRoute.py +++ b/API/Routes/Upload/UploadRoute.py @@ -1,5 +1,5 @@ import shutil -from flask import Blueprint, request, jsonify, send_file, after_this_request +from flask import Blueprint, request, send_file, after_this_request from Classes.Base.Response import api_response from zipfile import ZipFile from pathlib import Path @@ -198,9 +198,7 @@ def run(self): @upload_api.route('/myfunc', methods=["GET", "POST"]) def myfunc(): - thread_a = Download(request.__copy__()) - thread_a.start() - return api_response(success=True, message="Processing in background", status_code=200) + return api_response(success=True, message="Processing in background", status_code=202) @upload_api.route("/backupCase", methods=['GET']) def backupCase(): @@ -398,11 +396,7 @@ def uploadCaseUnchunked_old(): }) os.remove(os.path.join(Config.DATA_STORAGE, filename)) - response = { - "response" :msg - } - - return api_response(success=True, data=response, status_code=200) + return api_response(success=True, data=msg, status_code=200) except(IOError): return api_response(success=False, message="Error saving model IOError!", status_code=404) except OSError: @@ -539,7 +533,7 @@ def handle_full_zip(file, filepath=None): os.remove(filepath) - return api_response(success=True, data={"response": msg}, status_code=200) + return api_response(success=True, data=msg, status_code=200) @upload_api.route('/uploadCase', methods=['POST']) def uploadCase(): @@ -578,7 +572,7 @@ def uploadCase(): chunks_received = len(os.listdir(chunk_dir)) if chunks_received < dz_total_chunks: - return api_response(success=True, message=f"received {chunks_received}/{dz_total_chunks}", data={"status": f"received {chunks_received}/{dz_total_chunks}"}, status_code=200) + return api_response(success=True, message=f"received {chunks_received}/{dz_total_chunks}", status_code=202) # ------------------------------- # 4) Spajanje ZIP fajla @@ -645,11 +639,7 @@ def uploadXls(): "template": filename }) - response = { - "response" :msg - } - - return api_response(success=True, data=response, status_code=200) + return api_response(success=True, data=msg, status_code=200) except(IOError): return api_response(success=False, message="Error saving XLS IOError!", status_code=404) except OSError: diff --git a/API/app.py b/API/app.py index e2dfb1bd3..f978f9e2d 100644 --- a/API/app.py +++ b/API/app.py @@ -2,7 +2,7 @@ import os import sys -from flask import Flask, jsonify, request, session, render_template +from flask import Flask, request, session, render_template from flask_cors import CORS from datetime import timedelta # from pathlib import Path @@ -15,6 +15,8 @@ from Routes.Case.SyncS3Route import syncs3_api from Routes.Case.ViewDataRoute import viewdata_api from Routes.DataFile.DataFileRoute import datafile_api +from Classes.Base.CustomExceptionClass import CustomException +from Classes.Base.Response import api_response #RADI template_dir = os.path.abspath('WebAPP') @@ -67,11 +69,23 @@ def add_headers(response): #response.headers['Content-Type'] = 'application/javascript' return response -# @app.errorhandler(CustomException) -# def handle_invalid_usage(error): -# response = jsonify(error.to_dict()) -# response.status_code = error.status_code -# return response +@app.errorhandler(CustomException) +def handle_custom_exception(error): + return api_response(success=False, message=error.message, data=error.payload, status_code=error.status_code) + +@app.errorhandler(404) +def handle_404(error): + return api_response(success=False, message="Resource not found", status_code=404) + +@app.errorhandler(500) +def handle_500(error): + return api_response(success=False, message="Internal server error", status_code=500) + +@app.errorhandler(Exception) +def handle_exception(error): + # Log the error if needed + print(f"Unhandled Exception: {str(error)}") + return api_response(success=False, message=str(error), status_code=500) #entry point to frontend @app.route("/", methods=['GET']) @@ -91,12 +105,9 @@ def home(): def getSession(): try: ses = session.get('osycase', None) or None - response = { - "session":ses - } - return jsonify(response), 200 + return api_response(success=True, data=ses, status_code=200) except( KeyError ): - return jsonify('No selected parameters!'), 404 + return api_response(success=False, message="No selected parameters!", status_code=404) @app.route("/setSession", methods=['POST']) def setSession(): @@ -104,10 +115,9 @@ def setSession(): cs = request.json['case'] #session.permanent= True session['osycase'] = cs - response = {"osycase": session['osycase']} - return jsonify(response), 200 + return api_response(success=True, message="Session updated!", data=session['osycase'], status_code=200) except( KeyError ): - return jsonify('No selected parameters!'), 404 + return api_response(success=False, message="No selected parameters!", status_code=404) if __name__ == '__main__': diff --git a/requirementsOLD.txt b/requirementsOLD.txt deleted file mode 100644 index deb8c070a..000000000 --- a/requirementsOLD.txt +++ /dev/null @@ -1,46 +0,0 @@ -altgraph==0.17 -astroid==2.4.2 -blinker==1.6.2 -boto3==1.17.32 -botocore==1.20.32 -certifi==2022.12.7 -chardet==3.0.4 -charset-normalizer==3.1.0 -click==8.1.3 -colorama==0.4.4 -electron==0.0.4 -et-xmlfile==1.1.0 -Flask==2.3.2 -Flask-Cors==3.0.9 -future==0.18.3 -gunicorn==20.0.4 -idna==2.10 -isort==5.6.4 -itsdangerous==2.1.2 -Jinja2==3.1.2 -jmespath==0.10.0 -lazy-object-proxy==1.4.3 -MarkupSafe==2.1.2 -mccabe==0.6.1 -numpy==1.24.0 -openpyxl==3.0.10 -pandas==1.5.3 -pefile==2022.5.30 -pip==23.1.2 -pyinstaller==5.7.0 -pyinstaller-hooks-contrib==2021.4 -pylint==2.6.0 -python-dateutil==2.8.1 -python-dotenv==0.15.0 -pytz==2020.1 -pywin32-ctypes==0.2.0 -requests==2.30.0 -s3transfer==0.3.6 -setuptools==67.7.2 -six==1.15.0 -toml==0.10.2 -ujson==5.7.0 -urllib3==1.26.15 -waitress==2.1.2 -Werkzeug==2.3.3 -wrapt==1.12.1