77
88from dateutil import parser
99
10- from circuit_maintenance_parser .parser import Html , Impact , CircuitImpact , Status
10+ from circuit_maintenance_parser .parser import CircuitImpact , EmailSubjectParser , Html , Impact , Status
1111
1212# pylint: disable=too-many-nested-blocks,no-member, too-many-branches
1313
1414
1515logger = logging .getLogger (__name__ )
1616
1717
18+ class SubjectParserZayo1 (EmailSubjectParser ):
19+ """Parser for Zayo subject string, email type 1.
20+
21+ Subject: {MESSAGE TYPE}?***{ACCOUNT NAME}***ZAYO {MAINTENANCE_ID} {URGENCY}...***
22+ END OF WINDOW NOTIFICATION***Customer Inc.***ZAYO TTN-0000123456 Planned***
23+ ***Customer Inc***ZAYO TTN-0001234567 Emergency MAINTENANCE NOTIFICATION***
24+ RESCHEDULE NOTIFICATION***Customer Inc***ZAYO TTN-0005423873 Planned***
25+ """
26+
27+ def parse_subject (self , subject ):
28+ """Parse subject of email message."""
29+ data = {}
30+ tokens = subject .split ("***" )
31+ if len (tokens ) == 4 :
32+ data ["account" ] = tokens [1 ]
33+ data ["maintenance_id" ] = tokens [2 ].split (" " )[1 ]
34+ return [data ]
35+
36+
1837class HtmlParserZayo1 (Html ):
1938 """Notifications Parser for Zayo notifications."""
2039
@@ -24,6 +43,14 @@ def parse_html(self, soup):
2443 self .parse_bs (soup .find_all ("b" ), data )
2544 self .parse_tables (soup .find_all ("table" ), data )
2645
46+ if data :
47+ if "status" not in data :
48+ text = soup .get_text ()
49+ if "will be commencing momentarily" in text :
50+ data ["status" ] = Status ("IN-PROCESS" )
51+ elif "has been completed" in text :
52+ data ["status" ] = Status ("COMPLETED" )
53+
2754 return [data ]
2855
2956 def parse_bs (self , btags : ResultSet , data : dict ):
@@ -32,10 +59,29 @@ def parse_bs(self, btags: ResultSet, data: dict):
3259 if isinstance (line , bs4 .element .Tag ):
3360 if line .text .lower ().strip ().startswith ("maintenance ticket #:" ):
3461 data ["maintenance_id" ] = self .clean_line (line .next_sibling )
35- elif line .text .lower ().strip ().startswith ("urgency:" ):
36- urgency = self .clean_line (line .next_sibling )
37- if urgency == "Planned" :
62+ elif "serves as official notification" in line .text .lower ():
63+ if "will be performing maintenance" in line .text .lower ():
3864 data ["status" ] = Status ("CONFIRMED" )
65+ elif "has cancelled" in line .text .lower ():
66+ data ["status" ] = Status ("CANCELLED" )
67+ # Some Zayo notifications may include multiple activity dates.
68+ # For lack of a better way to handle this, we consolidate these into a single extended activity range.
69+ #
70+ # For example, given:
71+ #
72+ # 1st Activity Date
73+ # 01-Nov-2021 00:01 to 01-Nov-2021 05:00 ( Mountain )
74+ # 01-Nov-2021 06:01 to 01-Nov-2021 11:00 ( GMT )
75+ #
76+ # 2nd Activity Date
77+ # 02-Nov-2021 00:01 to 02-Nov-2021 05:00 ( Mountain )
78+ # 02-Nov-2021 06:01 to 02-Nov-2021 11:00 ( GMT )
79+ #
80+ # 3rd Activity Date
81+ # 03-Nov-2021 00:01 to 03-Nov-2021 05:00 ( Mountain )
82+ # 03-Nov-2021 06:01 to 03-Nov-2021 11:00 ( GMT )
83+ #
84+ # our end result would be (start: "01-Nov-2021 06:01", end: "03-Nov-2021 11:00")
3985 elif "activity date" in line .text .lower ():
4086 logger .info ("Found 'activity date': %s" , line .text )
4187 for sibling in line .next_siblings :
@@ -44,9 +90,15 @@ def parse_bs(self, btags: ResultSet, data: dict):
4490 if "( GMT )" in text :
4591 window = self .clean_line (sibling ).strip ("( GMT )" ).split (" to " )
4692 start = parser .parse (window .pop (0 ))
47- data ["start" ] = self .dt2ts (start )
93+ start_ts = self .dt2ts (start )
94+ # Keep the earliest of any listed start times
95+ if "start" not in data or data ["start" ] > start_ts :
96+ data ["start" ] = start_ts
4897 end = parser .parse (window .pop (0 ))
49- data ["end" ] = self .dt2ts (end )
98+ end_ts = self .dt2ts (end )
99+ # Keep the latest of any listed end times
100+ if "end" not in data or data ["end" ] < end_ts :
101+ data ["end" ] = end_ts
50102 break
51103 elif line .text .lower ().strip ().startswith ("reason for maintenance:" ):
52104 data ["summary" ] = self .clean_line (line .next_sibling )
@@ -80,10 +132,12 @@ def parse_tables(self, tables: ResultSet, data: Dict):
80132 number_of_circuits = int (len (data_rows ) / 5 )
81133 for idx in range (number_of_circuits ):
82134 data_circuit = {}
83- data_circuit ["circuit_id" ] = self .clean_line (data_rows [0 + idx ])
84- impact = self .clean_line (data_rows [1 + idx ])
135+ data_circuit ["circuit_id" ] = self .clean_line (data_rows [0 + 5 * idx ])
136+ impact = self .clean_line (data_rows [1 + 5 * idx ])
85137 if "hard down" in impact .lower ():
86138 data_circuit ["impact" ] = Impact ("OUTAGE" )
87- circuits .append (CircuitImpact (** data_circuit ))
139+ elif "no expected impact" in impact .lower ():
140+ data_circuit ["impact" ] = Impact ("NO-IMPACT" )
141+ circuits .append (CircuitImpact (** data_circuit ))
88142 if circuits :
89143 data ["circuits" ] = circuits
0 commit comments