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 00000000..66fdbd22 Binary files /dev/null and b/tests/unit/data/kpn/kpn1.xlsx differ 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 00000000..ae00a1e8 Binary files /dev/null and b/tests/unit/data/kpn/kpn2.xlsx differ 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,