Skip to content

Commit 9fb83fd

Browse files
authored
Merge pull request #416 from i3dnet-akoopen/parser-vodafone
Add parser for Vodafone
2 parents a4636cc + a5c25d1 commit 9fb83fd

17 files changed

Lines changed: 1486 additions & 0 deletions

changes/416.added

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added parser for Vodafone.

circuit_maintenance_parser/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
Telxius,
4040
Turkcell,
4141
Verizon,
42+
Vodafone,
4243
Windstream,
4344
Zayo,
4445
)
@@ -76,6 +77,7 @@
7677
Telxius,
7778
Turkcell,
7879
Verizon,
80+
Vodafone,
7981
Windstream,
8082
Zayo,
8183
)
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
"""Vodafone parser."""
2+
3+
import logging
4+
import re
5+
from typing import Any, Dict, List
6+
7+
from bs4 import BeautifulSoup
8+
from bs4.element import ResultSet # type: ignore
9+
from dateutil import parser
10+
11+
from circuit_maintenance_parser.parser import EmailSubjectParser, Html, Impact, Status
12+
13+
logger = logging.getLogger(__name__)
14+
15+
16+
class HtmlParserVodafone1(Html):
17+
"""Notifications Parser for Vodafone notifications."""
18+
19+
def parse_html(self, soup: BeautifulSoup) -> List[Dict]:
20+
"""Execute parsing."""
21+
data: Dict[str, Any] = {"circuits": []}
22+
self.parse_crq(soup, data)
23+
self.parse_tables(soup.find_all("table"), data)
24+
self.parse_bold(soup.find_all("b"), data)
25+
26+
return [data]
27+
28+
def parse_tables(self, tables: ResultSet, data: Dict):
29+
"""Parse table element to find circuit ID's and account."""
30+
for table in tables:
31+
col_mapping = {}
32+
for tr_elem in table.find_all("tr"):
33+
col = 0
34+
cid = 0
35+
impact = 0
36+
# look for table header
37+
for th_elem in tr_elem.find_all("th"):
38+
# Map column headers to column number
39+
if th_elem.text.strip() != "":
40+
col_mapping[th_elem.text.strip()] = col
41+
col += 1
42+
43+
# look for regular columns
44+
for td_elem in tr_elem.find_all("td"):
45+
if "Customer" in col_mapping and col == col_mapping["Customer"]:
46+
data["account"] = td_elem.text.strip()
47+
elif "Services Affected" in col_mapping and col == col_mapping["Services Affected"]:
48+
cid = td_elem.text.strip()
49+
elif "Service Impact" in col_mapping and col == col_mapping["Service Impact"]:
50+
# not sure if other impact types exist, can be expanded of need-be. Default to DEGRADED.
51+
if "loss of service" in td_elem.text.lower():
52+
impact = Impact("OUTAGE")
53+
else:
54+
impact = Impact("DEGRADED")
55+
col += 1
56+
57+
# at the end of the table row, add circuits to list, if defined
58+
if cid != 0 and impact != 0:
59+
data["circuits"].append({"circuit_id": cid, "impact": impact})
60+
61+
def parse_bold(self, bolds: ResultSet, data: Dict):
62+
"""Parse B (bold) elements to find summary and start+end date/time.
63+
64+
Example:
65+
<b>New Scheduled Start/End Date &amp; Outage Window:</b><br>
66+
06/04/2026 00:00 to 13/04/2026 00:00 UTC <br>
67+
"""
68+
window = 0
69+
for bold in bolds:
70+
text_lower = bold.text.lower()
71+
72+
# find start/end date/time
73+
# in case the window is re-schedulded, the original and new window are listed; ignore original window
74+
if "original scheduled start" in text_lower:
75+
continue
76+
77+
if "scheduled start" in text_lower:
78+
window_next = bold.next_sibling
79+
while window_next:
80+
text = window_next.text.strip()
81+
if text != "":
82+
window = text
83+
break
84+
window_next = window_next.next_sibling
85+
86+
# find summary
87+
if "description" in text_lower:
88+
description_next = bold.next_sibling
89+
while description_next:
90+
text = description_next.text.strip()
91+
if text != "":
92+
data["summary"] = text
93+
break
94+
description_next = description_next.next_sibling
95+
96+
if window != 0:
97+
start_str, end_str = window.replace(" UTC", "").split(" to ")
98+
data["start"] = self.dt2ts(parser.parse(start_str, dayfirst=True))
99+
data["end"] = self.dt2ts(parser.parse(end_str, dayfirst=True))
100+
101+
def parse_crq(self, soup: ResultSet, data: Dict):
102+
"""Vodafone maintenance_id's are in the format of CRQ[0-9] with 12 digits.
103+
104+
Please be advised that the Planned Works have been Completed: CRQ000001312927
105+
"""
106+
text = soup.get_text(separator=" ")
107+
match = re.search(r"\bCRQ\d{12}\b", text)
108+
if match:
109+
data.setdefault("maintenance_id", match.group(0))
110+
111+
112+
class SubjectParserVodafone1(EmailSubjectParser):
113+
"""Parse status and (when present) the CRQ from the subject line."""
114+
115+
def parse_subject(self, subject: str) -> List[Dict]:
116+
"""Parse the email subject."""
117+
data: Dict = {}
118+
subject_lower = subject.lower()
119+
120+
if "completed" in subject_lower:
121+
data["status"] = Status("COMPLETED")
122+
elif "rescheduled" in subject_lower:
123+
data["status"] = Status("RE-SCHEDULED")
124+
elif "postponed" in subject_lower or "cancelled" in subject_lower:
125+
data["status"] = Status("CANCELLED")
126+
else:
127+
data["status"] = Status("CONFIRMED")
128+
129+
crq_match = re.search(r"\bCRQ\d{12}\b", subject)
130+
if crq_match:
131+
data["maintenance_id"] = crq_match.group(0)
132+
133+
return [data]

circuit_maintenance_parser/provider.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
from circuit_maintenance_parser.parsers.telxius import HtmlParserTelxius1, SubjectParserTelxius1
4747
from circuit_maintenance_parser.parsers.turkcell import HtmlParserTurkcell1
4848
from circuit_maintenance_parser.parsers.verizon import HtmlParserVerizon1
49+
from circuit_maintenance_parser.parsers.vodafone import HtmlParserVodafone1, SubjectParserVodafone1
4950
from circuit_maintenance_parser.parsers.windstream import HtmlParserWindstream1
5051
from circuit_maintenance_parser.parsers.zayo import HtmlParserZayo1, SubjectParserZayo1
5152
from circuit_maintenance_parser.processor import CombinedProcessor, GenericProcessor, SimpleProcessor
@@ -589,6 +590,17 @@ class Verizon(GenericProvider):
589590
_default_organizer = PrivateAttr("NO-REPLY-sched-maint@EMEA.verizonbusiness.com")
590591

591592

593+
class Vodafone(GenericProvider):
594+
"""Vodafone provider custom class."""
595+
596+
_processors: List[GenericProcessor] = PrivateAttr(
597+
[
598+
CombinedProcessor(data_parsers=[EmailDateParser, SubjectParserVodafone1, HtmlParserVodafone1]),
599+
]
600+
)
601+
_default_organizer = PrivateAttr("networkchangemanagement@vodafone.com")
602+
603+
592604
class Windstream(GenericProvider):
593605
"""Windstream provider custom class."""
594606

0 commit comments

Comments
 (0)