mirror of
https://github.com/Telecominfraproject/oopt-gnpy-api.git
synced 2025-11-01 02:18:08 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51b266bd2a | ||
|
|
13a81e8f94 | ||
|
|
c30308eb92 | ||
|
|
7695db8674 | ||
|
|
192bb265bd | ||
|
|
b9acee661c | ||
|
|
b0bda64b39 | ||
|
|
fbb3d1dc7a | ||
|
|
2133ded1a8 |
39
.github/workflows/main.yml
vendored
39
.github/workflows/main.yml
vendored
@@ -53,6 +53,45 @@ jobs:
|
||||
- tox_env: docs
|
||||
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:
|
||||
name: Tests on other platforms
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
24
README.md
24
README.md
@@ -1,34 +1,14 @@
|
||||
# GNPy API
|
||||
|
||||
[](https://pypi.org/project/gnpy/)
|
||||
|
||||
REST API (experimental)
|
||||
-----------------------
|
||||
``gnpyapi`` provides an experimental api for requesting several paths at once. It is based on Flask server.
|
||||
You can run it through command line or Docker.
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ gnpy-rest
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ docker run -p 8080:8080 -it emmanuelledelfour/gnpy-experimental:candi-1.1 gnpy-rest
|
||||
|
||||
When starting the api server will aks for an encryption/decryption key. This key i used to encrypt equipment file when using /api/v1/equipments endpoint.
|
||||
This key is a Fernet key and can be generated this way:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from cryptography.fernet import Fernet
|
||||
Fernet.generate_key()
|
||||
|
||||
|
||||
After typing the key, you can detach the container by typing ^P^Q.
|
||||
After starting the api server, you can launch a request
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ curl -v -X POST -H "Content-Type: application/json" -d @<PATH_TO_JSON_REQUEST_FILE> https://localhost:8080/api/v1/path-computation -k
|
||||
$ curl --location 'http://localhost:8080/api/v1/path-request' --header 'Content-Type: application/json' --data @gnpyapi/exampledata/planning_demand_example.json
|
||||
|
||||
TODO: api documentation, unit tests, real WSGI server with trusted certificates
|
||||
|
||||
|
||||
@@ -4,4 +4,9 @@
|
||||
"""
|
||||
from flask import Flask
|
||||
|
||||
API_VERSION = "/api/v0.1"
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
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
|
||||
from pathlib import Path
|
||||
|
||||
from flask import request
|
||||
|
||||
@@ -7,14 +6,12 @@ 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
|
||||
from gnpyapi.core import API_VERSION
|
||||
|
||||
PATH_COMPUTATION_BASE_PATH = '/api/v1/path-computation'
|
||||
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'
|
||||
PATH_REQUEST_BASE_PATH = '/path-request'
|
||||
|
||||
|
||||
@app.route(API_VERSION + PATH_REQUEST_BASE_PATH, methods=['POST'])
|
||||
@app.route(PATH_REQUEST_BASE_PATH, methods=['POST'])
|
||||
def path_request(path_request_service: PathRequestService):
|
||||
data = request.json
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# coding: utf-8
|
||||
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():
|
||||
return {"version": "v1", "status": "ok"}, 200
|
||||
return {"version": "v0.1", "status": "ok"}, 200
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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.path_computation_error import PathComputationError
|
||||
from gnpyapi.core.exception.topology_error import TopologyError
|
||||
import argparse
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -52,13 +53,22 @@ def _init_app():
|
||||
app.register_error_handler(error_code, common_error_handler)
|
||||
|
||||
|
||||
def main():
|
||||
def main(http: bool = False):
|
||||
_init_logger()
|
||||
_init_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__':
|
||||
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]
|
||||
name = gnpyapi
|
||||
name = gnpy-api
|
||||
description-file = README.md
|
||||
description-content-type = text/markdown; variant=GFM
|
||||
author = Telecom Infra Project
|
||||
author-email = tbd
|
||||
author-email = adamico@nec-labs.com
|
||||
license = BSD-3-Clause
|
||||
home-page = https://github.com/Telecominfraproject/oopt-gnpy-api
|
||||
project_urls =
|
||||
@@ -51,6 +51,7 @@ install_requires =
|
||||
gnpy==2.12.1
|
||||
flask>=1.1.2
|
||||
Flask-Injector
|
||||
pyopenssl==25.0.0
|
||||
|
||||
[options.extras_require]
|
||||
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 -*-
|
||||
# @Author: Esther Le Rouzic
|
||||
# @Date: 2025-02-03
|
||||
|
||||
import json
|
||||
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'
|
||||
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():
|
||||
"""Verify that yang models pss pyang
|
||||
"""
|
||||
res = subprocess.run(['pyang', '-f', 'tree', '--tree-line-length', '69',
|
||||
'-p', YANG_DIR, YANG_DIR / 'gnpy-api@2021-01-06.yang'],
|
||||
stdout=subprocess.PIPE, check=True)
|
||||
if res.returncode != 0:
|
||||
assert False, f'pyang failed: exit code {res.returncode}'
|
||||
API_VERSION = '/api/v0.1'
|
||||
|
||||
|
||||
def read_json_file(path):
|
||||
with open(path, "r") as file:
|
||||
return json.load(file)
|
||||
|
||||
|
||||
@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