From 4a4b0128bfa1cdfa2c69d2a269553f49eb59e07f Mon Sep 17 00:00:00 2001 From: amit-62 Date: Thu, 18 Aug 2022 23:02:25 +0530 Subject: [PATCH 1/4] updated api methods --- fri/README.md | 6 ++++++ fri/server/main.py | 40 ++++++++++++++++++++++++++++------------ 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/fri/README.md b/fri/README.md index e0bae61..e3c654b 100644 --- a/fri/README.md +++ b/fri/README.md @@ -2,6 +2,12 @@ The Control-Core File Receiving Interface (FRI) is built with is Python-3.10. It is the core component that makes the distributed executions a reality in the Control-Core framework. +# Install Dependencies + +Install Jupyter lab +```` +$ pip install jupyterlab +```` # Building FRI Container diff --git a/fri/server/main.py b/fri/server/main.py index 1c6577d..ecf897a 100644 --- a/fri/server/main.py +++ b/fri/server/main.py @@ -26,13 +26,18 @@ def upload(dir): errors = {} success = False - if not os.path.exists(secure_filename(dirname)): - os.makedirs(secure_filename(dirname)) + cur_path = os.path.dirname(os.path.abspath(__file__)) + concore_path = os.path.abspath(os.path.join(cur_path, '../../')) + directory_name = os.path.abspath(os.path.join(concore_path, secure_filename(dirname))) + + if not os.path.isdir(directory_name): + os.mkdir(directory_name) + for file in files: if file: filename = secure_filename(file.filename) - file.save(secure_filename(dirname)+"/"+filename) + file.save(directory_name+"/"+filename) success = True if success and errors: @@ -95,9 +100,11 @@ def execute(dir): # to download /build/?fetch=. For example, /build/test?fetch=sample1 @app.route('/build/', methods=['POST']) def build(dir): - graphml_file = request.args.get('fetch') - makestudy_dir = dir+ "/" + graphml_file #for makestudy - cur_path = os.getcwd() + graphml_file = request.args.get('fetch') + apikey = request.args.get('apikey') + dirname = dir + "_" + apikey + makestudy_dir = dirname + "/" + graphml_file #for makestudy + cur_path = os.path.dirname(os.path.abspath(__file__)) concore_path = os.path.abspath(os.path.join(cur_path, '../../')) dir_path = os.path.abspath(os.path.join(concore_path, graphml_file)) #path for ./build if not os.path.exists(secure_filename(dir_path)): @@ -114,12 +121,12 @@ def build(dir): @app.route('/debug/', methods=['POST']) def debug(dir): - cur_path = os.getcwd() + cur_path = os.path.dirname(os.path.abspath(__file__)) concore_path = os.path.abspath(os.path.join(cur_path, '../../')) dir_path = os.path.abspath(os.path.join(concore_path, dir)) proc = call(["./debug"], cwd=dir_path) if(proc == 0): - resp = jsonify({'message': 'Close the pop window after obtaing result'}) + resp = jsonify({'message': 'Close the pop window after obtaining result'}) resp.status_code = 201 return resp else: @@ -134,13 +141,18 @@ def download(dir): apikey = request.args.get('apikey') dirname = dir + "_" + apikey + cur_path = os.path.dirname(os.path.abspath(__file__)) + concore_path = os.path.abspath(os.path.join(cur_path, '../../')) + directory_name = os.path.abspath(os.path.join(concore_path, secure_filename(dirname))) + + if not os.path.exists(secure_filename(dirname)): resp = jsonify({'message': 'Directory not found'}) resp.status_code = 400 return resp try: - return send_from_directory(secure_filename(dirname), download_file, as_attachment=True) + return send_from_directory(directory_name, download_file, as_attachment=True) except: resp = jsonify({'message': 'file not found'}) resp.status_code = 400 @@ -149,7 +161,8 @@ def download(dir): @app.route('/destroy/', methods=['DELETE']) def destroy(dir): - cur_path = os.getcwd() + # cur_path = os.getcwd() + cur_path = os.path.dirname(os.path.abspath(__file__)) concore_path = os.path.abspath(os.path.join(cur_path, '../../')) proc = call(["./destroy", dir], cwd=concore_path) if(proc == 0): @@ -163,7 +176,8 @@ def destroy(dir): @app.route('/getFilesList/', methods=['POST']) def getFilesList(dir): - cur_path = os.getcwd() + # cur_path = os.getcwd() + cur_path = os.path.dirname(os.path.abspath(__file__)) concore_path = os.path.abspath(os.path.join(cur_path, '../../')) dir_path = os.path.abspath(os.path.join(concore_path, dir)) res = [] @@ -174,7 +188,9 @@ def getFilesList(dir): @app.route('/openJupyter/', methods=['POST']) def openJupyter(): - cur_path = os.getcwd() + # cur_path = os.getcwd() + cur_path = os.path.dirname(os.path.abspath(__file__)) + print(cur_path) concore_path = os.path.abspath(os.path.join(cur_path, '../../')) proc = subprocess.Popen(['jupyter', 'lab'], shell=False, stdout=subprocess.PIPE, cwd=concore_path) if proc.poll() is None: From 27442fe16ccb2c84f925c340237a9d783463da3a Mon Sep 17 00:00:00 2001 From: amit-62 Date: Fri, 19 Aug 2022 00:07:27 +0530 Subject: [PATCH 2/4] updated test --- fri/test.py | 59 +++++++++++++++++++++++++++++------------------------ 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/fri/test.py b/fri/test.py index fab03e7..8ce3f56 100644 --- a/fri/test.py +++ b/fri/test.py @@ -1,17 +1,17 @@ +from cgi import test import requests import os import urllib.request # function to test upload() method. -def upload(): - url = "http://127.0.0.1:5000/upload/test?apikey=xyz" - path = os.path.abspath("example.py") +def upload(files): + url = "http://127.0.0.1:5000/upload/test?apikey=xyz" payload={} - files=[ - ('files[]',('example.py',open(path,'rb'),'application/octet-stream')) - ] + # files=[ + # ('files[]',('example.py',open(path,'rb'),'application/octet-stream')) + # ] headers = {} response = requests.request("POST", url, headers=headers, data=payload, files=files) @@ -37,34 +37,28 @@ def execute(): print(response.text) -# function to test download() method. -def download(): - url = "http://127.0.0.1:5000/download/test?fetch=f1.txt&apikey=xyz" - urllib.request.urlretrieve(url, "f1.txt") - - # function to check build -def build(): - url = "http://127.0.0.1:5000/build/test?fetch=sample1" +def build(dir, graphml, apikey): + url = "http://127.0.0.1:5000/build/"+dir+"?"+"fetch="+graphml+"&"+"apikey="+apikey response = requests.request("POST", url) print(response.text) # function to debug -def debug(): - url = "http://127.0.0.1:5000/debug/sample1" +def debug(graphml): + url = "http://127.0.0.1:5000/debug/"+graphml response = requests.request("POST", url) print(response.text) #function to destroy dir. -def destroy(): - url = "http://127.0.0.1:5000/destroy/sample1" +def destroy(dir): + url = "http://127.0.0.1:5000/destroy/" + dir response = requests.request("DELETE", url) print(response.text) -def getFilesList(): - url = "http://127.0.0.1:5000/getFilesList/test" +def getFilesList(dir): + url = "http://127.0.0.1:5000/getFilesList/" + dir response = requests.request("POST", url) print(response.text) @@ -73,13 +67,24 @@ def openJupyter(): response = requests.request("POST", url) print(response.text) +# function to test download() method. +def download(): + url = "http://127.0.0.1:5000/download/test?fetch=f1.txt&apikey=xyz" + urllib.request.urlretrieve(url, "f1.txt") + +# file list to be uploaded +files=[ + #('files[]',(file_name,open(file_path,'rb'),'application/octet-stream')) + +] -# upload() -# execute() -# download() -# build() -# debug() -# destroy() -getFilesList() + +upload(files) +execute() +build("test", "sample1", "xyz") +debug("sample1") +destroy("sample1") +# getFilesList("fri") # openJupyter() +# download() From 8e53f0a7ab4f5072afbd621e07adc8fc3fe0e4ad Mon Sep 17 00:00:00 2001 From: amit-62 Date: Wed, 24 Aug 2022 23:53:39 +0530 Subject: [PATCH 3/4] added clear and stop method --- fri/server/main.py | 106 +++++++++++++++++++-------------------------- fri/test.py | 51 +++++++++++++--------- 2 files changed, 75 insertions(+), 82 deletions(-) diff --git a/fri/server/main.py b/fri/server/main.py index ecf897a..23d9cb6 100644 --- a/fri/server/main.py +++ b/fri/server/main.py @@ -6,6 +6,9 @@ import json import subprocess +cur_path = os.path.dirname(os.path.abspath(__file__)) +concore_path = os.path.abspath(os.path.join(cur_path, '../../')) + app = Flask(__name__) app.secret_key = "secret key" @@ -26,8 +29,6 @@ def upload(dir): errors = {} success = False - cur_path = os.path.dirname(os.path.abspath(__file__)) - concore_path = os.path.abspath(os.path.join(cur_path, '../../')) directory_name = os.path.abspath(os.path.join(concore_path, secure_filename(dirname))) if not os.path.isdir(directory_name): @@ -54,58 +55,15 @@ def upload(dir): resp.status_code = 500 return resp -# To execute any python file. For example, /execute/test?apikey=xyz -@app.route('/execute/', methods=['POST']) -def execute(dir): - apikey = request.args.get('apikey') - dirname = dir + "_" + apikey - - if 'file' not in request.files: - resp = jsonify({'message': 'No file in the request'}) - resp.status_code = 400 - return resp - - file = request.files['file'] - - if file.filename == '': - resp = jsonify({'message': 'No file selected for Executing'}) - resp.status_code = 400 - return resp - - errors = {} - success = False - - if not os.path.exists(secure_filename(dirname)): - os.makedirs(secure_filename(dirname)) - - if file: - filename = secure_filename(file.filename) - file.save(secure_filename(dirname)+"/"+filename) - output_filename = filename + ".out" - file_path = secure_filename(dirname) + "/"+filename - outputfile_path = secure_filename(dirname)+"/"+output_filename - f = open(outputfile_path, "w") - call(["nohup", "python3", file_path], stdout=f) - success = True - if success: - resp = jsonify({'message': 'Files successfully executed'}) - resp.status_code = 201 - return resp - else: - resp = jsonify(errors) - resp.status_code = 500 - return resp -# to download /build/?fetch=. For example, /build/test?fetch=sample1 +# to download /build/?fetch=. For example, /build/test?fetch=sample1&apikey=xyz @app.route('/build/', methods=['POST']) def build(dir): graphml_file = request.args.get('fetch') apikey = request.args.get('apikey') dirname = dir + "_" + apikey makestudy_dir = dirname + "/" + graphml_file #for makestudy - cur_path = os.path.dirname(os.path.abspath(__file__)) - concore_path = os.path.abspath(os.path.join(cur_path, '../../')) dir_path = os.path.abspath(os.path.join(concore_path, graphml_file)) #path for ./build if not os.path.exists(secure_filename(dir_path)): proc = call(["./makestudy", makestudy_dir], cwd=concore_path) @@ -121,8 +79,6 @@ def build(dir): @app.route('/debug/', methods=['POST']) def debug(dir): - cur_path = os.path.dirname(os.path.abspath(__file__)) - concore_path = os.path.abspath(os.path.join(cur_path, '../../')) dir_path = os.path.abspath(os.path.join(concore_path, dir)) proc = call(["./debug"], cwd=dir_path) if(proc == 0): @@ -134,15 +90,53 @@ def debug(dir): resp.status_code = 500 return resp + +@app.route('/run/', methods=['POST']) +def run(dir): + dir_path = os.path.abspath(os.path.join(concore_path, dir)) + proc = call(["./run"], cwd=dir_path) + if(proc == 0): + resp = jsonify({'message': 'result prepared'}) + resp.status_code = 201 + return resp + else: + resp = jsonify({'message': 'There is an Error'}) + resp.status_code = 500 + return resp + +@app.route('/stop/', methods=['POST']) +def stop(dir): + dir_path = os.path.abspath(os.path.join(concore_path, dir)) + proc = call(["./stop"], cwd=dir_path) + if(proc == 0): + resp = jsonify({'message': 'resources cleaned'}) + resp.status_code = 201 + return resp + else: + resp = jsonify({'message': 'There is an Error'}) + resp.status_code = 500 + return resp + + +@app.route('/clear/', methods=['POST']) +def clear(dir): + dir_path = os.path.abspath(os.path.join(concore_path, dir)) + proc = call(["./clear"], cwd=dir_path) + if(proc == 0): + resp = jsonify({'message': 'result deleted'}) + resp.status_code = 201 + return resp + else: + resp = jsonify({'message': 'There is an Error'}) + resp.status_code = 500 + return resp + # to download /download/?fetch=. For example, /download/test?fetch=example.py.out&apikey=xyz @app.route('/download/', methods=['POST', 'GET']) def download(dir): download_file = request.args.get('fetch') apikey = request.args.get('apikey') dirname = dir + "_" + apikey - - cur_path = os.path.dirname(os.path.abspath(__file__)) - concore_path = os.path.abspath(os.path.join(cur_path, '../../')) directory_name = os.path.abspath(os.path.join(concore_path, secure_filename(dirname))) @@ -161,9 +155,6 @@ def download(dir): @app.route('/destroy/', methods=['DELETE']) def destroy(dir): - # cur_path = os.getcwd() - cur_path = os.path.dirname(os.path.abspath(__file__)) - concore_path = os.path.abspath(os.path.join(cur_path, '../../')) proc = call(["./destroy", dir], cwd=concore_path) if(proc == 0): resp = jsonify({'message': 'Successfuly deleted Dirctory'}) @@ -176,9 +167,6 @@ def destroy(dir): @app.route('/getFilesList/', methods=['POST']) def getFilesList(dir): - # cur_path = os.getcwd() - cur_path = os.path.dirname(os.path.abspath(__file__)) - concore_path = os.path.abspath(os.path.join(cur_path, '../../')) dir_path = os.path.abspath(os.path.join(concore_path, dir)) res = [] res = os.listdir(dir_path) @@ -188,10 +176,6 @@ def getFilesList(dir): @app.route('/openJupyter/', methods=['POST']) def openJupyter(): - # cur_path = os.getcwd() - cur_path = os.path.dirname(os.path.abspath(__file__)) - print(cur_path) - concore_path = os.path.abspath(os.path.join(cur_path, '../../')) proc = subprocess.Popen(['jupyter', 'lab'], shell=False, stdout=subprocess.PIPE, cwd=concore_path) if proc.poll() is None: resp = jsonify({'message': 'Successfuly opened Jupyter'}) diff --git a/fri/test.py b/fri/test.py index 8ce3f56..bfb5571 100644 --- a/fri/test.py +++ b/fri/test.py @@ -2,6 +2,7 @@ import requests import os import urllib.request +import time # function to test upload() method. @@ -21,21 +22,7 @@ def upload(files): # # ******* -# function to test execute() method. -def execute(): - url = "http://127.0.0.1:5000/execute/test?apikey=xyz" - path = os.path.abspath("example.py") - - payload={} - files=[ - ('file',('example.py',open(path,'rb'),'application/octet-stream')) - ] - headers = {} - - response = requests.request("POST", url, headers=headers, data=payload, files=files) - - print(response.text) # function to check build def build(dir, graphml, apikey): @@ -49,6 +36,22 @@ def debug(graphml): response = requests.request("POST", url) print(response.text) +# function to test run() method. +def run(graphml): + url = "http://127.0.0.1:5000/run/"+graphml + response = requests.request("POST", url) + print(response.text) + +def clear(graphml): + url = "http://127.0.0.1:5000/clear/"+graphml + response = requests.request("POST", url) + print(response.text) + +def stop(graphml): + url = "http://127.0.0.1:5000/stop/"+graphml + response = requests.request("POST", url) + print(response.text) + #function to destroy dir. def destroy(dir): @@ -75,16 +78,22 @@ def download(): # file list to be uploaded files=[ #('files[]',(file_name,open(file_path,'rb'),'application/octet-stream')) - + ('files[]',('controller.py',open('/home/amit/Desktop/test_xyz/controller.py','rb'),'application/octet-stream')), + ('files[]',('pm.py',open('/home/amit/Desktop/test_xyz/pm.py','rb'),'application/octet-stream')), + ('files[]',('sample1.graphml',open('/home/amit/Desktop/test_xyz/sample1.graphml','rb'),'application/octet-stream')), + # ('files[]',('example.py',open('/home/amit/Desktop/fri/example.py','rb'),'application/octet-stream')) ] -upload(files) -execute() -build("test", "sample1", "xyz") -debug("sample1") -destroy("sample1") -# getFilesList("fri") +# upload(files) +# build("test", "sample1", "xyz") +# time.sleep(6) +# debug("sample1") +# run("sample1") +# clear("sample1") +# stop("sample1") +# getFilesList("sample1") +# destroy("sample1") # openJupyter() # download() From 2d08262b97d1b853ecb5b4485954192c5adb6c67 Mon Sep 17 00:00:00 2001 From: amit-62 Date: Thu, 1 Sep 2022 23:00:35 +0530 Subject: [PATCH 4/4] updated upload method --- fri/server/main.py | 28 +++++++++++--------- fri/test.py | 64 ++++++++++++++++++++++++++-------------------- 2 files changed, 52 insertions(+), 40 deletions(-) diff --git a/fri/server/main.py b/fri/server/main.py index 23d9cb6..e546761 100644 --- a/fri/server/main.py +++ b/fri/server/main.py @@ -17,7 +17,7 @@ @app.route('/upload/', methods=['POST']) def upload(dir): apikey = request.args.get('apikey') - dirname = dir + "_" + apikey + dirname = secure_filename(dir) + "_" + apikey if 'files[]' not in request.files: resp = jsonify({'message': 'No file in the request'}) @@ -62,10 +62,10 @@ def upload(dir): def build(dir): graphml_file = request.args.get('fetch') apikey = request.args.get('apikey') - dirname = dir + "_" + apikey + dirname = secure_filename(dir) + "_" + apikey makestudy_dir = dirname + "/" + graphml_file #for makestudy dir_path = os.path.abspath(os.path.join(concore_path, graphml_file)) #path for ./build - if not os.path.exists(secure_filename(dir_path)): + if not os.path.exists(dir_path): proc = call(["./makestudy", makestudy_dir], cwd=concore_path) if(proc == 0): resp = jsonify({'message': 'Directory successfully created'}) @@ -79,6 +79,7 @@ def build(dir): @app.route('/debug/', methods=['POST']) def debug(dir): + dir = secure_filename(dir) dir_path = os.path.abspath(os.path.join(concore_path, dir)) proc = call(["./debug"], cwd=dir_path) if(proc == 0): @@ -93,6 +94,7 @@ def debug(dir): @app.route('/run/', methods=['POST']) def run(dir): + dir = secure_filename(dir) dir_path = os.path.abspath(os.path.join(concore_path, dir)) proc = call(["./run"], cwd=dir_path) if(proc == 0): @@ -106,6 +108,7 @@ def run(dir): @app.route('/stop/', methods=['POST']) def stop(dir): + dir = secure_filename(dir) dir_path = os.path.abspath(os.path.join(concore_path, dir)) proc = call(["./stop"], cwd=dir_path) if(proc == 0): @@ -120,6 +123,7 @@ def stop(dir): @app.route('/clear/', methods=['POST']) def clear(dir): + dir = secure_filename(dir) dir_path = os.path.abspath(os.path.join(concore_path, dir)) proc = call(["./clear"], cwd=dir_path) if(proc == 0): @@ -131,20 +135,17 @@ def clear(dir): resp.status_code = 500 return resp -# to download /download/?fetch=. For example, /download/test?fetch=example.py.out&apikey=xyz +# to download /download/?fetch=. For example, /download/test?fetchDir=xyz&fetch=u @app.route('/download/', methods=['POST', 'GET']) def download(dir): download_file = request.args.get('fetch') - apikey = request.args.get('apikey') - dirname = dir + "_" + apikey - directory_name = os.path.abspath(os.path.join(concore_path, secure_filename(dirname))) - - - if not os.path.exists(secure_filename(dirname)): + sub_folder = request.args.get('fetchDir') + dirname = secure_filename(dir) + "/" + secure_filename(sub_folder) + directory_name = os.path.abspath(os.path.join(concore_path, dirname)) + if not os.path.exists(directory_name): resp = jsonify({'message': 'Directory not found'}) resp.status_code = 400 return resp - try: return send_from_directory(directory_name, download_file, as_attachment=True) except: @@ -155,6 +156,7 @@ def download(dir): @app.route('/destroy/', methods=['DELETE']) def destroy(dir): + dir = secure_filename(dir) proc = call(["./destroy", dir], cwd=concore_path) if(proc == 0): resp = jsonify({'message': 'Successfuly deleted Dirctory'}) @@ -167,7 +169,9 @@ def destroy(dir): @app.route('/getFilesList/', methods=['POST']) def getFilesList(dir): - dir_path = os.path.abspath(os.path.join(concore_path, dir)) + sub_dir = request.args.get('fetch') + dirname = secure_filename(dir) + "/" + secure_filename(sub_dir) + dir_path = os.path.abspath(os.path.join(concore_path, dirname)) res = [] res = os.listdir(dir_path) res = json.dumps(res) diff --git a/fri/test.py b/fri/test.py index bfb5571..7241e87 100644 --- a/fri/test.py +++ b/fri/test.py @@ -8,22 +8,14 @@ def upload(files): url = "http://127.0.0.1:5000/upload/test?apikey=xyz" - payload={} - # files=[ - # ('files[]',('example.py',open(path,'rb'),'application/octet-stream')) - # ] headers = {} - response = requests.request("POST", url, headers=headers, data=payload, files=files) - print(response.text) # # ******* - - # function to check build def build(dir, graphml, apikey): url = "http://127.0.0.1:5000/build/"+dir+"?"+"fetch="+graphml+"&"+"apikey="+apikey @@ -60,8 +52,8 @@ def destroy(dir): print(response.text) -def getFilesList(dir): - url = "http://127.0.0.1:5000/getFilesList/" + dir +def getFilesList(dir, sub_dir = ""): + url = "http://127.0.0.1:5000/getFilesList/" + dir + "?"+"fetch="+sub_dir response = requests.request("POST", url) print(response.text) @@ -71,29 +63,45 @@ def openJupyter(): print(response.text) # function to test download() method. -def download(): - url = "http://127.0.0.1:5000/download/test?fetch=f1.txt&apikey=xyz" - urllib.request.urlretrieve(url, "f1.txt") +def download(dir, subDir, fileName ): + url = "http://127.0.0.1:5000/download/"+dir+"?"+"fetchDir="+subDir+"&"+"fetch="+ fileName + urllib.request.urlretrieve(url, fileName) # file list to be uploaded +cur_path = os.path.dirname(os.path.abspath(__file__)) +demo_path = os.path.abspath(os.path.join(cur_path, '../demo')) +file_name1 = "controller.py" +file_name2 = "pm.py" +file_name3 = "sample1.graphml" +path_file1 = demo_path + "/" +file_name1 +path_file2 = demo_path + "/" +file_name2 +path_file3 = demo_path + "/" +file_name3 files=[ #('files[]',(file_name,open(file_path,'rb'),'application/octet-stream')) - ('files[]',('controller.py',open('/home/amit/Desktop/test_xyz/controller.py','rb'),'application/octet-stream')), - ('files[]',('pm.py',open('/home/amit/Desktop/test_xyz/pm.py','rb'),'application/octet-stream')), - ('files[]',('sample1.graphml',open('/home/amit/Desktop/test_xyz/sample1.graphml','rb'),'application/octet-stream')), - # ('files[]',('example.py',open('/home/amit/Desktop/fri/example.py','rb'),'application/octet-stream')) + ('files[]',(file_name1,open(path_file1,'rb'),'application/octet-stream')), + ('files[]',(file_name2,open(path_file2,'rb'),'application/octet-stream')), + ('files[]',(file_name3,open(path_file3,'rb'),'application/octet-stream')), ] -# upload(files) -# build("test", "sample1", "xyz") -# time.sleep(6) -# debug("sample1") -# run("sample1") -# clear("sample1") -# stop("sample1") -# getFilesList("sample1") -# destroy("sample1") -# openJupyter() -# download() +upload(files) +time.sleep(2) +build("test", "sample1", "xyz") +time.sleep(6) +method = input("methods - 1 for debug, 0 for run :") +if method == 1: + debug("sample1") +else: + run("sample1") +time.sleep(2) +stop("sample1") +time.sleep(2) +getFilesList("sample1", "cu") +getFilesList("sample1", "pym") +time.sleep(5) +download("sample1", "cu", "u") +clear("sample1") +destroy("sample1") +openJupyter() +