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:
EstherLerouzic
2021-11-04 15:56:01 +01:00
parent 0bc1fb3bf8
commit a0758d0da5
10 changed files with 288 additions and 0 deletions

View File

@@ -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>

View 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)

Binary file not shown.

View 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
1 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
2 Lannion_CAS,Corlay
3 Lannion_CAS,Stbrieuc
4 Lannion_CAS,Morlaix
5 Lorient_KMA,Loudeac
6 Lorient_KMA,Vannes_KBE
7 Lorient_KMA,Quimper
8 Vannes_KBE,Lorient_KMA
9 Vannes_KBE,Ploermel
10 Stbrieuc,Lannion_CAS
11 Rennes_STA,Stbrieuc
12 Rennes_STA,Ploermel
13 Brest_KLA,Morlaix
14 Brest_KLA,Quimper
15 Quimper,Brest_KLA
16 Ploermel,Vannes_KBE
17 a,b
18 a,c
19 b,a
20 b,f
21 c,a
22 c,d
23 c,f
24 d,c
25 d,e
26 f,c
27 f,b
28 f,h
29 e,d
30 e,g
31 g,e
32 g,h
33 h,f
34 h,g

Binary file not shown.

Binary file not shown.

View 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
1 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
2 a,b
3 a,d
4 a,e
5 c,b
6 c,d
7 c,e
8 d,c
9 e,a

View 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)