diff --git a/docs/tut.restapi.rst b/docs/tut.restapi.rst index 8696243..08d1d99 100644 --- a/docs/tut.restapi.rst +++ b/docs/tut.restapi.rst @@ -16,6 +16,8 @@ and is implemented in the class :class:`~epyt_flow.rest_api.server.RestApiServic +-----------+-------------------------------------------------------+------------------------------------------------------------------------------------------+-----------------------------------------------------------------------------------------------------+ | DELETE | /scenario/{scenario_id} | :class:`~epyt_flow.rest_api.scenario_handler.ScenarioRemoveHandler` | Deletes a scenario. | +-----------+-------------------------------------------------------+------------------------------------------------------------------------------------------+-----------------------------------------------------------------------------------------------------+ +| GET | /scenario/{scenario_id}/export | :class:`~epyt_flow.rest_api.scenario_handler.ScenarioExportHandler` | Exports a given scenario to an .inp and (optionally) .msx file. | ++-----------+-------------------------------------------------------+------------------------------------------------------------------------------------------+-----------------------------------------------------------------------------------------------------+ | GET | /scenario/{scenario_id}/topology | :class:`~epyt_flow.rest_api.scenario_handler.ScenarioTopologyHandler` | Gets the topology of a given scenario. | +-----------+-------------------------------------------------------+------------------------------------------------------------------------------------------+-----------------------------------------------------------------------------------------------------+ | GET | /scenario/{scenario_id}/scenario_config | :class:`~epyt_flow.rest_api.scenario_handler.ScenarioConfigHandler` | Gets the entire configuration/specification of a given scenario. | diff --git a/epyt_flow/rest_api/scenario_handler.py b/epyt_flow/rest_api/scenario_handler.py index 674ac4c..7f5fc3c 100644 --- a/epyt_flow/rest_api/scenario_handler.py +++ b/epyt_flow/rest_api/scenario_handler.py @@ -2,10 +2,12 @@ This module provides all handlers for requests concerning scenarios. """ import warnings +import os import falcon from .base_handler import BaseHandler from .res_manager import ResourceManager +from ..utils import get_temp_folder, pack_zip_archive from .scada_data_handler import ScadaDataManager from ..simulation import ScenarioSimulator, Leakage, SensorConfig, SensorFault @@ -69,6 +71,78 @@ def on_delete(self, _, resp: falcon.Response, scenario_id: str) -> None: resp.status = falcon.HTTP_INTERNAL_SERVER_ERROR +class ScenarioExportHandler(ScenarioBaseHandler): + """ + Class for handling GET requests for exporting a given scenario to EPANET files + -- i.e. .inp and (otpionally) .msx files. + """ + def __create_temp_file_path(self, scenario_id: str, file_ext: str) -> None: + """ + Returns a path to a temporary file for storing the scenario. + + Parameters + ---------- + scenario_id : `str` + UUID of the scenario. + file_ext : `str` + File extension. + """ + return os.path.join(get_temp_folder(), f"{scenario_id}.{file_ext}") + + def __send_temp_file(self, resp: falcon.Response, tmp_file: str, + content_type: str = "application/octet-stream") -> None: + """ + Sends a given file (`tmp_file`) to the the client. + + Parameters + ---------- + resp : `falcon.Response` + Response instance. + tmp_file : `str` + Path to the temporary file to be send. + """ + resp.status = falcon.HTTP_200 + resp.content_type = content_type + with open(tmp_file, 'rb') as f: + resp.text = f.read() + + def on_get(self, _, resp: falcon.Response, scenario_id: str) -> None: + """ + Exports a given scenario to an .inp and (optionally) .msx file. + + Parameters + ---------- + resp : `falcon.Response` + Response instance. + scenario_id : `str` + UUID of the scenario. + """ + try: + if self.scenario_mgr.validate_uuid(scenario_id) is False: + self.send_invalid_resource_id_error(resp) + return + + my_scenario = self.scenario_mgr.get(scenario_id) + + f_inp_out = self.__create_temp_file_path(scenario_id, "inp") + f_msx_out = self.__create_temp_file_path(scenario_id, "msx") + my_scenario.save_to_epanet_file(f_inp_out, f_msx_out) + + if os.path.isfile(f_msx_out): + f_out = self.__create_temp_file_path(scenario_id, "zip") + pack_zip_archive([f_inp_out, f_msx_out], f_out) + + self.__send_temp_file(resp, f_out) + os.remove(f_out) + os.remove(f_msx_out) + else: + self.__send_temp_file(resp, f_inp_out) + os.remove(f_inp_out) + except Exception as ex: + warnings.warn(str(ex)) + resp.status = falcon.HTTP_INTERNAL_SERVER_ERROR + + class ScenarioConfigHandler(ScenarioBaseHandler): """ Class for handling a GET request for getting the scenario configuration of a given scenario. diff --git a/epyt_flow/rest_api/server.py b/epyt_flow/rest_api/server.py index 6ea3f56..cd0cc59 100644 --- a/epyt_flow/rest_api/server.py +++ b/epyt_flow/rest_api/server.py @@ -8,7 +8,7 @@ ScenarioGeneralParamsHandler, ScenarioSensorConfigHandler, ScenarioSimulationHandler, \ ScenarioTopologyHandler, ScenarioConfigHandler, ScenarioLeakageHandler, \ ScenarioBasicQualitySimulationHandler, ScenarioAdvancedQualitySimulationHandler, \ - ScenarioNodeDemandPatternHandler, ScenarioSensorFaultHandler + ScenarioNodeDemandPatternHandler, ScenarioSensorFaultHandler, ScenarioExportHandler from .scada_data_handler import ScadaDataManager, ScadaDataSensorConfigHandler, \ ScadaDataPressuresHandler, ScadaDataDemandsHandler, ScadaDataFlowsHandler, \ ScadaDataLinksQualityHandler, ScadaDataNodesQualityHandler, ScadaDataRemoveHandler, \ @@ -40,6 +40,8 @@ def __init__(self, port: int = 8080): ScenarioNewHandler(self.scenario_mgr)) self.app.add_route("/scenario/{scenario_id}", ScenarioRemoveHandler(self.scenario_mgr)) + self.app.add_route("/scenario/{scenario_id}/export", + ScenarioExportHandler(self.scenario_mgr)) self.app.add_route("/scenario/{scenario_id}/topology", ScenarioTopologyHandler(self.scenario_mgr)) self.app.add_route("/scenario/{scenario_id}/scenario_config", diff --git a/epyt_flow/utils.py b/epyt_flow/utils.py index 93bafa1..a4a0334 100644 --- a/epyt_flow/utils.py +++ b/epyt_flow/utils.py @@ -234,6 +234,22 @@ def create_path_if_not_exist(path_in: str) -> None: Path(path_in).mkdir(parents=True, exist_ok=True) +def pack_zip_archive(f_in: list[str], f_out: str) -> None: + """ + Compresses a given list of files into a .zip archive. + + Parameters + ---------- + f_in : `list[str]` + List of files to be compressed into the .zip archive. + f_out : `str` + Path to the final .zip file. + """ + with zipfile.ZipFile(f_out, "w") as f_zip_out: + for f_cur_in in f_in: + f_zip_out.write(f_cur_in, compress_type=zipfile.ZIP_DEFLATED) + + def unpack_zip_archive(f_in: str, folder_out: str) -> None: """ Unpacks a .zip archive.