mirror of
https://github.com/Telecominfraproject/oopt-gnpy.git
synced 2025-10-29 09:12:37 +00:00
Move and refactor create_eqpt_sheet.py and add tests on it
Co-authored-by: Rodrigo Sasse David <rodrigo.sassedavid@orange.com> Signed-off-by: EstherLerouzic <esther.lerouzic@orange.com> Change-Id: Ib961c5c0e203f2225a0f1e2e7a091485567189c3
This commit is contained in:
@@ -29,6 +29,7 @@ To learn how to contribute, please see CONTRIBUTING.md
|
||||
- Raj Nagarajan (Lumentum) <raj.nagarajan@lumentum.com>
|
||||
- Renato Ambrosone (Politecnico di Torino) <renato.ambrosone@polito.it>
|
||||
- Roberts Miculens (Lattelecom) <roberts.miculens@lattelecom.lv>
|
||||
- Rodrigo Sasse David (Orange) <rodrigo.sassedavid@orange.com>
|
||||
- Sami Alavi (NUST) <sami.mansooralavi1999@gmail.com>
|
||||
- Shengxiang Zhu (University of Arizona) <szhu@email.arizona.edu>
|
||||
- Stefan Melin (Telia Company) <Stefan.Melin@teliacompany.com>
|
||||
|
||||
149
gnpy/tools/create_eqpt_sheet.py
Normal file
149
gnpy/tools/create_eqpt_sheet.py
Normal file
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
# Utility functions that creates an Eqpt sheet template
|
||||
# Copyright (C) 2025 Telecom Infra Project and GNPy contributors
|
||||
# see AUTHORS.rst for a list of contributors
|
||||
|
||||
"""
|
||||
create_eqpt_sheet.py
|
||||
====================
|
||||
|
||||
XLS parser that can be called to create a "City" column in the "Eqpt" sheet.
|
||||
|
||||
If not present in the "Nodes" sheet, the "Type" column will be implicitly
|
||||
determined based on the topology.
|
||||
"""
|
||||
|
||||
from argparse import ArgumentParser
|
||||
from pathlib import Path
|
||||
import csv
|
||||
from typing import List, Dict, Optional
|
||||
from logging import getLogger
|
||||
import dataclasses
|
||||
|
||||
from gnpy.core.exceptions import NetworkTopologyError
|
||||
from gnpy.tools.xls_utils import generic_open_workbook, get_sheet, XLS_EXCEPTIONS, all_rows, fast_get_sheet_rows, \
|
||||
WorkbookType, SheetType
|
||||
|
||||
|
||||
logger = getLogger(__name__)
|
||||
EXAMPLE_DATA_DIR = Path(__file__).parent.parent / 'example-data'
|
||||
|
||||
PARSER = ArgumentParser()
|
||||
PARSER.add_argument('workbook', type=Path, nargs='?', default=f'{EXAMPLE_DATA_DIR}/meshTopologyExampleV2.xls',
|
||||
help='create the mandatory columns in Eqpt sheet')
|
||||
PARSER.add_argument('-o', '--output', type=Path, help='Store CSV file')
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Node:
|
||||
"""Represents a network node with a unique identifier, connected nodes, and equipment type.
|
||||
|
||||
:param uid: Unique identifier of the node.
|
||||
:type uid: str
|
||||
:param to_node: List of connected node identifiers.
|
||||
:type to_node: List[str.]
|
||||
:param eqpt: Equipment type associated with the node (ROADM, ILA, FUSED).
|
||||
:type eqpt: str
|
||||
"""
|
||||
def __init__(self, uid: str, to_node: List[str], eqpt: str = None):
|
||||
self.uid = uid
|
||||
self.to_node = to_node
|
||||
self.eqpt = eqpt
|
||||
|
||||
|
||||
def open_sheet_with_error_handling(wb: WorkbookType, sheet_name: str, is_xlsx: bool) -> SheetType:
|
||||
"""Opens a sheet from the workbook with error handling.
|
||||
|
||||
:param wb: The opened workbook.
|
||||
:type wb: WorkbookType
|
||||
:param sheet_name: Name of the sheet to open.
|
||||
:type sheet_name: str
|
||||
:param is_xlsx: Boolean indicating if the file is XLSX format.
|
||||
:type is_xlsx: bool
|
||||
:return: The worksheet object.
|
||||
:rtype: SheetType
|
||||
:raises NetworkTopologyError: If the sheet is not found.
|
||||
"""
|
||||
try:
|
||||
sheet = get_sheet(wb, sheet_name, is_xlsx)
|
||||
return sheet
|
||||
except XLS_EXCEPTIONS as exc:
|
||||
msg = f'Error: no {sheet_name} sheet in the file.'
|
||||
raise NetworkTopologyError(msg) from exc
|
||||
|
||||
|
||||
def read_excel(input_filename: Path) -> Dict[str, Node]:
|
||||
"""Reads the 'Nodes' and 'Links' sheets from an Excel file to build a network graph.
|
||||
|
||||
:param input_filename: Path to the Excel file.
|
||||
:type input_filename: Path
|
||||
:return: Dictionary of nodes with their connectivity and equipment type.
|
||||
:rtype: Dict[str, Node]
|
||||
"""
|
||||
wobo, is_xlsx = generic_open_workbook(input_filename)
|
||||
links_sheet = open_sheet_with_error_handling(wobo, 'Links', is_xlsx)
|
||||
get_rows_links = fast_get_sheet_rows(links_sheet) if is_xlsx else None
|
||||
|
||||
nodes = {}
|
||||
for row in all_rows(links_sheet, is_xlsx, start=5, get_rows=get_rows_links):
|
||||
node_a, node_z = row[0].value, row[1].value
|
||||
# Add connection in both directions
|
||||
for node1, node2 in [(node_a, node_z), (node_z, node_a)]:
|
||||
if node1 in nodes:
|
||||
nodes[node1].to_node.append(node2)
|
||||
else:
|
||||
nodes[node1] = Node(node1, [node2])
|
||||
|
||||
nodes_sheet = open_sheet_with_error_handling(wobo, 'Nodes', is_xlsx)
|
||||
get_rows_nodes = fast_get_sheet_rows(nodes_sheet) if is_xlsx else None
|
||||
|
||||
for row in all_rows(nodes_sheet, is_xlsx, start=5, get_rows=get_rows_nodes):
|
||||
node = row[0].value
|
||||
eqpt = row[6].value
|
||||
if node not in nodes:
|
||||
raise NetworkTopologyError(f'Error: node {node} is not listed on the links sheet.')
|
||||
if eqpt == 'ILA' and len(nodes[node].to_node) != 2:
|
||||
degree = len(nodes[node].to_node)
|
||||
raise NetworkTopologyError(f'Error: node {node} has an incompatible node degree ({degree}) '
|
||||
+ 'for its equipment type (ILA).')
|
||||
if eqpt == '' and len(nodes[node].to_node) == 2:
|
||||
nodes[node].eqpt = 'ILA'
|
||||
elif eqpt == '' and len(nodes[node].to_node) != 2:
|
||||
nodes[node].eqpt = 'ROADM'
|
||||
else:
|
||||
nodes[node].eqpt = eqpt
|
||||
return nodes
|
||||
|
||||
|
||||
def create_eqpt_template(nodes: Dict[str, Node], input_filename: Path, output_filename: Optional[Path] = None):
|
||||
"""Creates a CSV template to help users populate equipment types for nodes.
|
||||
|
||||
:param nodes: Dictionary of nodes.
|
||||
:type nodes: Dict[str, Node]
|
||||
:param input_filename: Path to the original Excel file.
|
||||
:type input_filename: Path
|
||||
:param output_filename: Path to save the CSV file; generated if None.
|
||||
:type output_filename: Optional(Path)
|
||||
"""
|
||||
if output_filename is None:
|
||||
output_filename = input_filename.parent / (input_filename.with_suffix('').stem + '_eqpt_sheet.csv')
|
||||
with open(output_filename, mode='w', encoding='utf-8', newline='') as output_file:
|
||||
output_writer = csv.writer(output_file, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL)
|
||||
amp_header = ['amp_type', 'att_in', 'amp_gain', 'tilt', 'att_out', 'delta_p']
|
||||
output_writer.writerow(['node_a', 'node_z'] + amp_header + amp_header)
|
||||
for node in nodes.values():
|
||||
if node.eqpt == 'ILA':
|
||||
output_writer.writerow([node.uid, node.to_node[0]])
|
||||
if node.eqpt == 'ROADM':
|
||||
for to_node in node.to_node:
|
||||
output_writer.writerow([node.uid, to_node])
|
||||
msg = f'File {output_filename} successfully created.'
|
||||
logger.info(msg)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
ARGS = PARSER.parse_args()
|
||||
create_eqpt_template(read_excel(ARGS.workbook), ARGS.workbook, ARGS.output)
|
||||
BIN
tests/data/create_eqpt_sheet/testTopology.xls
Normal file
BIN
tests/data/create_eqpt_sheet/testTopology.xls
Normal file
Binary file not shown.
34
tests/data/create_eqpt_sheet/testTopology_eqpt_sheet.csv
Normal file
34
tests/data/create_eqpt_sheet/testTopology_eqpt_sheet.csv
Normal file
@@ -0,0 +1,34 @@
|
||||
node_a,node_z,amp_type,att_in,amp_gain,tilt,att_out,delta_p,amp_type,att_in,amp_gain,tilt,att_out,delta_p
|
||||
Lannion_CAS,Corlay
|
||||
Lannion_CAS,Stbrieuc
|
||||
Lannion_CAS,Morlaix
|
||||
Lorient_KMA,Loudeac
|
||||
Lorient_KMA,Vannes_KBE
|
||||
Lorient_KMA,Quimper
|
||||
Vannes_KBE,Lorient_KMA
|
||||
Vannes_KBE,Ploermel
|
||||
Stbrieuc,Lannion_CAS
|
||||
Rennes_STA,Stbrieuc
|
||||
Rennes_STA,Ploermel
|
||||
Brest_KLA,Morlaix
|
||||
Brest_KLA,Quimper
|
||||
Quimper,Brest_KLA
|
||||
Ploermel,Vannes_KBE
|
||||
a,b
|
||||
a,c
|
||||
b,a
|
||||
b,f
|
||||
c,a
|
||||
c,d
|
||||
c,f
|
||||
d,c
|
||||
d,e
|
||||
f,c
|
||||
f,b
|
||||
f,h
|
||||
e,d
|
||||
e,g
|
||||
g,e
|
||||
g,h
|
||||
h,f
|
||||
h,g
|
||||
|
BIN
tests/data/create_eqpt_sheet/test_ces_key_err.xls
Normal file
BIN
tests/data/create_eqpt_sheet/test_ces_key_err.xls
Normal file
Binary file not shown.
BIN
tests/data/create_eqpt_sheet/test_ces_no_err.xls
Normal file
BIN
tests/data/create_eqpt_sheet/test_ces_no_err.xls
Normal file
Binary file not shown.
BIN
tests/data/create_eqpt_sheet/test_ces_node_degree_err.xls
Normal file
BIN
tests/data/create_eqpt_sheet/test_ces_node_degree_err.xls
Normal file
Binary file not shown.
9
tests/data/create_eqpt_sheet/test_create_eqpt_sheet.csv
Normal file
9
tests/data/create_eqpt_sheet/test_create_eqpt_sheet.csv
Normal file
@@ -0,0 +1,9 @@
|
||||
node_a,node_z,amp_type,att_in,amp_gain,tilt,att_out,delta_p,amp_type,att_in,amp_gain,tilt,att_out,delta_p
|
||||
a,b
|
||||
a,d
|
||||
a,e
|
||||
c,b
|
||||
c,d
|
||||
c,e
|
||||
d,c
|
||||
e,a
|
||||
|
95
tests/test_create_eqpt_sheet.py
Normal file
95
tests/test_create_eqpt_sheet.py
Normal file
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
# test_create_eqpt_sheet
|
||||
# Copyright (C) 2025 Telecom Infra Project and GNPy contributors
|
||||
# see AUTHORS.rst for a list of contributors
|
||||
|
||||
"""
|
||||
Checks create_eqpt_sheet.py: verify that output is as expected
|
||||
"""
|
||||
from pathlib import Path
|
||||
from os import symlink, unlink
|
||||
|
||||
import pytest
|
||||
from gnpy.tools.create_eqpt_sheet import Node, read_excel, create_eqpt_template
|
||||
from gnpy.core.exceptions import NetworkTopologyError
|
||||
|
||||
|
||||
TEST_DIR = Path(__file__).parent
|
||||
DATA_DIR = TEST_DIR / 'data' / 'create_eqpt_sheet'
|
||||
|
||||
TEST_FILE_NO_ERR = DATA_DIR / 'test_ces_no_err.xls'
|
||||
|
||||
TEST_FILE = 'testTopology.xls'
|
||||
EXPECTED_OUTPUT = DATA_DIR / 'testTopology_eqpt_sheet.csv'
|
||||
|
||||
TEST_FILE_NODE_DEGREE_ERR = DATA_DIR / 'test_ces_node_degree_err.xls'
|
||||
TEST_FILE_KEY_ERR = DATA_DIR / 'test_ces_key_err.xls'
|
||||
TEST_OUTPUT_FILE_CSV = DATA_DIR / 'test_create_eqpt_sheet.csv'
|
||||
PYTEST_OUTPUT_FILE_NAME = 'test_ces_pytest_output.csv'
|
||||
EXPECTED_OUTPUT_CSV_NAME = 'testTopology_eqpt_sheet.csv'
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def test_node():
|
||||
"""Fixture of simple Node."""
|
||||
return Node(1, ['A', 'B'], 'ROADM')
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def test_nodes_list():
|
||||
"""Fixture of nodes list parsing."""
|
||||
return read_excel(TEST_FILE_NO_ERR)
|
||||
|
||||
|
||||
def test_node_append(test_node):
|
||||
"""Test Node's append method."""
|
||||
expected = {'uid': 1, 'to_node': ['A', 'B', 'C'], 'eqpt': 'ROADM'}
|
||||
test_node.to_node.append('C')
|
||||
assert test_node.__dict__ == expected
|
||||
|
||||
|
||||
def test_read_excel(test_nodes_list):
|
||||
"""Test method read_excel()."""
|
||||
expected = {}
|
||||
expected['a'] = Node('a', ['b', 'd', 'e'], 'ROADM')
|
||||
expected['b'] = Node('b', ['a', 'c'], 'FUSED')
|
||||
expected['c'] = Node('c', ['b', 'd', 'e'], 'ROADM')
|
||||
expected['d'] = Node('d', ['c', 'a'], 'ILA')
|
||||
expected['e'] = Node('e', ['a', 'c'], 'ILA')
|
||||
assert set(test_nodes_list) == set(expected)
|
||||
|
||||
|
||||
def test_read_excel_node_degree_err():
|
||||
"""Test node degree error (eqpt == 'ILA' and len(nodes[node].to_node) != 2)."""
|
||||
with pytest.raises(NetworkTopologyError):
|
||||
_ = read_excel(TEST_FILE_NODE_DEGREE_ERR)
|
||||
|
||||
|
||||
def test_read_excel_key_err():
|
||||
"""Test node not listed on the links sheets."""
|
||||
with pytest.raises(NetworkTopologyError):
|
||||
_ = read_excel(TEST_FILE_KEY_ERR)
|
||||
|
||||
|
||||
def test_create_eqpt_template(tmpdir, test_nodes_list):
|
||||
"""Test method create_eqt_template()."""
|
||||
create_eqpt_template(test_nodes_list, DATA_DIR / TEST_FILE_NO_ERR,
|
||||
tmpdir / PYTEST_OUTPUT_FILE_NAME)
|
||||
with open((tmpdir / PYTEST_OUTPUT_FILE_NAME).strpath, 'r') as actual, \
|
||||
open(TEST_OUTPUT_FILE_CSV, 'r') as expected:
|
||||
assert set(actual.readlines()) == set(expected.readlines())
|
||||
unlink(tmpdir / PYTEST_OUTPUT_FILE_NAME)
|
||||
|
||||
|
||||
def test_create_eqpt(tmpdir):
|
||||
"""Test method create_eqt_template()."""
|
||||
# create a fake file in tempdir in order to test the automatic output filename generation
|
||||
symlink(DATA_DIR / TEST_FILE, tmpdir / TEST_FILE)
|
||||
create_eqpt_template(read_excel(DATA_DIR / TEST_FILE), Path((tmpdir / TEST_FILE).strpath))
|
||||
with open(DATA_DIR / EXPECTED_OUTPUT_CSV_NAME, 'r') as expected, \
|
||||
open(tmpdir / EXPECTED_OUTPUT_CSV_NAME, 'r') as actual:
|
||||
assert set(actual.readlines()) == set(expected.readlines())
|
||||
unlink(tmpdir / EXPECTED_OUTPUT_CSV_NAME)
|
||||
Reference in New Issue
Block a user