mirror of
				https://github.com/Telecominfraproject/oopt-gnpy.git
				synced 2025-10-30 17:47:50 +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> | - Raj Nagarajan (Lumentum) <raj.nagarajan@lumentum.com> | ||||||
| - Renato Ambrosone (Politecnico di Torino) <renato.ambrosone@polito.it> | - Renato Ambrosone (Politecnico di Torino) <renato.ambrosone@polito.it> | ||||||
| - Roberts Miculens (Lattelecom) <roberts.miculens@lattelecom.lv> | - Roberts Miculens (Lattelecom) <roberts.miculens@lattelecom.lv> | ||||||
|  | - Rodrigo Sasse David (Orange) <rodrigo.sassedavid@orange.com> | ||||||
| - Sami Alavi (NUST) <sami.mansooralavi1999@gmail.com> | - Sami Alavi (NUST) <sami.mansooralavi1999@gmail.com> | ||||||
| - Shengxiang Zhu (University of Arizona) <szhu@email.arizona.edu> | - Shengxiang Zhu (University of Arizona) <szhu@email.arizona.edu> | ||||||
| - Stefan Melin (Telia Company) <Stefan.Melin@teliacompany.com> | - 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
	 EstherLerouzic
					EstherLerouzic