Skip to content

Commit e30783e

Browse files
authored
Merge pull request #395 from nsw3550/summitig
Add in a parser for vendor SummitIG
2 parents 9fb83fd + 2266be5 commit e30783e

9 files changed

Lines changed: 571 additions & 0 deletions

File tree

changes/395.added

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

circuit_maintenance_parser/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
PacketFabric,
3434
Seaborn,
3535
Sparkle,
36+
SummitIG,
3637
Tata,
3738
Telia,
3839
Telstra,
@@ -71,6 +72,7 @@
7172
RETN,
7273
Seaborn,
7374
Sparkle,
75+
SummitIG,
7476
Tata,
7577
Telia,
7678
Telstra,
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
"""SummitIG Parser."""
2+
3+
import re
4+
from datetime import datetime
5+
from typing import Any, ClassVar, Dict, List
6+
from zoneinfo import ZoneInfo
7+
8+
from bs4.element import ResultSet # type: ignore
9+
10+
from circuit_maintenance_parser.parser import CircuitImpact, Html, Impact, Status
11+
12+
13+
class HtmlParserSummitIG(Html):
14+
"""Custom parser for SummitIG HTML circuit maintenance notifications."""
15+
16+
STATUS_MAP: ClassVar[Dict[str, Status]] = {
17+
"Scheduled": Status.CONFIRMED,
18+
"Restored": Status.COMPLETED,
19+
"Splicing": Status.IN_PROCESS,
20+
}
21+
22+
def parse_html(self, soup: ResultSet) -> List[Dict]:
23+
"""Parse a summit circuit maintenance email.
24+
25+
Args:
26+
soup (ResultSet): beautiful soup object containing the html portion of an email.
27+
28+
Returns:
29+
List[Dict]: Data dictionaries containing the circuit maintenance data.
30+
"""
31+
data: Dict[str, Any] = {}
32+
33+
table = soup.find("table")
34+
if table is None:
35+
return [data]
36+
37+
for row in table.find_all("tr"):
38+
th = row.find("th")
39+
td = row.find("td")
40+
if not th or not td:
41+
continue
42+
43+
key = th.get_text(strip=True)
44+
value = td.get_text(strip=True)
45+
self._apply_field(data, key, value)
46+
47+
return [data]
48+
49+
def _apply_field(self, data: Dict[str, Any], key: str, value: str) -> None:
50+
"""Map a table field into the parsed data dictionary.
51+
52+
Args:
53+
data (dict[str, Any]): a dictionary containing tags and their values.
54+
key (str): the key that we want to map to the appropriate field.
55+
value (str): the value that we want to associate with the appropriate field.
56+
"""
57+
simple_fields = {
58+
"Company": ("account", value),
59+
"Event": ("maintenance_id", value),
60+
"Description": ("summary", value),
61+
}
62+
63+
if key in simple_fields:
64+
field_name, field_value = simple_fields[key]
65+
data[field_name] = field_value
66+
return
67+
68+
if key == "Status":
69+
data["status"] = self.STATUS_MAP.get(value, Status.TENTATIVE)
70+
return
71+
72+
if key == "Circuits":
73+
circuits = []
74+
for circuit in value.split(","):
75+
c = CircuitImpact(circuit_id=circuit, impact=Impact.OUTAGE)
76+
circuits.append(c)
77+
data["circuits"] = circuits
78+
return
79+
80+
if "Start" in key:
81+
timezone = _extract_timezone(key)
82+
full_time = _parse_summit_datetime(value, timezone)
83+
data["start"] = full_time
84+
return
85+
86+
if "End" in key:
87+
timezone = _extract_timezone(key)
88+
full_time = _parse_summit_datetime(value, timezone)
89+
data["end"] = full_time
90+
91+
92+
def _extract_timezone(key: str) -> str:
93+
"""Extract a timezone from a SummitIG table start or end time.
94+
95+
Args:
96+
key (str): A string in the format of 'Start Time (EST)' or 'End Time (EST)'.
97+
98+
Returns:
99+
str: The timezone (EST, CDT, ...) extracted from the input.
100+
"""
101+
# We need to pull the timezone out of the key
102+
# Start Time (EST)
103+
match = re.search(r"\((.*?)\)", key)
104+
if not match:
105+
raise ValueError(f"No timezone found in Key: '{key}'")
106+
return match.group(1)
107+
108+
109+
def _parse_summit_datetime(date_time: str, timezone: str) -> int:
110+
"""Create a unix timestamp based on a date and a timezone provided by Summit.
111+
112+
Args:
113+
date_time: A datetime string without the timezone '2/21/2025'.
114+
timezone: The abbreviated timezone 'EST', 'PST', etc.
115+
116+
Returns:
117+
int: The unix timestamp based on the datetime and timezone inputs.
118+
"""
119+
# Parse datetime string
120+
dt = datetime.strptime(date_time, "%m/%d/%Y %I:%M %p")
121+
122+
# Map timezone abbreviation → IANA timezone
123+
tz_map = {
124+
"EST": "America/New_York",
125+
"EDT": "America/New_York",
126+
"CST": "America/Chicago",
127+
"CDT": "America/Chicago",
128+
"MST": "America/Denver",
129+
"MDT": "America/Denver",
130+
"PST": "America/Los_Angeles",
131+
"PDT": "America/Los_Angeles",
132+
}
133+
134+
if timezone not in tz_map:
135+
raise ValueError(f"Unknown timezone: {timezone}")
136+
137+
# Attach timezone
138+
dt = dt.replace(tzinfo=ZoneInfo(tz_map[timezone]))
139+
140+
return int(dt.timestamp())

circuit_maintenance_parser/provider.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
SubjectParserSeaborn2,
4242
)
4343
from circuit_maintenance_parser.parsers.sparkle import HtmlParserSparkle1
44+
from circuit_maintenance_parser.parsers.summitig import HtmlParserSummitIG
4445
from circuit_maintenance_parser.parsers.tata import HtmlParserTata, SubjectParserTata
4546
from circuit_maintenance_parser.parsers.telstra import HtmlParserTelstra1, HtmlParserTelstra2
4647
from circuit_maintenance_parser.parsers.telxius import HtmlParserTelxius1, SubjectParserTelxius1
@@ -525,6 +526,17 @@ class Sparkle(GenericProvider):
525526
_default_organizer = PrivateAttr("TISAmericaNOC@tisparkle.com")
526527

527528

529+
class SummitIG(GenericProvider):
530+
"""SummitIG provider custom class."""
531+
532+
_processors: List[GenericProcessor] = PrivateAttr(
533+
[
534+
CombinedProcessor(data_parsers=[HtmlParserSummitIG, EmailDateParser]),
535+
]
536+
)
537+
_default_organizer = PrivateAttr("outages@summitig.net")
538+
539+
528540
class Tata(GenericProvider):
529541
"""Tata provider custom class."""
530542

0 commit comments

Comments
 (0)