Skip to content

Commit e180a1f

Browse files
Add parser for Vodafone
1 parent b2a8b84 commit e180a1f

14 files changed

Lines changed: 1459 additions & 0 deletions

circuit_maintenance_parser/__init__.py

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

circuit_maintenance_parser/provider.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
from circuit_maintenance_parser.parsers.telstra import HtmlParserTelstra1, HtmlParserTelstra2
4545
from circuit_maintenance_parser.parsers.turkcell import HtmlParserTurkcell1
4646
from circuit_maintenance_parser.parsers.verizon import HtmlParserVerizon1
47+
from circuit_maintenance_parser.parsers.vodafone import HtmlParserVodafone1
4748
from circuit_maintenance_parser.parsers.windstream import HtmlParserWindstream1
4849
from circuit_maintenance_parser.parsers.zayo import HtmlParserZayo1, SubjectParserZayo1
4950
from circuit_maintenance_parser.processor import CombinedProcessor, GenericProcessor, SimpleProcessor
@@ -565,6 +566,17 @@ class Verizon(GenericProvider):
565566
_default_organizer = PrivateAttr("NO-REPLY-sched-maint@EMEA.verizonbusiness.com")
566567

567568

569+
class Vodafone(GenericProvider):
570+
"""Vodafone provider custom class."""
571+
572+
_processors: List[GenericProcessor] = PrivateAttr(
573+
[
574+
CombinedProcessor(data_parsers=[EmailDateParser, HtmlParserVodafone1]),
575+
]
576+
)
577+
_default_organizer = PrivateAttr("networkchangemanagement@vodafone.com")
578+
579+
568580
class Windstream(GenericProvider):
569581
"""Windstream provider custom class."""
570582

0 commit comments

Comments
 (0)