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/.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..a9c1f3c3b --- /dev/null +++ b/API/Classes/Base/Response.py @@ -0,0 +1,26 @@ +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. + """ + if status_code == 200 and not success: + status_code = 400 + + 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..249f06de1 100644 --- a/API/Routes/Case/CaseRoute.py +++ b/API/Routes/Case/CaseRoute.py @@ -1,10 +1,11 @@ -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 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,21 @@ 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!", + 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 +49,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(): @@ -58,13 +59,9 @@ 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 jsonify(response), 200 + return api_response(success=True, message="Get model description success", data={"desc": genData['osy-desc']}, 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(): @@ -77,25 +74,18 @@ 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='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(response) + return api_response(success=True, message='Model '+ case + ' copied!', status_code=201) 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(): @@ -107,20 +97,13 @@ 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 jsonify(response), 200 + return api_response(success=True, message='Model '+ case + ' deleted!', 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 +114,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 +126,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 +149,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(): @@ -182,14 +163,9 @@ 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 jsonify(response), 200 + return api_response(success=True, message="You have updated parameters & variables data!", 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(): @@ -200,14 +176,9 @@ 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 jsonify(response), 200 + return api_response(success=True, message="You have updated scenarios order data!", 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(): @@ -222,13 +193,9 @@ def updateData(): sourceData[param] = data File.writeFile(sourceData, dataPath) #File.writeFileUJson(sourceData, dataPath) - response = { - "message": "Your data has been saved!", - "status_code": "success" - } - return jsonify(response), 200 + return api_response(success=True, message="Your data has been saved!", 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(): @@ -303,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)): @@ -362,29 +311,17 @@ 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" - } - else: - response = { - "message": "Model with same name already exists!", - "status_code": "exist" - } + return api_response(success=True, message="Your model configuration has been saved!", data={"status_code": "created"}, status_code=201) - return jsonify(response), 200 + else: + return api_response(success=False, message="Model with same name already exists!", data={"status_code": "exist"}, status_code=409) 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(): @@ -406,14 +343,10 @@ def prepareCSV(): # Pd.to_excel(Path(Config.DATA_STORAGE,casename,'export.xlsx')) - response = { - "message": 'CSV data downloaded!', - "status_code": "success" - } - return jsonify(response), 200 + return api_response(success=True, message='CSV data downloaded!', 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 +358,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 +367,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..3c35798c6 100644 --- a/API/Routes/Case/SyncS3Route.py +++ b/API/Routes/Case/SyncS3Route.py @@ -1,9 +1,10 @@ -from flask import Blueprint, jsonify, request +from flask import Blueprint, request import os from pathlib import Path 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__) @@ -17,15 +18,11 @@ def deleteResultsPreSync(): shutil.rmtree(resPath) os.remove(dataPath) - response = { - "message": 'Case '+ case + ' deleted!', - "status_code": "success" - } - return jsonify(response), 200 + return api_response(success=True, message='Case '+ case + ' deleted!', 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(): @@ -36,15 +33,11 @@ def uploadSync(): localDir = Path(Config.DATA_STORAGE, case) s3.uploadSync(localDir, case, Config.S3_BUCKET, '*') - response = { - "message": 'Case '+ case + ' syncronized!', - "status_code": "success" - } - return jsonify(response), 200 + return api_response(success=True, message='Case '+ case + ' syncronized!', 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(): @@ -54,15 +47,11 @@ def deleteSync(): s3 = SyncS3() s3.deleteSync(case) - response = { - "message": 'Case '+ case + ' deleted!', - "status_code": "success" - } - return jsonify(response), 200 + return api_response(success=True, message='Case '+ case + ' deleted!', 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(): @@ -74,15 +63,11 @@ 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 jsonify(response), 200 + return api_response(success=True, message='Case '+ case + ' deleted!', 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(): @@ -94,12 +79,8 @@ def updateSyncParamFile(): s3.updateSync(localDir, case, Config.S3_BUCKET) - response = { - "message": 'Case '+ case + ' deleted!', - "status_code": "success" - } - return jsonify(response), 200 + return api_response(success=True, message='Case '+ case + ' deleted!', 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..1db72c4e1 100644 --- a/API/Routes/Case/ViewDataRoute.py +++ b/API/Routes/Case/ViewDataRoute.py @@ -1,5 +1,6 @@ -from flask import Blueprint, jsonify, request +from flask import Blueprint, 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(): @@ -55,19 +56,11 @@ 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 jsonify(response), 200 + return api_response(success=False, message="No case data selected!", status_code=400) 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(): @@ -84,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 jsonify(response), 200 + return api_response(success=False, message="No case data selected!", status_code=400) 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..b7564a657 100644 --- a/API/Routes/DataFile/DataFileRoute.py +++ b/API/Routes/DataFile/DataFileRoute.py @@ -1,8 +1,9 @@ -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 from Classes.Base import Config +from Classes.Base.Response import api_response datafile_api = Blueprint('DataFileRoute', __name__) @@ -15,13 +16,9 @@ def generateDataFile(): if casename != None: txtFile = DataFile(casename) txtFile.generateDatafile(caserunname) - response = { - "message": "You have created data file!", - "status_code": "success" - } - return jsonify(response), 200 + return api_response(success=True, message="You have created data file!", status_code=201) 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 +31,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 +47,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 +72,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 +88,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 +103,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 +118,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 +133,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 +148,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 +171,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 +182,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 +194,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 +205,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 +215,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 +235,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 +248,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..6a7f29227 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 flask import Blueprint, request, 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 @@ -197,9 +198,7 @@ def run(self): @upload_api.route('/myfunc', methods=["GET", "POST"]) 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=202) @upload_api.route("/backupCase", methods=['GET']) def backupCase(): @@ -239,9 +238,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(): @@ -397,15 +396,11 @@ def uploadCaseUnchunked_old(): }) os.remove(os.path.join(Config.DATA_STORAGE, filename)) - response = { - "response" :msg - } - - return jsonify(response), 200 + return api_response(success=True, data=msg, 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 +434,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 +533,7 @@ def handle_full_zip(file, filepath=None): os.remove(filepath) - return jsonify({"response": msg}), 200 + return api_response(success=True, data=msg, status_code=200) @upload_api.route('/uploadCase', methods=['POST']) def uploadCase(): @@ -577,7 +572,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}", status_code=202) # ------------------------------- # 4) Spajanje ZIP fajla @@ -605,7 +600,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(): @@ -644,12 +639,8 @@ def uploadXls(): "template": filename }) - response = { - "response" :msg - } - - return jsonify(response), 200 + return api_response(success=True, data=msg, 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..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') @@ -58,7 +60,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/') @@ -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__': @@ -124,7 +134,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 f1a065dda..bad26ef84 100644 Binary files a/requirements.txt and b/requirements.txt differ 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