Skip to content

Commit f0d9a04

Browse files
authored
Add new Windstream Parser (#280)
* Add new Windstream Parser * Fix pydocstyle error * fix linter issue for local variables * Fix unit tests * Fix recommendations from PR * Timezone was not in UTC. Fixed. * Add timezone_converter helper function * Fix pydocstyle error * Fix pylint errors
1 parent 8fe88f6 commit f0d9a04

File tree

9 files changed

+310
-0
lines changed

9 files changed

+310
-0
lines changed

circuit_maintenance_parser/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
Telstra,
3333
Turkcell,
3434
Verizon,
35+
Windstream,
3536
Zayo,
3637
)
3738

@@ -62,6 +63,7 @@
6263
Telstra,
6364
Turkcell,
6465
Verizon,
66+
Windstream,
6567
Zayo,
6668
)
6769

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""Windstream parser."""
2+
import logging
3+
from datetime import timezone
4+
5+
from circuit_maintenance_parser.parser import Html, Impact, CircuitImpact, Status
6+
from circuit_maintenance_parser.utils import convert_timezone
7+
8+
# pylint: disable=too-many-nested-blocks, too-many-branches
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
class HtmlParserWindstream1(Html):
14+
"""Notifications Parser for Windstream notifications."""
15+
16+
def parse_html(self, soup):
17+
"""Execute parsing."""
18+
data = {}
19+
data["circuits"] = []
20+
impact = Impact("NO-IMPACT")
21+
confirmation_words = [
22+
"Demand Maintenance Notification",
23+
"Planned Maintenance Notification",
24+
"Emergency Maintenance Notification",
25+
]
26+
cancellation_words = ["Postponed Maintenance Notification", "Cancelled Maintenance Notification"]
27+
28+
h1_tag = soup.find("h1")
29+
if h1_tag.string.strip() == "Completed Maintenance Notification":
30+
data["status"] = Status("COMPLETED")
31+
elif any(keyword in h1_tag.string.strip() for keyword in confirmation_words):
32+
data["status"] = Status("CONFIRMED")
33+
elif h1_tag.string.strip() == "Updated Maintenance Notification":
34+
data["status"] = Status("RE-SCHEDULED")
35+
elif any(keyword in h1_tag.string.strip() for keyword in cancellation_words):
36+
data["status"] = Status("CANCELLED")
37+
38+
div_tag = h1_tag.find_next_sibling("div")
39+
summary_text = div_tag.get_text(separator="\n", strip=True)
40+
summary_text = summary_text.split("\nDESCRIPTION OF MAINTENANCE")[0]
41+
42+
data["summary"] = summary_text
43+
44+
table = soup.find("table")
45+
for row in table.find_all("tr"):
46+
if len(row) < 2:
47+
continue
48+
cols = row.find_all("td")
49+
header_tag = cols[0].string
50+
if header_tag is None or header_tag == "Maintenance Address:":
51+
continue
52+
header_tag = header_tag.string.strip()
53+
value_tag = cols[1].string.strip()
54+
if header_tag == "WMT:":
55+
data["maintenance_id"] = value_tag
56+
elif "Date & Time:" in header_tag:
57+
dt_time = convert_timezone(value_tag)
58+
if "Event Start" in header_tag:
59+
data["start"] = int(dt_time.replace(tzinfo=timezone.utc).timestamp())
60+
elif "Event End" in header_tag:
61+
data["end"] = int(dt_time.replace(tzinfo=timezone.utc).timestamp())
62+
elif header_tag == "Outage":
63+
impact = Impact("OUTAGE")
64+
else:
65+
continue
66+
67+
table = soup.find("table", "circuitTable")
68+
for row in table.find_all("tr"):
69+
cols = row.find_all("td")
70+
if len(cols) == 9:
71+
if cols[0].string.strip() == "Name":
72+
continue
73+
data["account"] = cols[0].string.strip()
74+
data["circuits"].append(CircuitImpact(impact=impact, circuit_id=cols[2].string.strip()))
75+
76+
return [data]

circuit_maintenance_parser/provider.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
from circuit_maintenance_parser.parsers.telstra import HtmlParserTelstra1, HtmlParserTelstra2
4141
from circuit_maintenance_parser.parsers.turkcell import HtmlParserTurkcell1
4242
from circuit_maintenance_parser.parsers.verizon import HtmlParserVerizon1
43+
from circuit_maintenance_parser.parsers.windstream import HtmlParserWindstream1
4344
from circuit_maintenance_parser.parsers.zayo import HtmlParserZayo1, SubjectParserZayo1
4445
from circuit_maintenance_parser.processor import CombinedProcessor, GenericProcessor, SimpleProcessor
4546
from circuit_maintenance_parser.utils import rgetattr
@@ -452,6 +453,17 @@ class Verizon(GenericProvider):
452453
_default_organizer = PrivateAttr("NO-REPLY-sched-maint@EMEA.verizonbusiness.com")
453454

454455

456+
class Windstream(GenericProvider):
457+
"""Windstream provider custom class."""
458+
459+
_processors: List[GenericProcessor] = PrivateAttr(
460+
[
461+
CombinedProcessor(data_parsers=[EmailDateParser, HtmlParserWindstream1]),
462+
]
463+
)
464+
_default_organizer = PrivateAttr("wci.maintenance.notifications@windstream.com")
465+
466+
455467
class Zayo(GenericProvider):
456468
"""Zayo provider custom class."""
457469

circuit_maintenance_parser/utils.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import logging
44
from typing import Tuple, Dict, Union
55
import csv
6+
import datetime
7+
import pytz
68

79
from geopy.exc import GeocoderUnavailable, GeocoderTimedOut, GeocoderServiceError # type: ignore
810
from geopy.geocoders import Nominatim # type: ignore
@@ -128,6 +130,52 @@ def city_timezone(self, city: str) -> str:
128130
raise ParserError("Timezone resolution not properly initalized.")
129131

130132

133+
def convert_timezone(time_str):
134+
"""
135+
Converts a string representing a date/time in the format 'MM/DD/YY HH:MM Timezone' to a datetime object in UTC.
136+
137+
Args:
138+
time_str (str): A string representing a date/time followed by a timezone abbreviation.
139+
140+
Returns:
141+
datetime: A datetime object representing the converted date/time in UTC.
142+
143+
Example:
144+
convert_timezone("01/20/24 06:00 ET")
145+
"""
146+
# Convert timezone abbreviation to timezone string for pytz.
147+
timezone_mapping = {
148+
"ET": "US/Eastern",
149+
"CT": "US/Central",
150+
"MT": "US/Mountain",
151+
"PT": "US/Pacific"
152+
# Add more mappings as needed
153+
}
154+
155+
datetime_str, tz_abbr = time_str.rsplit(maxsplit=1)
156+
# Parse the datetime string
157+
dt_time = datetime.datetime.strptime(datetime_str, "%m/%d/%y %H:%M")
158+
159+
timezone = timezone_mapping.get(tz_abbr)
160+
if timezone is None:
161+
try:
162+
# Get the timezone object
163+
tz_zone = pytz.timezone(tz_abbr)
164+
except ValueError as exc:
165+
raise ValueError("Timezone not found: " + str(exc)) # pylint: disable=raise-missing-from
166+
else:
167+
# Get the timezone object
168+
tz_zone = pytz.timezone(timezone)
169+
170+
# Convert to the specified timezone
171+
dt_time = tz_zone.localize(dt_time)
172+
173+
# Convert to UTC
174+
dt_utc = dt_time.astimezone(pytz.utc)
175+
176+
return dt_utc
177+
178+
131179
def rgetattr(obj, attr):
132180
"""Recursive GetAttr to look for nested attributes."""
133181
nested_value = getattr(obj, attr)
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
MIME-Version: 1.0
2+
Date: Wed, 13 Mar 2024 00:05:34 +1030
3+
From:
4+
"wci.maintenance.notifications@windstream.com"
5+
<wci.maintenance.notifications@windstream.com>
6+
Subject: Windstream (Demand Notification WMT#: 123456)
7+
Thread-Topic: Windstream (Demand Notification WMT#: 123456)
8+
Message-ID: <20240312133524.9B07A18000267@azlapp0432.windstream.com>
9+
To: receiver <receiver@testing.com>
10+
Content-Transfer-Encoding: quoted-printable
11+
Content-Type: text/html; charset="utf-8"
12+
13+
<html><head>
14+
<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Dutf-8"><s=
15+
tyle>
16+
<!--
17+
body
18+
{font-size:13px;
19+
font-family:Arial,Helvetica,sans-serif;
20+
color:#404040;
21+
margin:0px;
22+
padding:0;
23+
text-align:left;
24+
background-color:#fff;
25+
margin:10px 5px 10px 5px}
26+
.headerRow
27+
{background:#E1F3C8;
28+
font-weight:bold}
29+
.circuitTable
30+
{border:2px solid #D5D5D5;
31+
border-right:0px}
32+
.circuitTable td
33+
{padding:5px;
34+
border-right:2px solid #D5D5D5}
35+
.summary
36+
{padding:0px 5px}
37+
.tblPadding
38+
{border:2px solid #D5D5D5;
39+
border-right:0px;
40+
border-bottom:0px}
41+
.tblPadding td
42+
{padding:5px;
43+
border:2px solid #D5D5D5;
44+
border-top:0px;
45+
border-left:0px}
46+
.bottomBorder td
47+
{border-bottom:2px solid #D5D5D5}
48+
.altColor
49+
{background:#F0F0F0}
50+
h1
51+
{font-size:22px;
52+
text-align:center}
53+
h2
54+
{font-size:14px;
55+
margin-bottom:5px;
56+
margin-left:5px}
57+
.description
58+
{padding-left:7px}
59+
.timeZoneTable
60+
{border:1px solid #000000;
61+
border-bottom:0px;
62+
font-size:12px;
63+
font-family:Calibri,Arial,Helvetica,sans-serif}
64+
.timeZoneTable td
65+
{border-bottom:1px solid #000000;
66+
padding-left:12px;
67+
padding-right:12px;
68+
padding-top:5px}
69+
-->
70+
</style></head><body><p align=3D"center" style=3D"text-align:center"><img w=
71+
idth=3D"200" height=3D"67" src=3D"https://we-uata.windstream.com/cdn2/win_m=
72+
op_logo.png"></p><h1>Planned Maintenance Notification</h1><p></p><div class=
73+
=3D"summary"><br>Windstream has identified a network fault and must perform=
74+
demand maintenance in order to restore our network services to their full =
75+
capabilities. This maintenance is required to resolve current service impac=
76+
ts or remove the risk for potential impacts. We understand the challenges t=
77+
his may present and are working to minimize disruptions. The details of the=
78+
planned maintenance are listed below along with descriptions and planned i=
79+
mpact timeframes.<br><br><h2>DESCRIPTION OF MAINTENANCE</h2><span class=3D"=
80+
description">Fiber Maintenance - DOT Mandated Project<br><br><b>Related WMT=
81+
: </b>123456</span><br><br><h2>MAINTENANCE INFORMATION</h2><table border=3D=
82+
"0" cellspacing=3D"0" cellpadding=3D"0"><tbody><tr><td valign=3D"top"><tabl=
83+
e border=3D"0" cellspacing=3D"0" cellpadding=3D"0" class=3D"tblPadding"><tb=
84+
ody><tr><td><b>WMT:</b></td><td>123456</td></tr><tr><td><b>Maintenance Addr=
85+
ess:</b></td><td><div>Location1</div><div>RANDOM ADDRESS FOR PARSING =
86+
</div></td></tr><tr><td><b>Service Affecting:</b></td><=
87+
td>YES</td></tr><tr><td><b>Event Start Date &amp; Time:</b></td><td>03/20/2=
88+
4 02:00 ET</td></tr><tr><td><b>Event End Date &amp; Time:</b></td><td>03/20=
89+
/24 08:00 ET</td></tr></tbody></table></td><td valign=3D"top" style=3D"padd=
90+
ing-left:15px"><table border=3D"0" cellspacing=3D"0" cellpadding=3D"0" clas=
91+
s=3D"tblPadding"><tbody><tr><td><b>Impact Type:</b></td><td><b>Duration</b>=
92+
</td><td><b>Impact Start</b></td><td><b>Impact End</b></td></tr><tr><td>Out=
93+
age</td><td>360 minute(s)</td><td>03/20/24 02:00 ET</td><td>03/20/24 08:00 =
94+
ET</td></tr><tr><td colspan=3D"4"><em style=3D"font-size:11pt">Note: Servic=
95+
e impact may be experienced at varying times throughout the implementation =
96+
window.</em></td></tr></tbody></table></td></tr></tbody></table><br><br><h2=
97+
></h2><table border=3D"0" cellpadding=3D"0" cellspacing=3D"0" class=3D"circ=
98+
uitTable"><tbody><tr class=3D"headerRow bottomBorder"><td>Name</td><td>Acco=
99+
unt</td><td>Circuit ID</td><td>Rate Code</td><td>Cust Ckt ID</td><td>A-Loc<=
100+
/td><td>A-Loc Address</td><td>Z-Loc</td><td>Z-Loc Address</td></tr><tr><td>=
101+
TEST ACCOUNT</td><td>*****1234</td><td>WS/KJUS/1=
102+
2345/ /WXN /</td><td>100G</td><td>12345678</td><td>ABCDE012</td><td>123 Fa=
103+
ke ST</td><td>ABCDEF</td><td>456 Fake ST</td></tr><tr><td>=
104+
TEST ACCOUNT</td><td>*****1234</td><td>WS/KJUS/2=
105+
2345/ /WXN /</td><td>100G</td><td> 12345678</td><td>ABCDE012</td><td>123 Fa=
106+
ke ST</td><td> ABCDEF</td><td>456 Fake ST</td></tr></tbody></table><br=
107+
><br>If you experience any trouble with your service prior to or after this=
108+
maintenance window time frame shown in this notification, please contact t=
109+
he appropriate Service Assurance group.<br><ul><li><b>Enterprise</b>: 800-6=
110+
00-5050</li><li><b>Wholesale</b>: 844-946-2662</li><li><b>Fiber To The Towe=
111+
r</b>: 877-473-8042</li></ul><br>If you have questions pertaining to or in =
112+
reference to this maintenance, please contact the Windstream Change Managem=
113+
ent Team via email directly at wci.maintenance.notifications@windstream.com=
114+
or by calling us at 800-892-6785.<br><br>Thank you for your business!</div=
115+
></body></html>=
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[
2+
{
3+
"account": "TEST ACCOUNT",
4+
"circuits": [
5+
{
6+
"circuit_id": "WS/KJUS/12345/ /WXN /",
7+
"impact": "OUTAGE"
8+
},
9+
{
10+
"circuit_id": "WS/KJUS/22345/ /WXN /",
11+
"impact": "OUTAGE"
12+
}
13+
],
14+
"end": 1710936000,
15+
"maintenance_id": "123456",
16+
"start": 1710914400,
17+
"status": "CONFIRMED",
18+
"summary": "Windstream has identified a network fault and must perform demand maintenance in order to restore our network services to their full capabilities. This maintenance is required to resolve current service impacts or remove the risk for potential impacts. We understand the challenges this may present and are working to minimize disruptions. The details of the planned maintenance are listed below along with descriptions and planned impact timeframes."
19+
}
20+
]
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[
2+
{
3+
"account": "TEST ACCOUNT",
4+
"circuits": [
5+
{
6+
"circuit_id": "WS/KJUS/12345/ /WXN /",
7+
"impact": "OUTAGE"
8+
},
9+
{
10+
"circuit_id": "WS/KJUS/22345/ /WXN /",
11+
"impact": "OUTAGE"
12+
}
13+
],
14+
"end": 1710936000,
15+
"maintenance_id": "123456",
16+
"stamp": 1710250534,
17+
"start": 1710914400,
18+
"status": "CONFIRMED",
19+
"summary": "Windstream has identified a network fault and must perform demand maintenance in order to restore our network services to their full capabilities. This maintenance is required to resolve current service impacts or remove the risk for potential impacts. We understand the challenges this may present and are working to minimize disruptions. The details of the planned maintenance are listed below along with descriptions and planned impact timeframes."
20+
}
21+
]

tests/unit/test_e2e.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
Telstra,
3838
Turkcell,
3939
Verizon,
40+
Windstream,
4041
Zayo,
4142
)
4243

@@ -853,6 +854,14 @@
853854
Path(dir_path, "data", "date", "email_date_1_result.json"),
854855
],
855856
),
857+
# Windstream
858+
(
859+
Windstream,
860+
[
861+
("email", Path(dir_path, "data", "windstream", "windstream1.eml")),
862+
],
863+
[Path(dir_path, "data", "windstream", "windstream1_result.json")],
864+
),
856865
# Zayo
857866
(
858867
Zayo,

tests/unit/test_parsers.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from circuit_maintenance_parser.parsers.telstra import HtmlParserTelstra1, HtmlParserTelstra2
3434
from circuit_maintenance_parser.parsers.turkcell import HtmlParserTurkcell1
3535
from circuit_maintenance_parser.parsers.verizon import HtmlParserVerizon1
36+
from circuit_maintenance_parser.parsers.windstream import HtmlParserWindstream1
3637
from circuit_maintenance_parser.parsers.zayo import HtmlParserZayo1, SubjectParserZayo1
3738

3839
dir_path = os.path.dirname(os.path.realpath(__file__))
@@ -572,6 +573,12 @@ def default(self, o):
572573
Path(dir_path, "data", "verizon", "verizon5.html"),
573574
Path(dir_path, "data", "verizon", "verizon5_result.json"),
574575
),
576+
# Windstream
577+
(
578+
HtmlParserWindstream1,
579+
Path(dir_path, "data", "windstream", "windstream1.eml"),
580+
Path(dir_path, "data", "windstream", "windstream1_parser_result.json"),
581+
),
575582
# Zayo
576583
(
577584
SubjectParserZayo1,

0 commit comments

Comments
 (0)