Skip to content

Commit 0f6a05d

Browse files
authored
Merge pull request #419 from i3dnet-akoopen/flag-parser
Add parser for FLAG (fka Global Cloud Xchange)
2 parents e30783e + 9fc1279 commit 0f6a05d

17 files changed

Lines changed: 825 additions & 0 deletions

changes/419.added

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added parser for FLAG (fka Globalcloudexchange)

circuit_maintenance_parser/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
ATT,
1111
AWS,
1212
BSO,
13+
FLAG,
1314
GTT,
1415
HGC,
1516
NTT,
@@ -58,6 +59,7 @@
5859
CrownCastle,
5960
Equinix,
6061
EUNetworks,
62+
FLAG,
6163
GlobalCloudXchange,
6264
Google,
6365
GTT,
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
"""Circuit Maintenance Parser for FLAG Notifications.
2+
3+
Note: this is a fork of Globalcloudexchange parser.
4+
"""
5+
6+
import re
7+
from datetime import datetime
8+
from typing import Any, Dict, List
9+
10+
from bs4 import BeautifulSoup
11+
from bs4.element import ResultSet # type: ignore
12+
13+
from circuit_maintenance_parser.output import Impact
14+
from circuit_maintenance_parser.parser import EmailSubjectParser, Html, Status
15+
16+
17+
class HtmlParserFlag1(Html):
18+
"""Custom Parser for HTML portion of FLAG circuit maintenance notifications."""
19+
20+
def parse_html(self, soup: BeautifulSoup) -> List[Dict]:
21+
"""Parse an FLAG circuit maintenance email.
22+
23+
Args:
24+
soup (BeautifulSoup): beautiful soup object containing the html portion of an email.
25+
26+
Returns:
27+
Dict: The data dict containing circuit maintenance data.
28+
"""
29+
data: Dict[str, Any] = {"circuits": []}
30+
self.parse_tables(soup.find_all("table", attrs={"border-collapse": "collapse"}), data)
31+
self.parse_paragraphs(soup.find_all("p"), data)
32+
33+
return [data]
34+
35+
def parse_tables(self, tables: ResultSet, data: Dict):
36+
"""Parse table elements to find maintenance windows (start/end) and circuit ID's."""
37+
date_format = "%d-%b-%Y %H:%M"
38+
for table in tables:
39+
table_type = ""
40+
for row in table.find_all("tr"):
41+
cols = row.find_all("td")
42+
if cols[0].text.strip() == "Service ID":
43+
table_type = "circuits"
44+
continue
45+
if cols[0].text.strip() == "Window":
46+
table_type = "windows"
47+
continue
48+
49+
# this table is listing all circuits
50+
if table_type == "circuits":
51+
impact = Impact.OUTAGE
52+
if "at risk" in cols[1].text.lower():
53+
impact = Impact.REDUCED_REDUNDANCY
54+
55+
data["circuits"].append({"circuit_id": cols[0].text.strip(), "impact": impact})
56+
# this table is listing windows (note: for now, we will only use the last listed window)
57+
elif table_type == "windows":
58+
data["start"] = self.dt2ts(datetime.strptime(cols[1].text.strip(), date_format))
59+
data["end"] = self.dt2ts(datetime.strptime(cols[2].text.strip(), date_format))
60+
61+
def parse_paragraphs(self, paragraphs: ResultSet, data: Dict):
62+
"""Parse paragraph elements to find account and summary."""
63+
for p in paragraphs:
64+
for pstring in p.strings:
65+
search = re.search("Dear (.*),", pstring)
66+
if search:
67+
data["account"] = search.group(1).strip()
68+
continue
69+
70+
# after account has been set, next paragraph is the summary
71+
if "account" in data and "summary" not in data:
72+
data["summary"] = pstring.strip()
73+
continue
74+
75+
76+
class SubjectParserFlag1(EmailSubjectParser):
77+
"""Parse the subject of a FLAG circuit maintenance email. The subject contains the maintenance ID and status."""
78+
79+
def parse_subject(self, subject: str) -> List[Dict]:
80+
"""Parse the FLAG Email subject for maintenance ID and status.
81+
82+
Args:
83+
subject (str): subject of email
84+
e.g. 'FLAG | PE2025102750538 | Planned Event | Rescheduled'.
85+
86+
87+
Returns:
88+
List[Dict]: Returns the data object with maintenance_id and status fields.
89+
"""
90+
data = {}
91+
search = re.search(
92+
r"^FLAG \| ([A-Z0-9]+)\b",
93+
subject,
94+
)
95+
if search:
96+
data["maintenance_id"] = search.group(1)
97+
98+
if "completed" in subject.lower():
99+
data["status"] = Status.COMPLETED
100+
elif "rescheduled" in subject.lower():
101+
data["status"] = Status.RE_SCHEDULED
102+
elif "scheduled" in subject.lower() or "reminder" in subject.lower() or "notice" in subject.lower():
103+
data["status"] = Status.CONFIRMED
104+
elif "cancelled" in subject.lower():
105+
data["status"] = Status.CANCELLED
106+
else:
107+
# Some FLAG notifications don't clearly state a status in their subject.
108+
# From inspection of examples, it looks like "Confirmed" would be the most appropriate in this case.
109+
data["status"] = Status.CONFIRMED
110+
111+
return [data]

circuit_maintenance_parser/provider.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from circuit_maintenance_parser.parsers.colt import CsvParserColt1, SubjectParserColt1, SubjectParserColt2
2424
from circuit_maintenance_parser.parsers.crowncastle import HtmlParserCrownCastle1
2525
from circuit_maintenance_parser.parsers.equinix import HtmlParserEquinix, SubjectParserEquinix
26+
from circuit_maintenance_parser.parsers.flag import HtmlParserFlag1, SubjectParserFlag1
2627
from circuit_maintenance_parser.parsers.globalcloudxchange import HtmlParserGcx1, SubjectParserGcx1
2728
from circuit_maintenance_parser.parsers.google import HtmlParserGoogle1, SubjectParserGoogle1
2829
from circuit_maintenance_parser.parsers.gtt import HtmlParserGTT1
@@ -362,6 +363,17 @@ class EUNetworks(GenericProvider):
362363
_default_organizer = "noc@eunetworks.com"
363364

364365

366+
class FLAG(GenericProvider):
367+
"""FLAG provider custom class."""
368+
369+
_processors: List[GenericProcessor] = PrivateAttr(
370+
[
371+
CombinedProcessor(data_parsers=[EmailDateParser, SubjectParserFlag1, HtmlParserFlag1]),
372+
]
373+
)
374+
_default_organizer = PrivateAttr("change@flagtel.com")
375+
376+
365377
class GlobalCloudXchange(GenericProvider):
366378
"""Global Cloud Xchange provider custom class."""
367379

0 commit comments

Comments
 (0)