mirror of
https://github.com/Telecominfraproject/oopt-gnpy-api.git
synced 2025-11-01 02:18:08 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6041db35d | ||
|
|
256341f937 | ||
|
|
51b266bd2a | ||
|
|
13a81e8f94 | ||
|
|
c30308eb92 | ||
|
|
7695db8674 | ||
|
|
192bb265bd | ||
|
|
b9acee661c |
39
.github/workflows/main.yml
vendored
39
.github/workflows/main.yml
vendored
@@ -53,6 +53,45 @@ jobs:
|
|||||||
- tox_env: docs
|
- tox_env: docs
|
||||||
dnf_install: graphviz
|
dnf_install: graphviz
|
||||||
|
|
||||||
|
release-build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && github.repository_owner == 'Telecominfraproject' }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: build release distributions
|
||||||
|
run: |
|
||||||
|
python -m pip install build
|
||||||
|
python -m build
|
||||||
|
|
||||||
|
- name: upload windows dists
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: release-dists
|
||||||
|
path: dist/
|
||||||
|
|
||||||
|
pypi-publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- release-build
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Retrieve release distributions
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: release-dists
|
||||||
|
path: dist/
|
||||||
|
|
||||||
|
- name: Publish release distributions to PyPI
|
||||||
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
|
|
||||||
other-platforms:
|
other-platforms:
|
||||||
name: Tests on other platforms
|
name: Tests on other platforms
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
|||||||
72
README.md
72
README.md
@@ -1,17 +1,71 @@
|
|||||||
# GNPy API
|
# GNPy API
|
||||||
[](https://pypi.org/project/gnpy/)
|
[](https://pypi.org/project/gnpy-api/)
|
||||||
|
[](https://pypi.org/project/gnpy-api/)
|
||||||
|
[](https://review.gerrithub.io/q/project:Telecominfraproject/oopt-gnpy-api)
|
||||||
|
|
||||||
REST API (experimental)
|
REST API (experimental)
|
||||||
-----------------------
|
-----------------------
|
||||||
``gnpyapi`` provides an experimental api for requesting several paths at once. It is based on Flask server.
|
This repository extends GNPy with additional interfaces, allowing for more flexible control and interaction with its simulation engine. These interfaces can be used to integrate GNPy into software-defined networking (SDN) architectures or other external applications.
|
||||||
You can run it through command line or Docker.
|
|
||||||
|
[GNPy](https://github.com/Telecominfraproject/oopt-gnpy) is an open-source Python-based library that models and evaluates the performance of optical networks. It is widely used for path computation, QoT (Quality of Transmission) estimation, and network planning.
|
||||||
|
|
||||||
|
## 🚀 Installation
|
||||||
|
### Build from Source - Option 1
|
||||||
|
Clone the repository and install the package:
|
||||||
|
```bash
|
||||||
|
python3 setup.py install
|
||||||
|
```
|
||||||
|
or if you want to install it in a docker container:
|
||||||
|
```bash
|
||||||
|
docker docker build ./ -t gnpy-api
|
||||||
|
```
|
||||||
|
|
||||||
|
### PiPy - Option 2
|
||||||
|
```bash
|
||||||
|
pip install gnpy-api
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### DockerHub - Option 3
|
||||||
$ curl --location 'http://localhost:8080/api/v1/path-request' --header 'Content-Type: application/json' --data @gnpyapi/exampledata/planning_demand_example.json
|
Coming....
|
||||||
|
|
||||||
TODO: api documentation, unit tests, real WSGI server with trusted certificates
|
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
tbd
|
## 🧪 Usage - CLI
|
||||||
|
Start the REST API server:
|
||||||
|
```bash
|
||||||
|
python ./samples/rest_example.py
|
||||||
|
```
|
||||||
|
See the help for the REST API:
|
||||||
|
```bash
|
||||||
|
python ./samples/rest_example.py -h
|
||||||
|
```
|
||||||
|
Send example data to the REST API:
|
||||||
|
```bash
|
||||||
|
curl --location 'https://localhost:8080/api/v0.1/path-request' --header 'Content-Type: application/json' --data @gnpyapi/exampledata/planning_demand_example.json -k
|
||||||
|
```
|
||||||
|
## 🧪 Usage - Docker
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 🔄 Compatibility
|
||||||
|
|
||||||
|
Different versions of this interface extension are compatible with specific versions of [GNPy](https://github.com/Telecominfraproject/oopt-gnpy). Please refer to the table below to ensure that you are using a supported version combination.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Interface Version | Compatible GNPy Version | Notes |
|
||||||
|
|-------------------|-----------------|--------------------------------|
|
||||||
|
| `v0.1.x` | `>=2.12.1` | Initial release |
|
||||||
|
|
||||||
|
⚠️ If you use an incompatible combination, some features may not work correctly or may produce unexpected errors.
|
||||||
|
|
||||||
|
To check your GNPy version, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip show gnpy
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 Documentation
|
||||||
|
Refer to the official [GNPy documentation](https://github.com/Telecominfraproject/oopt-gnpy/tree/master/docs) for information about network modeling and simulation capabilities.
|
||||||
|
|
||||||
|
API documentation is available in the docs folder.
|
||||||
@@ -4,6 +4,9 @@
|
|||||||
"""
|
"""
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
|
||||||
|
API_VERSION = "/api/v0.1"
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
import gnpyapi.core.route.path_request_route # noqa: F401, E402
|
import gnpyapi.core.route.path_request_route # noqa: E402
|
||||||
|
import gnpyapi.core.route.status_route # noqa: F401, E402
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
|
|
||||||
@@ -7,15 +6,12 @@ from gnpyapi.core import app
|
|||||||
from gnpyapi.core.exception.equipment_error import EquipmentError
|
from gnpyapi.core.exception.equipment_error import EquipmentError
|
||||||
from gnpyapi.core.exception.topology_error import TopologyError
|
from gnpyapi.core.exception.topology_error import TopologyError
|
||||||
from gnpyapi.core.service.path_request_service import PathRequestService
|
from gnpyapi.core.service.path_request_service import PathRequestService
|
||||||
|
from gnpyapi.core import API_VERSION
|
||||||
|
|
||||||
PATH_COMPUTATION_BASE_PATH = '/api/v1/path-computation'
|
PATH_REQUEST_BASE_PATH = '/path-request'
|
||||||
PATH_REQUEST_BASE_PATH = '/api/v1/path-request'
|
|
||||||
AUTODESIGN_PATH = PATH_COMPUTATION_BASE_PATH + '/<path_computation_id>/autodesign'
|
|
||||||
|
|
||||||
_examples_dir = Path(__file__).parent.parent.parent / 'example-data'
|
|
||||||
|
|
||||||
|
|
||||||
@app.route(PATH_REQUEST_BASE_PATH, methods=['POST'])
|
@app.route(API_VERSION + PATH_REQUEST_BASE_PATH, methods=['POST'])
|
||||||
def path_request(path_request_service: PathRequestService):
|
def path_request(path_request_service: PathRequestService):
|
||||||
data = request.json
|
data = request.json
|
||||||
service = data['gnpy-api:service']
|
service = data['gnpy-api:service']
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from gnpyapi.core import app
|
from gnpyapi.core import app
|
||||||
|
from gnpyapi.core import API_VERSION
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/v1/status', methods=['GET'])
|
@app.route(API_VERSION + '/status', methods=['GET'])
|
||||||
def api_status():
|
def api_status():
|
||||||
return {"version": "v1", "status": "ok"}, 200
|
return {"version": "v0.1", "status": "ok"}, 200
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ 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.exception_handler import bad_request_handler, common_error_handler
|
||||||
from gnpyapi.core.exception.path_computation_error import PathComputationError
|
from gnpyapi.core.exception.path_computation_error import PathComputationError
|
||||||
from gnpyapi.core.exception.topology_error import TopologyError
|
from gnpyapi.core.exception.topology_error import TopologyError
|
||||||
|
import argparse
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -52,13 +53,22 @@ def _init_app():
|
|||||||
app.register_error_handler(error_code, common_error_handler)
|
app.register_error_handler(error_code, common_error_handler)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main(http: bool = False):
|
||||||
_init_logger()
|
_init_logger()
|
||||||
_init_app()
|
_init_app()
|
||||||
FlaskInjector(app=app)
|
FlaskInjector(app=app)
|
||||||
|
|
||||||
app.run(host='0.0.0.0', port=8080)
|
if http:
|
||||||
|
app.run(host='0.0.0.0', port=8080)
|
||||||
|
else:
|
||||||
|
app.run(host='0.0.0.0', port=8080, ssl_context='adhoc')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
parser = argparse.ArgumentParser(description="Rest API example")
|
||||||
|
|
||||||
|
parser.add_argument("--http", action="store_true", help="run server with http instead of https")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
main(http=args.http)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
[metadata]
|
[metadata]
|
||||||
name = gnpyapi
|
name = gnpy-api
|
||||||
description-file = README.md
|
description-file = README.md
|
||||||
description-content-type = text/markdown; variant=GFM
|
description-content-type = text/markdown; variant=GFM
|
||||||
author = Telecom Infra Project
|
author = Telecom Infra Project
|
||||||
author-email = tbd
|
author-email = adamico@nec-labs.com
|
||||||
license = BSD-3-Clause
|
license = BSD-3-Clause
|
||||||
home-page = https://github.com/Telecominfraproject/oopt-gnpy-api
|
home-page = https://github.com/Telecominfraproject/oopt-gnpy-api
|
||||||
project_urls =
|
project_urls =
|
||||||
@@ -51,6 +51,7 @@ install_requires =
|
|||||||
gnpy==2.12.1
|
gnpy==2.12.1
|
||||||
flask>=1.1.2
|
flask>=1.1.2
|
||||||
Flask-Injector
|
Flask-Injector
|
||||||
|
pyopenssl==25.0.0
|
||||||
|
|
||||||
[options.extras_require]
|
[options.extras_require]
|
||||||
tests =
|
tests =
|
||||||
|
|||||||
1578
tests/data/req/planning_demand_example.json
Normal file
1578
tests/data/req/planning_demand_example.json
Normal file
File diff suppressed because it is too large
Load Diff
1578
tests/data/req/planning_demand_wrong_eqpt.json
Normal file
1578
tests/data/req/planning_demand_wrong_eqpt.json
Normal file
File diff suppressed because it is too large
Load Diff
1578
tests/data/req/planning_demand_wrong_topology.json
Normal file
1578
tests/data/req/planning_demand_wrong_topology.json
Normal file
File diff suppressed because it is too large
Load Diff
1469
tests/data/res/planning_demand_res.json
Normal file
1469
tests/data/res/planning_demand_res.json
Normal file
File diff suppressed because it is too large
Load Diff
0
tests/service/__init__.py
Normal file
0
tests/service/__init__.py
Normal file
55
tests/service/test_path_request_service.py
Normal file
55
tests/service/test_path_request_service.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# @Author: Esther Le Rouzic
|
||||||
|
# @Date: 2025-02-03
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from gnpyapi.core.exception.equipment_error import EquipmentError
|
||||||
|
|
||||||
|
from gnpyapi.core.service.path_request_service import PathRequestService
|
||||||
|
from gnpyapi.core.exception.topology_error import TopologyError
|
||||||
|
|
||||||
|
TEST_DATA_DIR = Path(__file__).parent.parent / 'data'
|
||||||
|
TEST_REQ_DIR = TEST_DATA_DIR / 'req'
|
||||||
|
TEST_RES_DIR = TEST_DATA_DIR / 'res'
|
||||||
|
|
||||||
|
|
||||||
|
def read_json_file(path):
|
||||||
|
with open(path, "r") as file:
|
||||||
|
return json.load(file)
|
||||||
|
|
||||||
|
|
||||||
|
def test_path_request_success():
|
||||||
|
input_data = read_json_file(TEST_REQ_DIR / "planning_demand_example.json")
|
||||||
|
expected_response = read_json_file(TEST_RES_DIR / "planning_demand_res.json")
|
||||||
|
topology = input_data["gnpy-api:topology"]
|
||||||
|
equipment = input_data["gnpy-api:equipment"]
|
||||||
|
service = input_data["gnpy-api:service"]
|
||||||
|
|
||||||
|
result = PathRequestService.path_request(topology, equipment, service)
|
||||||
|
assert result == expected_response
|
||||||
|
|
||||||
|
|
||||||
|
def test_path_request_invalid_equipment():
|
||||||
|
input_data = read_json_file(TEST_REQ_DIR / "planning_demand_wrong_eqpt.json")
|
||||||
|
topology = input_data["gnpy-api:topology"]
|
||||||
|
equipment = input_data["gnpy-api:equipment"]
|
||||||
|
service = input_data["gnpy-api:service"]
|
||||||
|
|
||||||
|
with pytest.raises(EquipmentError) as exc:
|
||||||
|
PathRequestService.path_request(topology, equipment, service)
|
||||||
|
assert "invalid" in str(exc.value).lower()
|
||||||
|
assert "deltap" in str(exc.value).lower()
|
||||||
|
|
||||||
|
|
||||||
|
def test_path_request_invalid_topology():
|
||||||
|
input_data = read_json_file(TEST_REQ_DIR / "planning_demand_wrong_topology.json")
|
||||||
|
topology = input_data["gnpy-api:topology"]
|
||||||
|
equipment = input_data["gnpy-api:equipment"]
|
||||||
|
service = input_data["gnpy-api:service"]
|
||||||
|
|
||||||
|
with pytest.raises(TopologyError) as exc:
|
||||||
|
PathRequestService.path_request(topology, equipment, service)
|
||||||
|
assert "can not find" in str(exc.value).lower()
|
||||||
@@ -2,21 +2,47 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# @Author: Esther Le Rouzic
|
# @Author: Esther Le Rouzic
|
||||||
# @Date: 2025-02-03
|
# @Date: 2025-02-03
|
||||||
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import subprocess
|
|
||||||
import pytest # noqa: F401
|
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from flask_injector import FlaskInjector
|
||||||
|
|
||||||
|
from gnpyapi.core import app
|
||||||
|
|
||||||
YANG_DIR = Path(__file__).parent.parent / 'gnpyapi' / 'yang'
|
YANG_DIR = Path(__file__).parent.parent / 'gnpyapi' / 'yang'
|
||||||
SAMPLE_DIR = Path(__file__).parent.parent / 'samples'
|
SAMPLE_DIR = Path(__file__).parent.parent / 'samples'
|
||||||
|
|
||||||
|
TEST_DATA_DIR = Path(__file__).parent / 'data'
|
||||||
|
TEST_REQ_DIR = TEST_DATA_DIR / 'req'
|
||||||
|
TEST_RES_DIR = TEST_DATA_DIR / 'res'
|
||||||
|
|
||||||
def test_pyang():
|
API_VERSION = '/api/v0.1'
|
||||||
"""Verify that yang models pss pyang
|
|
||||||
"""
|
|
||||||
res = subprocess.run(['pyang', '-f', 'tree', '--tree-line-length', '69',
|
def read_json_file(path):
|
||||||
'-p', YANG_DIR, YANG_DIR / 'gnpy-api@2021-01-06.yang'],
|
with open(path, "r") as file:
|
||||||
stdout=subprocess.PIPE, check=True)
|
return json.load(file)
|
||||||
if res.returncode != 0:
|
|
||||||
assert False, f'pyang failed: exit code {res.returncode}'
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client():
|
||||||
|
app.testing = True
|
||||||
|
FlaskInjector(app=app)
|
||||||
|
with app.test_client() as client:
|
||||||
|
yield client
|
||||||
|
|
||||||
|
|
||||||
|
def test_echo(client):
|
||||||
|
input_data = read_json_file(TEST_REQ_DIR / "planning_demand_example.json")
|
||||||
|
expected_response = read_json_file(TEST_RES_DIR / "planning_demand_res.json")
|
||||||
|
|
||||||
|
response = client.post(f"{API_VERSION}/path-request", json=input_data)
|
||||||
|
assert response.status_code == 201
|
||||||
|
assert response.get_json() == expected_response
|
||||||
|
|
||||||
|
|
||||||
|
def test_status(client):
|
||||||
|
response = client.get(f"{API_VERSION}/status")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.get_json() == {"version": "v0.1", "status": "ok"}
|
||||||
|
|||||||
Reference in New Issue
Block a user