From e48f524d1a1171e9936b32e9f4d41fea5165830b Mon Sep 17 00:00:00 2001 From: Renato Ambrosone Date: Tue, 25 Mar 2025 16:04:49 +0100 Subject: [PATCH] refactor: API now rely on gnpy function without re-implementation Change-Id: Ib71f62f74eaa9fd87606a977f1f2c830b71668d9 --- Dockerfile | 7 ++ gnpyapi/__init__.py | 0 gnpyapi/core/{__init__.py.py => __init__.py} | 3 + gnpyapi/core/exception/__init__.py | 1 + gnpyapi/core/exception/config_error.py | 14 ++++ gnpyapi/core/exception/equipment_error.py | 14 ++++ gnpyapi/core/exception/exception_handler.py | 34 ++++++++++ .../core/exception/path_computation_error.py | 14 ++++ gnpyapi/core/exception/topology_error.py | 13 ++++ gnpyapi/core/model/__init__.py | 1 + gnpyapi/core/model/error.py | 17 +++++ gnpyapi/core/model/result.py | 8 +++ gnpyapi/core/route/__init__.py | 1 + gnpyapi/core/route/path_request_route.py | 31 +++++++++ gnpyapi/core/route/status_route.py | 7 ++ gnpyapi/core/service/__init__.py | 1 + gnpyapi/core/service/config_service.py | 4 ++ gnpyapi/core/service/equipment_service.py | 5 ++ gnpyapi/core/service/path_request_service.py | 36 +++++++++++ gnpyapi/core/service/topology_service.py | 5 ++ gnpyapi/tools/__init__.py | 0 samples/fake_sample.py | 11 ---- samples/rest_example.py | 64 +++++++++++++++++++ tests/test_api.py | 9 --- 24 files changed, 280 insertions(+), 20 deletions(-) create mode 100644 Dockerfile create mode 100644 gnpyapi/__init__.py rename gnpyapi/core/{__init__.py.py => __init__.py} (50%) create mode 100644 gnpyapi/core/exception/__init__.py create mode 100644 gnpyapi/core/exception/config_error.py create mode 100644 gnpyapi/core/exception/equipment_error.py create mode 100644 gnpyapi/core/exception/exception_handler.py create mode 100644 gnpyapi/core/exception/path_computation_error.py create mode 100644 gnpyapi/core/exception/topology_error.py create mode 100644 gnpyapi/core/model/__init__.py create mode 100644 gnpyapi/core/model/error.py create mode 100644 gnpyapi/core/model/result.py create mode 100644 gnpyapi/core/route/__init__.py create mode 100644 gnpyapi/core/route/path_request_route.py create mode 100644 gnpyapi/core/route/status_route.py create mode 100644 gnpyapi/core/service/__init__.py create mode 100644 gnpyapi/core/service/config_service.py create mode 100644 gnpyapi/core/service/equipment_service.py create mode 100644 gnpyapi/core/service/path_request_service.py create mode 100644 gnpyapi/core/service/topology_service.py create mode 100644 gnpyapi/tools/__init__.py delete mode 100644 samples/fake_sample.py create mode 100644 samples/rest_example.py diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7881fca --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM python:3.9-slim +COPY . /oopt-gnpy-api +WORKDIR /oopt-gnpy-api +RUN apt update; apt install -y git +RUN pip install . +RUN mkdir -p /opt/application/oopt-gnpy/autodesign +CMD [ "python", "./samples/rest_example.py" ] \ No newline at end of file diff --git a/gnpyapi/__init__.py b/gnpyapi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gnpyapi/core/__init__.py.py b/gnpyapi/core/__init__.py similarity index 50% rename from gnpyapi/core/__init__.py.py rename to gnpyapi/core/__init__.py index 746e650..b57d02a 100644 --- a/gnpyapi/core/__init__.py.py +++ b/gnpyapi/core/__init__.py @@ -2,3 +2,6 @@ """GNPy official API """ +from flask import Flask + +app = Flask(__name__) diff --git a/gnpyapi/core/exception/__init__.py b/gnpyapi/core/exception/__init__.py new file mode 100644 index 0000000..57d631c --- /dev/null +++ b/gnpyapi/core/exception/__init__.py @@ -0,0 +1 @@ +# coding: utf-8 diff --git a/gnpyapi/core/exception/config_error.py b/gnpyapi/core/exception/config_error.py new file mode 100644 index 0000000..03425f0 --- /dev/null +++ b/gnpyapi/core/exception/config_error.py @@ -0,0 +1,14 @@ +# coding: utf-8 + + +class ConfigError(Exception): + """ Exception raise for configuration file error + Attributes: + message -- explanation of the error + """ + + def __init__(self, message): + self.message = message + + def __str__(self): + return self.message diff --git a/gnpyapi/core/exception/equipment_error.py b/gnpyapi/core/exception/equipment_error.py new file mode 100644 index 0000000..5dea7d6 --- /dev/null +++ b/gnpyapi/core/exception/equipment_error.py @@ -0,0 +1,14 @@ +# coding: utf-8 + + +class EquipmentError(Exception): + """ Exception raise for equipment error + Attributes: + message -- explanation of the error + """ + + def __init__(self, message): + self.message = message + + def __str__(self): + return self.message diff --git a/gnpyapi/core/exception/exception_handler.py b/gnpyapi/core/exception/exception_handler.py new file mode 100644 index 0000000..b32ac8b --- /dev/null +++ b/gnpyapi/core/exception/exception_handler.py @@ -0,0 +1,34 @@ +# coding: utf-8 +import json +import re + +import werkzeug + +from gnpyapi.core.model.error import Error + +_reaesc = re.compile(r'\x1b[^m]*m') + + +def common_error_handler(exception): + """ + + :type exception: Exception + + """ + status_code = 500 + if not isinstance(exception, werkzeug.exceptions.HTTPException): + exception = werkzeug.exceptions.InternalServerError() + exception.description = "Something went wrong on our side." + else: + status_code = exception.code + response = Error(message=exception.name, description=exception.description, + code=status_code) + + return werkzeug.Response(response=json.dumps(response.__dict__), status=status_code, mimetype='application/json') + + +def bad_request_handler(exception): + exception_str = " ".join(str(exception).split()) + response = Error(message='bad request', description=_reaesc.sub('', exception_str.replace("\n", " ")), + code=400) + return werkzeug.Response(response=json.dumps(response.__dict__), status=400, mimetype='application/json') diff --git a/gnpyapi/core/exception/path_computation_error.py b/gnpyapi/core/exception/path_computation_error.py new file mode 100644 index 0000000..1b7202e --- /dev/null +++ b/gnpyapi/core/exception/path_computation_error.py @@ -0,0 +1,14 @@ +# coding: utf-8 + + +class PathComputationError(Exception): + """ Exception raise for path computation error error + Attributes: + message -- explanation of the error + """ + + def __init__(self, message): + self.message = message + + def __str__(self): + return self.message diff --git a/gnpyapi/core/exception/topology_error.py b/gnpyapi/core/exception/topology_error.py new file mode 100644 index 0000000..e074477 --- /dev/null +++ b/gnpyapi/core/exception/topology_error.py @@ -0,0 +1,13 @@ +# coding: utf-8 + +class TopologyError(Exception): + """ Exception raise for topology error + Attributes: + message -- explanation of the error + """ + + def __init__(self, message): + self.message = message + + def __str__(self): + return self.message diff --git a/gnpyapi/core/model/__init__.py b/gnpyapi/core/model/__init__.py new file mode 100644 index 0000000..57d631c --- /dev/null +++ b/gnpyapi/core/model/__init__.py @@ -0,0 +1 @@ +# coding: utf-8 diff --git a/gnpyapi/core/model/error.py b/gnpyapi/core/model/error.py new file mode 100644 index 0000000..b81fdbf --- /dev/null +++ b/gnpyapi/core/model/error.py @@ -0,0 +1,17 @@ +# coding: utf-8 + + +class Error: + + def __init__(self, code: int = None, message: str = None, description: str = None): + """Error + :param code: The code of this Error. + :type code: int + :param message: The message of this Error. + :type message: str + :param description: The description of this Error. + :type description: str + """ + self.code = code + self.message = message + self.description = description diff --git a/gnpyapi/core/model/result.py b/gnpyapi/core/model/result.py new file mode 100644 index 0000000..2f87126 --- /dev/null +++ b/gnpyapi/core/model/result.py @@ -0,0 +1,8 @@ +# coding: utf-8 + + +class Result: + + def __init__(self, message: str = None, description: str = None): + self.message = message + self.description = description diff --git a/gnpyapi/core/route/__init__.py b/gnpyapi/core/route/__init__.py new file mode 100644 index 0000000..57d631c --- /dev/null +++ b/gnpyapi/core/route/__init__.py @@ -0,0 +1 @@ +# coding: utf-8 diff --git a/gnpyapi/core/route/path_request_route.py b/gnpyapi/core/route/path_request_route.py new file mode 100644 index 0000000..a776189 --- /dev/null +++ b/gnpyapi/core/route/path_request_route.py @@ -0,0 +1,31 @@ +# coding: utf-8 +from pathlib import Path + +from flask import request + +from gnpyapi.core import app +from gnpyapi.core.exception.equipment_error import EquipmentError +from gnpyapi.core.exception.topology_error import TopologyError +from gnpyapi.core.service.path_request_service import PathRequestService + +PATH_COMPUTATION_BASE_PATH = '/api/v1/path-computation' +PATH_REQUEST_BASE_PATH = '/api/v1/path-request' +AUTODESIGN_PATH = PATH_COMPUTATION_BASE_PATH + '//autodesign' + +_examples_dir = Path(__file__).parent.parent.parent / 'example-data' + + +@app.route(PATH_REQUEST_BASE_PATH, methods=['POST']) +def path_request(path_request_service: PathRequestService): + data = request.json + service = data['gnpy-api:service'] + if 'gnpy-api:topology' in data: + topology = data['gnpy-api:topology'] + else: + raise TopologyError('No topology found in request') + if 'gnpy-api:equipment' in data: + equipment = data['gnpy-api:equipment'] + else: + raise EquipmentError('No equipment found in request') + + return path_request_service.path_request(topology, equipment, service), 201 diff --git a/gnpyapi/core/route/status_route.py b/gnpyapi/core/route/status_route.py new file mode 100644 index 0000000..8fd3ebd --- /dev/null +++ b/gnpyapi/core/route/status_route.py @@ -0,0 +1,7 @@ +# coding: utf-8 +from gnpyapi.core import app + + +@app.route('/api/v1/status', methods=['GET']) +def api_status(): + return {"version": "v1", "status": "ok"}, 200 diff --git a/gnpyapi/core/service/__init__.py b/gnpyapi/core/service/__init__.py new file mode 100644 index 0000000..57d631c --- /dev/null +++ b/gnpyapi/core/service/__init__.py @@ -0,0 +1 @@ +# coding: utf-8 diff --git a/gnpyapi/core/service/config_service.py b/gnpyapi/core/service/config_service.py new file mode 100644 index 0000000..1477fac --- /dev/null +++ b/gnpyapi/core/service/config_service.py @@ -0,0 +1,4 @@ +# coding: utf-8 +class ConfigService: + def __init__(self): + pass diff --git a/gnpyapi/core/service/equipment_service.py b/gnpyapi/core/service/equipment_service.py new file mode 100644 index 0000000..86d05b2 --- /dev/null +++ b/gnpyapi/core/service/equipment_service.py @@ -0,0 +1,5 @@ +# coding: utf- +class EquipmentService: + + def __init__(self): + pass diff --git a/gnpyapi/core/service/path_request_service.py b/gnpyapi/core/service/path_request_service.py new file mode 100644 index 0000000..f4db128 --- /dev/null +++ b/gnpyapi/core/service/path_request_service.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- + +import logging + +from gnpy.core.exceptions import EquipmentConfigError, NetworkTopologyError +from gnpy.tools.json_io import results_to_json, load_eqpt_topo_from_json +from gnpy.tools.worker_utils import designed_network, planning +from gnpyapi.core.exception.topology_error import TopologyError + +from gnpyapi.core.exception.equipment_error import EquipmentError + +_logger = logging.getLogger(__name__) + + +class PathRequestService: + + def __init__(self): + pass + + @staticmethod + def path_request(topology: dict, equipment: dict, service: dict = None) -> dict: + try: + (equipment, network) = load_eqpt_topo_from_json(equipment, topology) + network, _, _ = designed_network(equipment, network) + # todo parse request + _, _, _, _, _, result = planning(network, equipment, service) + return results_to_json(result) + except EquipmentConfigError as e: + _logger.error(f"An equipment error occurred: {e}") + raise EquipmentError(str(e)) + except NetworkTopologyError as e: + _logger.error(f"An equipment error occurred: {e}") + raise TopologyError(str(e)) + except Exception as e: + _logger.error(f"An error occurred during path request: {e}") + raise diff --git a/gnpyapi/core/service/topology_service.py b/gnpyapi/core/service/topology_service.py new file mode 100644 index 0000000..a896f19 --- /dev/null +++ b/gnpyapi/core/service/topology_service.py @@ -0,0 +1,5 @@ +# coding: utf- + +class TopologyService: + def __init__(self): + pass diff --git a/gnpyapi/tools/__init__.py b/gnpyapi/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/samples/fake_sample.py b/samples/fake_sample.py deleted file mode 100644 index 5887d6a..0000000 --- a/samples/fake_sample.py +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -"""Examples of api calls -""" - -# for the moment just launch gnpy to check everything is OK - -from gnpy.tools.cli_examples import transmission_main_example - -transmission_main_example() diff --git a/samples/rest_example.py b/samples/rest_example.py new file mode 100644 index 0000000..84be367 --- /dev/null +++ b/samples/rest_example.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +''' +gnpy.tools.rest_example +======================= + +GNPy as a rest API example +''' + +import logging +from logging.handlers import RotatingFileHandler + +import werkzeug +from flask_injector import FlaskInjector + +from gnpyapi.core import app +from gnpyapi.core.exception.equipment_error import EquipmentError +from gnpyapi.core.exception.exception_handler import bad_request_handler, common_error_handler +from gnpyapi.core.exception.path_computation_error import PathComputationError +from gnpyapi.core.exception.topology_error import TopologyError + +_logger = logging.getLogger(__name__) + + +def _init_logger(): + handler = RotatingFileHandler('api.log', maxBytes=1024 * 1024, backupCount=5, encoding='utf-8') + ch = logging.StreamHandler() + logging.basicConfig(level=logging.INFO, handlers=[handler, ch], + format="%(asctime)s %(levelname)s %(name)s(%(lineno)s) [%(threadName)s - %(thread)d] - %(" + "message)s") + + +def _init_app(): + app.register_error_handler(KeyError, bad_request_handler) + app.register_error_handler(TypeError, bad_request_handler) + app.register_error_handler(ValueError, bad_request_handler) + # app.register_error_handler(exceptions.ConfigurationError, bad_request_handler) + # app.register_error_handler(exceptions.DisjunctionError, bad_request_handler) + # app.register_error_handler(exceptions.EquipmentConfigError, bad_request_handler) + # app.register_error_handler(exceptions.NetworkTopologyError, bad_request_handler) + # app.register_error_handler(exceptions.ServiceError, bad_request_handler) + # app.register_error_handler(exceptions.SpectrumError, bad_request_handler) + # app.register_error_handler(exceptions.ParametersError, bad_request_handler) + app.register_error_handler(AssertionError, bad_request_handler) + # app.register_error_handler(InternalServerError, common_error_handler) + app.register_error_handler(TopologyError, bad_request_handler) + app.register_error_handler(EquipmentError, bad_request_handler) + + app.register_error_handler(PathComputationError, bad_request_handler) + for error_code in werkzeug.exceptions.default_exceptions: + app.register_error_handler(error_code, common_error_handler) + + +def main(): + _init_logger() + _init_app() + FlaskInjector(app=app) + + app.run(host='0.0.0.0', port=8080) + + +if __name__ == '__main__': + main() diff --git a/tests/test_api.py b/tests/test_api.py index 3ec86de..0033651 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -12,15 +12,6 @@ YANG_DIR = Path(__file__).parent.parent / 'gnpyapi' / 'yang' SAMPLE_DIR = Path(__file__).parent.parent / 'samples' -def test_sample(): - """Just for the ci - """ - res = subprocess.run(['python', SAMPLE_DIR / 'fake_sample.py'], - stdout=subprocess.PIPE, check=True) - if res.returncode != 0: - assert False, f'gnpy call failed: exit code {res.returncode}' - - def test_pyang(): """Verify that yang models pss pyang """