Skip to content

Commit 60319aa

Browse files
authored
Add Pccw parser (#301)
* add pccw parser * add pccw provider * add pccw unit tests * add pccw in readme.md * use tuple type from typing module instead built-in tuple type for supporting python 3.8 * use pccw in all upper case to better match the company name * add e2e test in ical format * add comment for default circuit maintenance status
1 parent 6cbf7bc commit 60319aa

26 files changed

+959
-0
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ By default, there is a `GenericProvider` that supports a `SimpleProcessor` using
6363
- EXA (formerly GTT) (\*)
6464
- NTT
6565
- PacketFabric
66+
- PCCW
6667
- Telstra (\*)
6768

6869
#### Supported providers based on other parsers
@@ -82,6 +83,7 @@ By default, there is a `GenericProvider` that supports a `SimpleProcessor` using
8283
- Megaport
8384
- Momentum
8485
- Netflix (AS2906 only)
86+
- PCCW
8587
- Seaborn
8688
- Sparkle
8789
- Tata

circuit_maintenance_parser/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
Momentum,
2727
Netflix,
2828
PacketFabric,
29+
PCCW,
2930
Seaborn,
3031
Sparkle,
3132
Tata,
@@ -58,6 +59,7 @@
5859
Netflix,
5960
NTT,
6061
PacketFabric,
62+
PCCW,
6163
Seaborn,
6264
Sparkle,
6365
Tata,
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
"""Circuit maintenance parser for PCCW Email notifications."""
2+
import re
3+
from typing import List, Dict, Tuple, Any, ClassVar
4+
from datetime import datetime
5+
6+
from bs4.element import ResultSet # type: ignore
7+
from circuit_maintenance_parser.output import Status
8+
from circuit_maintenance_parser.parser import Html, EmailSubjectParser
9+
10+
11+
class HtmlParserPCCW(Html):
12+
"""Custom Parser for HTML portion of PCCW circuit maintenance notifications."""
13+
14+
DATE_TIME_FORMAT: ClassVar[str] = "%d/%m/%Y %H:%M:%S"
15+
PROVIDER: ClassVar[str] = "PCCW Global"
16+
17+
def parse_html(self, soup: ResultSet) -> List[Dict]:
18+
"""Parse PCCW circuit maintenance email.
19+
20+
Args:
21+
soup: BeautifulSoup ResultSet containing the email HTML content
22+
23+
Returns:
24+
List containing a dictionary with parsed maintenance data
25+
"""
26+
data: Dict[str, Any] = {
27+
"circuits": [],
28+
"provider": self.PROVIDER,
29+
"account": self._extract_account(soup),
30+
}
31+
start_time, end_time = self._extract_maintenance_window(soup)
32+
data["start"] = self.dt2ts(start_time)
33+
data["end"] = self.dt2ts(end_time)
34+
35+
return [data]
36+
37+
def _extract_account(self, soup: ResultSet) -> str:
38+
"""Extract customer account from soup."""
39+
customer_field = soup.find(string=re.compile("Customer Name :", re.IGNORECASE))
40+
return customer_field.split(":")[1].strip()
41+
42+
def _extract_maintenance_window(self, soup: ResultSet) -> Tuple[datetime, datetime]:
43+
"""Extract start and end times from maintenance window."""
44+
datetime_field = soup.find(string=re.compile("Date Time :", re.IGNORECASE))
45+
time_parts = (
46+
datetime_field.lower().replace("date time :", "-").replace("to", "-").replace("gmt", "-").split("-")
47+
)
48+
start_time = datetime.strptime(time_parts[1].strip(), self.DATE_TIME_FORMAT)
49+
end_time = datetime.strptime(time_parts[2].strip(), self.DATE_TIME_FORMAT)
50+
return start_time, end_time
51+
52+
53+
class SubjectParserPCCW(EmailSubjectParser):
54+
"""Custom Parser for Email subject of PCCW circuit maintenance notifications.
55+
56+
This parser extracts maintenance ID, status and summary from the email subject line.
57+
"""
58+
59+
# Only completion notification doesn't come with ICal. Other such as planned outage, urgent maintenance,
60+
# amendment and cacellation notifications come with ICal. Hence, maintenance status is set to COMPLETED.
61+
DEFAULT_STATUS: ClassVar[Status] = Status.COMPLETED
62+
63+
def parse_subject(self, subject: str) -> List[Dict]:
64+
"""Parse PCCW circuit maintenance email subject.
65+
66+
Args:
67+
subject: Email subject string to parse
68+
69+
Returns:
70+
List containing a dictionary with parsed subject data including:
71+
- maintenance_id: Extracted from end of subject
72+
- status: Default COMPLETED status
73+
- summary: Cleaned subject line
74+
"""
75+
data: Dict[str, Any] = {
76+
"maintenance_id": self._extract_maintenance_id(subject),
77+
"status": self.DEFAULT_STATUS,
78+
"summary": self._clean_summary(subject),
79+
}
80+
81+
return [data]
82+
83+
def _extract_maintenance_id(self, subject: str) -> str:
84+
"""Extract maintenance ID from the end of subject line."""
85+
return subject.split("-")[-1].strip()
86+
87+
def _clean_summary(self, subject: str) -> str:
88+
"""Clean and format the summary text."""
89+
return subject.strip().replace("\n", "")

circuit_maintenance_parser/provider.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from circuit_maintenance_parser.parsers.momentum import HtmlParserMomentum1, SubjectParserMomentum1
3131
from circuit_maintenance_parser.parsers.netflix import TextParserNetflix1
3232
from circuit_maintenance_parser.parsers.openai import OpenAIParser
33+
from circuit_maintenance_parser.parsers.pccw import HtmlParserPCCW, SubjectParserPCCW
3334
from circuit_maintenance_parser.parsers.seaborn import (
3435
HtmlParserSeaborn1,
3536
HtmlParserSeaborn2,
@@ -406,6 +407,29 @@ class PacketFabric(GenericProvider):
406407
_default_organizer = PrivateAttr("[email protected]")
407408

408409

410+
class PCCW(GenericProvider):
411+
"""PCCW provider custom class."""
412+
413+
_include_filter = PrivateAttr(
414+
{
415+
"Icalendar": ["BEGIN"],
416+
"ical": ["BEGIN"],
417+
EMAIL_HEADER_SUBJECT: [
418+
"Completion - Planned Outage Notification",
419+
"Completion - Urgent Maintenance Notification",
420+
],
421+
}
422+
)
423+
424+
_processors: List[GenericProcessor] = PrivateAttr(
425+
[
426+
SimpleProcessor(data_parsers=[ICal]),
427+
CombinedProcessor(data_parsers=[HtmlParserPCCW, SubjectParserPCCW, EmailDateParser]),
428+
]
429+
)
430+
_default_organizer = "mailto:[email protected]"
431+
432+
409433
class Seaborn(GenericProvider):
410434
"""Seaborn provider custom class."""
411435

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
BEGIN:VCALENDAR
2+
PRODID:-//iCal4j v1.0//EN
3+
VERSION:2.0
4+
BEGIN:VEVENT
5+
DTSTAMP:20241127T064203Z
6+
DTSTART:20241215T040000Z
7+
DTEND:20241215T120000Z
8+
SUMMARY:Amendment - Planned Outage Notification of International Service -RPO Reference: R1234\\56B-A1 - INC0123456789AB
9+
UID:20241119T061319Z-uidGen@fe80:0:0:0:42:acff:fe15:6%eth0
10+
SEQUENCE:1
11+
X-MAINTNOTE-PROVIDER:PCCW Global
12+
X-MAINTNOTE-ACCOUNT:Acme Pte Ltd
13+
X-MAINTNOTE-MAINTENANCE-ID:INC0123456789AB
14+
X-MAINTNOTE-OBJECT-ID;SR=SR654321:ABC(XXX)-DEF(XXX) EP4321:AAAAAA-654321-
15+
ELE
16+
X-MAINTNOTE-OBJECT-ID;SR=SR123456:ABC(XXX)-DEF(XXX) EP1234:AAAAAA-123456-
17+
ELE
18+
X-MAINTNOTE-IMPACT:OUTAGE
19+
X-MAINTNOTE-STATUS:CONFIRMED
20+
ORGANIZER:mailto:[email protected]
21+
BEGIN:VALARM
22+
TRIGGER:-PT15M
23+
ACTION:DISPLAY
24+
DESCRIPTION:Reminder
25+
END:VALARM
26+
END:VEVENT
27+
END:VCALENDAR
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[
2+
{
3+
"account": "Acme Pte Ltd",
4+
"circuits": [
5+
{
6+
"circuit_id": "ABC(XXX)-DEF(XXX) EP4321:AAAAAA-654321-",
7+
"impact": "OUTAGE"
8+
},
9+
{
10+
"circuit_id": "ABC(XXX)-DEF(XXX) EP1234:AAAAAA-123456-",
11+
"impact": "OUTAGE"
12+
}
13+
],
14+
"end": 1734264000,
15+
"maintenance_id": "INC0123456789AB",
16+
"organizer": "mailto:[email protected]",
17+
"provider": "PCCW Global",
18+
"sequence": 1,
19+
"stamp": 1732689723,
20+
"start": 1734235200,
21+
"status": "CONFIRMED",
22+
"summary": "Amendment - Planned Outage Notification of International Service -RPO Reference: R1234\\56B-A1 - INC0123456789AB",
23+
"uid": "20241119T061319Z-uidGen@fe80:0:0:0:42:acff:fe15:6%eth0"
24+
}
25+
]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
BEGIN:VCALENDAR
2+
PRODID:-//iCal4j v1.0//EN
3+
VERSION:2.0
4+
BEGIN:VEVENT
5+
DTSTAMP:20241204T013128Z
6+
DTSTART:20241205T150000Z
7+
DTEND:20241205T190000Z
8+
SUMMARY:Cancellation - Urgent Maintenance Notification of International Service -UMW Reference number U1234\\56E-A1 - INC0123456789AB
9+
STATUS:CANCELLED
10+
UID:20241202T075849Z-uidGen@fe80:0:0:0:42:acff:fe15:3%eth0
11+
SEQUENCE:1
12+
X-MAINTNOTE-PROVIDER:PCCW Global
13+
X-MAINTNOTE-ACCOUNT:Acme Pte Ltd
14+
X-MAINTNOTE-MAINTENANCE-ID:INC0123456789AB
15+
X-MAINTNOTE-OBJECT-ID;SR=SR654321:ABC(XXX)-DEF(XXX) EP4321:AAAAAA-654321-
16+
ELE
17+
X-MAINTNOTE-OBJECT-ID;SR=SR123456:ABC(XXX)-DEF(XXX) EP1234:AAAAAA-123456-
18+
ELE
19+
X-MAINTNOTE-IMPACT:OUTAGE
20+
X-MAINTNOTE-STATUS:CANCELLED
21+
ORGANIZER:mailto:[email protected]
22+
END:VEVENT
23+
END:VCALENDAR
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[
2+
{
3+
"account": "Acme Pte Ltd",
4+
"circuits": [
5+
{
6+
"circuit_id": "ABC(XXX)-DEF(XXX) EP4321:AAAAAA-654321-",
7+
"impact": "OUTAGE"
8+
},
9+
{
10+
"circuit_id": "ABC(XXX)-DEF(XXX) EP1234:AAAAAA-123456-",
11+
"impact": "OUTAGE"
12+
}
13+
],
14+
"end": 1733425200,
15+
"maintenance_id": "INC0123456789AB",
16+
"organizer": "mailto:[email protected]",
17+
"provider": "PCCW Global",
18+
"sequence": 1,
19+
"stamp": 1733275888,
20+
"start": 1733410800,
21+
"status": "CANCELLED",
22+
"summary": "Cancellation - Urgent Maintenance Notification of International Service -UMW Reference number U1234\\56E-A1 - INC0123456789AB",
23+
"uid": "20241202T075849Z-uidGen@fe80:0:0:0:42:acff:fe15:3%eth0"
24+
}
25+
]
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<html>
2+
<head>
3+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
4+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
5+
<title>template</title>
6+
<style type="text/css">
7+
.ReadMsgBody{width: 100%;}
8+
.ExternalClass{width: 100%;}
9+
div, p, a, li, td { -webkit-text-size-adjust:none; }
10+
a[x-apple-data-detectors]{
11+
color: inherit !important;
12+
text-decoration: inherit !important;
13+
font-size: inherit !important;
14+
font-family: inherit !important;
15+
font-weight: inherit !i mportant;
16+
line-height: inherit !important;
17+
}
18+
</style>
19+
</head>
20+
<body>
21+
<div style="background-color:#FFEB9C; width:100%; padding:2pt; font-size:10pt; line-height:12pt; font-family:'Calibri'; color:Black; text-align: left;">
22+
<span style="color:#9C6500; font-weight:bold;">CAUTION:</span> This email has been sent from an external source. Do not click links, open attachments, or provide sensitive business information unless you can verify the sender’s legitimacy.
23+
</div>
24+
<br>
25+
<div>
26+
<table style="width: 720px;" border="0" cellspacing="0" cellpadding="0">
27+
<tbody>
28+
<tr style="line-height: 0px;">
29+
<td style="line-height: 0px;"><img src="https://www.pccwglobal.com/wp-content/uploads/notifications/request_planned_outage.jpg" alt="" width="700" height="250"></td>
30+
</tr>
31+
</tbody>
32+
</table>
33+
<table style="width: 720px;" border="0" cellspacing="0" cellpadding="0">
34+
<tbody>
35+
<tr>
36+
<td align="left" valign="top" height="30">
37+
<p>&nbsp;</p>
38+
</td>
39+
</tr>
40+
<tr>
41+
<td>
42+
<p><span style="font-size: 11pt; font-family: verdana, geneva;">Customer Name : Acme Pte Ltd
43+
<br>
44+
From : Global Services &amp; Operations Tel : +852-28291567<br>
45+
Date : 29/11/2024 23:38:36 GMT (dd/mm/yyyy)<br>
46+
<br>
47+
<br>
48+
Dear Sir/Madam,<br>
49+
<br>
50+
<br>
51+
PCCW Global wished to advise you that the following network activity that will affect your service.
52+
<br>
53+
<br>
54+
<br>
55+
RPO Reference Number : R1234\56B<br>
56+
Date Time : 26/11/2024 01:00:00 to 26/11/2024 10:00:00 GMT (dd/mm/yyyy)<br>
57+
Duration : 0 mins<br>
58+
Service/Circuit Affected :<br>
59+
SR654321 / ABC(XXX)-DEF(XXX) EP4321 / <br>
60+
SR123456 / ABC(XXX)-DEF(XXX) EP1234 / <br>
61+
<br>
62+
Outage Description : AAE-1: PW for Fiber migration works between XXXX - XXXXX (XXX-XXX) span within segment L.1, Services
63+
<br>
64+
<br>
65+
Impact : Switching hits<br>
66+
<br>
67+
PCCW Global apologies for all inconveniences caused.<br>
68+
<br>
69+
Should you have any question or any enquiries, please do not hesitate to contact us by calling our 24x7 customer service hotline at +852-28291567 / 80028291567 (Toll free number)<br>
70+
<br>
71+
<br>
72+
Best Regards,<br>
73+
<br>
74+
Global Services &amp; Operations <br>
75+
PCCW Global</span></p>
76+
</td>
77+
</tr>
78+
</tbody>
79+
</table>
80+
<table style="width: 720px;" border="0" cellspacing="0" cellpadding="0">
81+
<tbody>
82+
<tr style="line-height: 0px;">
83+
<td style="line-height: 0px;" width="479" height="91"></td>
84+
</tr>
85+
</tbody>
86+
</table>
87+
<div></div>
88+
</div>
89+
</body>
90+
</html>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[
2+
{
3+
"account": "Acme Pte Ltd",
4+
"circuits": [],
5+
"end": 1732615200,
6+
"provider": "PCCW Global",
7+
"start": 1732582800
8+
}
9+
]

0 commit comments

Comments
 (0)