From a43823856feb1aa216393435a672eb9f78ebb25b Mon Sep 17 00:00:00 2001 From: mkekez-sie Date: Mon, 17 Aug 2026 16:28:52 +0930 Subject: [PATCH] Add KPN provider, csv parser and tests --- changes/432.added | 1 + circuit_maintenance_parser/__init__.py | 2 + circuit_maintenance_parser/parsers/kpn.py | 116 ++++++++++++++++++ circuit_maintenance_parser/provider.py | 10 ++ tests/unit/data/kpn/kpn1.eml | 115 +++++++++++++++++ tests/unit/data/kpn/kpn1.xlsx | Bin 0 -> 5270 bytes tests/unit/data/kpn/kpn1_result.json | 25 ++++ .../data/kpn/kpn1_xlsx_parser_result.json | 20 +++ tests/unit/data/kpn/kpn2.eml | 113 +++++++++++++++++ tests/unit/data/kpn/kpn2.xlsx | Bin 0 -> 5187 bytes tests/unit/data/kpn/kpn2_result.json | 21 ++++ .../data/kpn/kpn2_xlsx_parser_result.json | 16 +++ tests/unit/test_e2e.py | 20 +++ tests/unit/test_parsers.py | 12 ++ 14 files changed, 471 insertions(+) create mode 100644 changes/432.added create mode 100644 circuit_maintenance_parser/parsers/kpn.py create mode 100644 tests/unit/data/kpn/kpn1.eml create mode 100644 tests/unit/data/kpn/kpn1.xlsx create mode 100644 tests/unit/data/kpn/kpn1_result.json create mode 100644 tests/unit/data/kpn/kpn1_xlsx_parser_result.json create mode 100644 tests/unit/data/kpn/kpn2.eml create mode 100644 tests/unit/data/kpn/kpn2.xlsx create mode 100644 tests/unit/data/kpn/kpn2_result.json create mode 100644 tests/unit/data/kpn/kpn2_xlsx_parser_result.json diff --git a/changes/432.added b/changes/432.added new file mode 100644 index 00000000..b9867528 --- /dev/null +++ b/changes/432.added @@ -0,0 +1 @@ +Added KPN Provider, Parser and Tests diff --git a/circuit_maintenance_parser/__init__.py b/circuit_maintenance_parser/__init__.py index 8b9b278d..bc9219a1 100644 --- a/circuit_maintenance_parser/__init__.py +++ b/circuit_maintenance_parser/__init__.py @@ -13,6 +13,7 @@ FLAG, GTT, HGC, + KPN, NTT, PCCW, RETN, @@ -68,6 +69,7 @@ GTT, Hawaiki, HGC, + KPN, Lumen, Megaport, Momentum, diff --git a/circuit_maintenance_parser/parsers/kpn.py b/circuit_maintenance_parser/parsers/kpn.py new file mode 100644 index 00000000..f0b63a96 --- /dev/null +++ b/circuit_maintenance_parser/parsers/kpn.py @@ -0,0 +1,116 @@ +"""KPN parser.""" + +import logging +from datetime import datetime, timezone +from typing import Dict, List + +from circuit_maintenance_parser.errors import ParserError +from circuit_maintenance_parser.output import CircuitImpact, Impact, Status +from circuit_maintenance_parser.parser import Xlsx + +logger = logging.getLogger(__name__) + +# KPN spreadsheet column names (bilingual Dutch | English headers) +COL_CIRCUIT_ID = "V/G/A-nummer | V/G/A number" +COL_MAINTENANCE_ID = "Astrid/IDA" +COL_START_DATE = "Startdatum | Start date" +COL_START_TIME = "Starttijd | Start time" +COL_END_DATE = "Einddatum | End date" +COL_END_TIME = "Eindtijd | End time" +COL_DOWNTIME = "Onderbrekingsduur | Downtime" +COL_LINE_ID = "Lijnbenaming | Line ID" +COL_STATUS = "Aankondiging" +COL_ACCOUNT = "Bedrijfsnaam A-locatie | Company A-location" + +# Dutch status values in the Aankondiging column +_STATUS_MAP = { + "aankondiging": Status.CONFIRMED, # announcement + "wijziging": Status.CONFIRMED, # change/reschedule — new times supersede old + "annulering": Status.CANCELLED, # cancellation + "afsluiting": Status.COMPLETED, # closure/completed +} + + +class XlsxParserKPN1(Xlsx): + """Parse KPN XLSX maintenance notification spreadsheets.""" + + @staticmethod + def parse_xlsx(records: List[Dict]) -> List[Dict]: + """Extract maintenance data from KPN Trans_*.xlsx attachment.""" + if not records: + raise ParserError("Empty KPN spreadsheet.") + + first = records[0] + _check_required_columns(first) + + maintenance_id = str(first[COL_MAINTENANCE_ID]).strip() + status = _parse_status(first) + start = _parse_datetime(first[COL_START_DATE], first[COL_START_TIME]) + end = _parse_datetime(first[COL_END_DATE], first[COL_END_TIME]) + account = str(first.get(COL_ACCOUNT, "")).strip() or "Customer info unavailable" + + circuits = [] + for row in records: + circuit_id = str(row[COL_CIRCUIT_ID]).strip() + if not circuit_id: + continue + impact = _parse_impact(row) + circuits.append(CircuitImpact(circuit_id=circuit_id, impact=impact)) + + line_id = str(first.get(COL_LINE_ID, "")).strip() + summary = f"KPN maintenance {maintenance_id}" + (f" ({line_id})" if line_id else "") + + return [ + { + "maintenance_id": maintenance_id, + "status": status, + "start": int(start.timestamp()), + "end": int(end.timestamp()), + "circuits": circuits, + "summary": summary, + "account": account, + } + ] + + +def _check_required_columns(row: Dict) -> None: + required = [COL_CIRCUIT_ID, COL_MAINTENANCE_ID, COL_START_DATE, COL_START_TIME, COL_END_DATE, COL_END_TIME] + missing = [c for c in required if c not in row] + if missing: + raise ParserError(f"KPN spreadsheet missing required columns: {missing}") + + +def _parse_datetime(date_str: str, time_str: str) -> datetime: + """Combine KPN date (DD-MM-YYYY) and time (HH:MM) into a UTC-aware datetime.""" + try: + dt = datetime.strptime(f"{date_str} {time_str}", "%d-%m-%Y %H:%M") + return dt.replace(tzinfo=timezone.utc) + except (ValueError, TypeError) as exc: + raise ParserError(f"KPN could not parse date/time '{date_str} {time_str}': {exc}") from exc + + +def _parse_status(row: Dict) -> Status: + """Map the Dutch status column value to a Status enum.""" + raw = str(row.get(COL_STATUS, "")).strip().lower() + status = _STATUS_MAP.get(raw) + if status is None: + logger.warning("Unrecognized KPN status %r, defaulting to CONFIRMED.", raw) + return Status.CONFIRMED + return status + + +def _parse_impact(row: Dict) -> Impact: + """Derive impact from the downtime-minutes column. + + KPN's 'Onderbrekingsduur | Downtime' field is an integer number of minutes. + Zero minutes means no service disruption; any positive value is an outage. + """ + raw = row.get(COL_DOWNTIME) + try: + minutes = int(raw) + except (TypeError, ValueError): + logger.warning("KPN could not parse downtime %r, defaulting to OUTAGE.", raw) + return Impact.OUTAGE + if minutes == 0: + return Impact.NO_IMPACT + return Impact.OUTAGE diff --git a/circuit_maintenance_parser/provider.py b/circuit_maintenance_parser/provider.py index 20e6fcaa..c922bd18 100644 --- a/circuit_maintenance_parser/provider.py +++ b/circuit_maintenance_parser/provider.py @@ -30,6 +30,7 @@ from circuit_maintenance_parser.parsers.gtt import HtmlParserGTT1 from circuit_maintenance_parser.parsers.hawaiki import SubjectParserHawaiki1, TextParserHawaiki1 from circuit_maintenance_parser.parsers.hgc import HtmlParserHGC1, HtmlParserHGC2, SubjectParserHGC1 +from circuit_maintenance_parser.parsers.kpn import XlsxParserKPN1 from circuit_maintenance_parser.parsers.lumen import HtmlParserLumen1 from circuit_maintenance_parser.parsers.megaport import HtmlParserMegaport1 from circuit_maintenance_parser.parsers.momentum import HtmlParserMomentum1, SubjectParserMomentum1 @@ -659,6 +660,15 @@ class Hawaiki(GenericProvider): _default_organizer = PrivateAttr("support@bw-digital.com") +class KPN(GenericProvider): + """KPN provider custom class.""" + + _processors: List[GenericProcessor] = PrivateAttr( + [CombinedProcessor(data_parsers=[EmailDateParser, XlsxParserKPN1])] + ) + _default_organizer = PrivateAttr("gwc-noreply@kpn.com") + + class Zayo(GenericProvider): """Zayo provider custom class.""" diff --git a/tests/unit/data/kpn/kpn1.eml b/tests/unit/data/kpn/kpn1.eml new file mode 100644 index 00000000..ef5b46ac --- /dev/null +++ b/tests/unit/data/kpn/kpn1.eml @@ -0,0 +1,115 @@ +Content-Type: multipart/mixed; boundary="===============7215816408978557903==" +MIME-Version: 1.0 +From: KPN gepland werk +Reply-To: gwc-noreply@kpn.com +To: test@example.com +Subject: Aankondiging geplande werkzaamheden KPN +Date: Tue, 15 Sep 2026 08:00:00 +0000 + +--===============7215816408978557903== +Content-Type: text/plain; charset="us-ascii" +MIME-Version: 1.0 +Content-Transfer-Encoding: 7bit + +KPN maintenance notification. See attached spreadsheet. +--===============7215816408978557903== +Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet +MIME-Version: 1.0 +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="kpn1.xlsx" + +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 +LnhtbFBLBQYAAAAACQAJAD4CAABCEgAAAAA= + +--===============7215816408978557903==-- diff --git a/tests/unit/data/kpn/kpn1.xlsx b/tests/unit/data/kpn/kpn1.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..66fdbd22d3fc9d85aa60641bdb7723481eaa9e29 GIT binary patch literal 5270 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 Ze~}eT6Av?w0ssgxr#vQyD^vb@`#+b+7@`0G literal 0 HcmV?d00001 diff --git a/tests/unit/data/kpn/kpn1_result.json b/tests/unit/data/kpn/kpn1_result.json new file mode 100644 index 00000000..05d58291 --- /dev/null +++ b/tests/unit/data/kpn/kpn1_result.json @@ -0,0 +1,25 @@ +[ + { + "account": "Dummy Corp A", + "circuits": [ + { + "circuit_id": "A000001", + "impact": "OUTAGE" + }, + { + "circuit_id": "B000002", + "impact": "OUTAGE" + } + ], + "end": 1789452000, + "maintenance_id": "CT00000000001", + "organizer": "gwc-noreply@kpn.com", + "provider": "kpn", + "sequence": 1, + "stamp": 1789459200, + "start": 1789437600, + "status": "CONFIRMED", + "summary": "KPN maintenance CT00000000001 (DUMMY-LINE-01)", + "uid": "0" + } +] diff --git a/tests/unit/data/kpn/kpn1_xlsx_parser_result.json b/tests/unit/data/kpn/kpn1_xlsx_parser_result.json new file mode 100644 index 00000000..7570906a --- /dev/null +++ b/tests/unit/data/kpn/kpn1_xlsx_parser_result.json @@ -0,0 +1,20 @@ +[ + { + "account": "Dummy Corp A", + "circuits": [ + { + "circuit_id": "A000001", + "impact": "OUTAGE" + }, + { + "circuit_id": "B000002", + "impact": "OUTAGE" + } + ], + "end": 1789452000, + "maintenance_id": "CT00000000001", + "start": 1789437600, + "status": "CONFIRMED", + "summary": "KPN maintenance CT00000000001 (DUMMY-LINE-01)" + } +] diff --git a/tests/unit/data/kpn/kpn2.eml b/tests/unit/data/kpn/kpn2.eml new file mode 100644 index 00000000..73d8d6da --- /dev/null +++ b/tests/unit/data/kpn/kpn2.eml @@ -0,0 +1,113 @@ +Content-Type: multipart/mixed; boundary="===============0259734084225756266==" +MIME-Version: 1.0 +From: KPN gepland werk +Reply-To: gwc-noreply@kpn.com +To: test@example.com +Subject: Aankondiging geplande werkzaamheden KPN +Date: Tue, 15 Sep 2026 08:00:00 +0000 + +--===============0259734084225756266== +Content-Type: text/plain; charset="us-ascii" +MIME-Version: 1.0 +Content-Transfer-Encoding: 7bit + +KPN maintenance notification. See attached spreadsheet. +--===============0259734084225756266== +Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet +MIME-Version: 1.0 +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="kpn2.xlsx" + +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 + +--===============0259734084225756266==-- diff --git a/tests/unit/data/kpn/kpn2.xlsx b/tests/unit/data/kpn/kpn2.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..ae00a1e8b84ba4581dc6ff416a8f1d321554bfa1 GIT binary patch literal 5187 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 literal 0 HcmV?d00001 diff --git a/tests/unit/data/kpn/kpn2_result.json b/tests/unit/data/kpn/kpn2_result.json new file mode 100644 index 00000000..388f7d28 --- /dev/null +++ b/tests/unit/data/kpn/kpn2_result.json @@ -0,0 +1,21 @@ +[ + { + "account": "Dummy Corp D", + "circuits": [ + { + "circuit_id": "A000003", + "impact": "NO-IMPACT" + } + ], + "end": 1789880400, + "maintenance_id": "CT00000000002", + "organizer": "gwc-noreply@kpn.com", + "provider": "kpn", + "sequence": 1, + "stamp": 1789459200, + "start": 1789866000, + "status": "CANCELLED", + "summary": "KPN maintenance CT00000000002 (DUMMY-LINE-03)", + "uid": "0" + } +] diff --git a/tests/unit/data/kpn/kpn2_xlsx_parser_result.json b/tests/unit/data/kpn/kpn2_xlsx_parser_result.json new file mode 100644 index 00000000..e2a591c3 --- /dev/null +++ b/tests/unit/data/kpn/kpn2_xlsx_parser_result.json @@ -0,0 +1,16 @@ +[ + { + "account": "Dummy Corp D", + "circuits": [ + { + "circuit_id": "A000003", + "impact": "NO-IMPACT" + } + ], + "end": 1789880400, + "maintenance_id": "CT00000000002", + "start": 1789866000, + "status": "CANCELLED", + "summary": "KPN maintenance CT00000000002 (DUMMY-LINE-03)" + } +] diff --git a/tests/unit/test_e2e.py b/tests/unit/test_e2e.py index 661c1712..184ac2a8 100644 --- a/tests/unit/test_e2e.py +++ b/tests/unit/test_e2e.py @@ -18,6 +18,7 @@ FLAG, GTT, HGC, + KPN, NTT, PCCW, RETN, @@ -618,6 +619,25 @@ Path(dir_path, "data", "hawaiki", "hawaiki1_result.json"), ], ), + # KPN + ( + KPN, + [ + ("email", Path(dir_path, "data", "kpn", "kpn1.eml")), + ], + [ + Path(dir_path, "data", "kpn", "kpn1_result.json"), + ], + ), + ( + KPN, + [ + ("email", Path(dir_path, "data", "kpn", "kpn2.eml")), + ], + [ + Path(dir_path, "data", "kpn", "kpn2_result.json"), + ], + ), # Lumen ( Lumen, diff --git a/tests/unit/test_parsers.py b/tests/unit/test_parsers.py index 6b12da56..1d2a48f3 100644 --- a/tests/unit/test_parsers.py +++ b/tests/unit/test_parsers.py @@ -23,6 +23,7 @@ from circuit_maintenance_parser.parsers.google import HtmlParserGoogle1, SubjectParserGoogle1 from circuit_maintenance_parser.parsers.gtt import HtmlParserGTT1 from circuit_maintenance_parser.parsers.hgc import HtmlParserHGC1, HtmlParserHGC2 +from circuit_maintenance_parser.parsers.kpn import XlsxParserKPN1 from circuit_maintenance_parser.parsers.lumen import HtmlParserLumen1 from circuit_maintenance_parser.parsers.megaport import HtmlParserMegaport1 from circuit_maintenance_parser.parsers.momentum import HtmlParserMomentum1 @@ -507,6 +508,17 @@ def default(self, o): Path(dir_path, "data", "hgc", "hgc2.eml"), Path(dir_path, "data", "hgc", "hgc2_html_result.json"), ), + # KPN + ( + XlsxParserKPN1, + Path(dir_path, "data", "kpn", "kpn1.xlsx"), + Path(dir_path, "data", "kpn", "kpn1_xlsx_parser_result.json"), + ), + ( + XlsxParserKPN1, + Path(dir_path, "data", "kpn", "kpn2.xlsx"), + Path(dir_path, "data", "kpn", "kpn2_xlsx_parser_result.json"), + ), # Lumen ( HtmlParserLumen1,