diff --git a/changes/429.added b/changes/429.added new file mode 100644 index 00000000..76bf4870 --- /dev/null +++ b/changes/429.added @@ -0,0 +1 @@ +Added Hawaiki Provider, Parser and Tests diff --git a/circuit_maintenance_parser/__init__.py b/circuit_maintenance_parser/__init__.py index 10f68024..8b9b278d 100644 --- a/circuit_maintenance_parser/__init__.py +++ b/circuit_maintenance_parser/__init__.py @@ -28,6 +28,7 @@ GenericProvider, GlobalCloudXchange, Google, + Hawaiki, Lumen, Megaport, Momentum, @@ -65,6 +66,7 @@ GlobalCloudXchange, Google, GTT, + Hawaiki, HGC, Lumen, Megaport, diff --git a/circuit_maintenance_parser/parsers/hawaiki.py b/circuit_maintenance_parser/parsers/hawaiki.py new file mode 100644 index 00000000..372b6f4f --- /dev/null +++ b/circuit_maintenance_parser/parsers/hawaiki.py @@ -0,0 +1,93 @@ +"""Hawaiki parser.""" + +import logging +import re +from typing import Dict, List + +from dateutil import parser + +from circuit_maintenance_parser.output import CircuitImpact, Impact, Status +from circuit_maintenance_parser.parser import EmailSubjectParser, Text + +logger = logging.getLogger(__name__) +IMPACT_LINE_RE = re.compile(r"^Service impact:\s*(.+)$", re.IGNORECASE | re.MULTILINE) + + +class SubjectParserHawaiki1(EmailSubjectParser): + """Extract maintenance_id from Hawaiki email subject.""" + + def parse_subject(self, subject: str) -> List[Dict]: + """Extract ticket number from subject like '[Ticket#2026072290001004] ...'.""" + match = re.search(r"\[Ticket#(\S+)\]", subject) + if match: + return [{"maintenance_id": match.group(1)}] + return [{}] + + +class TextParserHawaiki1(Text): + """Parse plain-text body of Hawaiki initial maintenance notifications.""" + + def parse_text(self, text: str) -> List[Dict]: + """Extract attributes from Hawaiki notification. + + Example: + Service ID: HW-CID-12345 + + Maintenance Window + Start: 2026-August-05 20:00 UTC + End: 2026-August-05 21:00 UTC + """ + data: Dict = { + "circuits": [], + "status": Status.CONFIRMED, + "account": "Customer info unavailable", + "summary": "Hawaiki maintenance notification", + } + + lines = text.splitlines() + in_maintenance_window = False + + # All circuits in a Hawaiki notification share one "Service impact" line, and it appears + # after the "Service ID" lines, so resolve it up front. + impact = self._parse_impact(text) + + for line in lines: + line = line.strip() + + match = re.match(r"Service ID:\s*(.+)", line) + if match: + circuit_id = match.group(1).strip() + data["circuits"].append(CircuitImpact(circuit_id=circuit_id, impact=impact)) + continue + + if line == "Maintenance Window": + in_maintenance_window = True + continue + + if in_maintenance_window: + match = re.match(r"Start:\s*(.+)", line) + if match: + data["start"] = self.dt2ts(parser.parse(match.group(1).strip())) + continue + match = re.match(r"End:\s*(.+)", line) + if match: + data["end"] = self.dt2ts(parser.parse(match.group(1).strip())) + in_maintenance_window = False + continue + + return [data] + + @staticmethod + def _parse_impact(text: str) -> Impact: + """Map the notification's single 'Service impact' line to an Impact.""" + match = IMPACT_LINE_RE.search(text) + if not match: + logger.warning("No Hawaiki 'Service impact' line found, defaulting to OUTAGE.") + return Impact.OUTAGE + impact_text = match.group(1).strip().lower() + if "no service impact" in impact_text: + return Impact.NO_IMPACT + if "redundancy" in impact_text or "re-routed" in impact_text: + return Impact.REDUCED_REDUNDANCY + logger.warning("Unrecognized Hawaiki service impact %r, defaulting to OUTAGE.", impact_text) + return Impact.OUTAGE diff --git a/circuit_maintenance_parser/provider.py b/circuit_maintenance_parser/provider.py index 09ce9bd3..20e6fcaa 100644 --- a/circuit_maintenance_parser/provider.py +++ b/circuit_maintenance_parser/provider.py @@ -28,6 +28,7 @@ from circuit_maintenance_parser.parsers.globalcloudxchange import HtmlParserGcx1, SubjectParserGcx1 from circuit_maintenance_parser.parsers.google import HtmlParserGoogle1, SubjectParserGoogle1 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.lumen import HtmlParserLumen1 from circuit_maintenance_parser.parsers.megaport import HtmlParserMegaport1 @@ -648,6 +649,16 @@ class Windstream(GenericProvider): _default_organizer = PrivateAttr("wci.maintenance.notifications@windstream.com") +class Hawaiki(GenericProvider): + """Hawaiki provider custom class.""" + + _include_filter = PrivateAttr({"text/plain": ["Maintenance Window"], "text/html": ["Maintenance Window"]}) + _processors: List[GenericProcessor] = PrivateAttr( + [CombinedProcessor(data_parsers=[EmailDateParser, TextParserHawaiki1, SubjectParserHawaiki1])] + ) + _default_organizer = PrivateAttr("support@bw-digital.com") + + class Zayo(GenericProvider): """Zayo provider custom class.""" diff --git a/tests/unit/data/hawaiki/hawaiki1.eml b/tests/unit/data/hawaiki/hawaiki1.eml new file mode 100644 index 00000000..7bff4174 --- /dev/null +++ b/tests/unit/data/hawaiki/hawaiki1.eml @@ -0,0 +1,67 @@ +Date: Wed, 5 Aug 2026 20:00:00 +0000 +From: HAWAIKI NOC +To: noc@example.com +Subject: [Ticket#2026072290001004] Hawaiki Submarine Cable: Hazardous Condition Notification 2026-Aug-05 20:00 UTC +Content-Type: multipart/mixed; boundary="----------=_boundary-hawaiki1" +MIME-Version: 1.0 + +------------=_boundary-hawaiki1 +Content-Type: text/plain; charset="utf-8" +Content-Transfer-Encoding: 7bit + +Dear valued Hawaiki customer, + +Please be advised of maintenance works to be carried out on Hawaiki network. +This work is NOT EXPECTED to impact your service identified below. + +Service ID: HW-CID-12345 + +Reason for Notification: Maintenance works to be carried out in Hawaiki network + +Maintenance Window +Start: 2026-August-05 20:00 UTC +End: 2026-August-05 21:00 UTC + +Contingency Window +Start: 2026-August-06 20:00 UTC +End: 2026-August-06 21:00 UTC + +Location of Maintenance: Pacific City + +Service impact: No service impact expected + +Hawaiki NOC will monitor the progress of works throughout the maintenance +window, keep you informed of developments as they occur and advise when the +activity is complete. + +Please contact the HAWAIKI SUBMARINE CABLE Network Operations Centre (NOC) +with any questions of concerns. + +----- +Best Regards, +HAWAIKI NOC + +------------=_boundary-hawaiki1 +Content-Type: text/html; charset="utf-8" +Content-Transfer-Encoding: 7bit + + +

Dear valued Hawaiki customer,

+

Please be advised of maintenance works to be carried out on Hawaiki network. This work is NOT EXPECTED to impact your service identified below.

+

Service ID: HW-CID-12345

+

Reason for Notification: Maintenance works to be carried out in Hawaiki network

+

Maintenance Window
+Start: 2026-August-05 20:00 UTC
+End: 2026-August-05 21:00 UTC
+
+Contingency Window
+Start: 2026-August-06 20:00 UTC
+End: 2026-August-06 21:00 UTC
+
+Location of Maintenance: Pacific City
+
+Service impact: No service impact expected

+

Hawaiki NOC will monitor the progress of works throughout the maintenance window.

+ + +------------=_boundary-hawaiki1-- diff --git a/tests/unit/data/hawaiki/hawaiki1_result.json b/tests/unit/data/hawaiki/hawaiki1_result.json new file mode 100644 index 00000000..e1119934 --- /dev/null +++ b/tests/unit/data/hawaiki/hawaiki1_result.json @@ -0,0 +1,21 @@ +[ + { + "account": "Customer info unavailable", + "circuits": [ + { + "circuit_id": "HW-CID-12345", + "impact": "NO-IMPACT" + } + ], + "end": 1785963600, + "maintenance_id": "2026072290001004", + "organizer": "support@bw-digital.com", + "provider": "hawaiki", + "sequence": 1, + "stamp": 1785960000, + "start": 1785960000, + "status": "CONFIRMED", + "summary": "Hawaiki maintenance notification", + "uid": "0" + } +] diff --git a/tests/unit/data/hawaiki/hawaiki_completion.eml b/tests/unit/data/hawaiki/hawaiki_completion.eml new file mode 100644 index 00000000..4c49d4bb --- /dev/null +++ b/tests/unit/data/hawaiki/hawaiki_completion.eml @@ -0,0 +1,38 @@ +Date: Wed, 5 Aug 2026 21:34:23 +0000 +From: HAWAIKI NOC +To: noc@example.com +Subject: Re: [Ticket#2026072290001004] Hawaiki Submarine Cable: Hazardous Condition Notification 2026-Aug-05 20:00 UTC +Content-Type: multipart/alternative; boundary="----------=_boundary-hawaiki-completion" +MIME-Version: 1.0 + +------------=_boundary-hawaiki-completion +Content-Type: text/plain; charset="utf-8" +Content-Transfer-Encoding: 7bit + +Dear Valued Hawaiki Customer, + +Please be informed that the maintenance has been completed. Contingency window +will not be required. + +Ticket Reference: +2026072290001004 + +----- +Best Regards, +HAWAIKI NOC + +------------=_boundary-hawaiki-completion +Content-Type: text/html; charset="utf-8" +Content-Transfer-Encoding: 7bit + + + +

Dear Valued Hawaiki Customer,

+

Please be informed that the maintenance has been completed. Contingency window will not be required.

+

Ticket Reference:
+2026072290001004

+

Best Regards,
+HAWAIKI NOC

+ + +------------=_boundary-hawaiki-completion-- diff --git a/tests/unit/test_e2e.py b/tests/unit/test_e2e.py index d0dd32df..661c1712 100644 --- a/tests/unit/test_e2e.py +++ b/tests/unit/test_e2e.py @@ -33,6 +33,7 @@ GenericProvider, GlobalCloudXchange, Google, + Hawaiki, Lumen, Megaport, Momentum, @@ -599,6 +600,24 @@ Path(dir_path, "data", "google", "google9_completed_no_end_time_result.json"), ], ), + # Hawaiki - completion/update emails contain no "Maintenance Window" and are filtered to [] + ( + Hawaiki, + [ + ("email", Path(dir_path, "data", "hawaiki", "hawaiki_completion.eml")), + ], + [], + ), + # Hawaiki - initial maintenance notification with Maintenance Window block + ( + Hawaiki, + [ + ("email", Path(dir_path, "data", "hawaiki", "hawaiki1.eml")), + ], + [ + Path(dir_path, "data", "hawaiki", "hawaiki1_result.json"), + ], + ), # Lumen ( Lumen,