Skip to content

Commit 322d821

Browse files
authored
Merge pull request #201 from networktocode/release-v2.2.1
Release v2.2.1
2 parents a38252e + 4402720 commit 322d821

25 files changed

+1748
-135
lines changed

.github/workflows/ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,8 @@ jobs:
149149
run: "echo RELEASE_VERSION=${GITHUB_REF:10} >> $GITHUB_ENV"
150150
- name: "Run Poetry Version"
151151
run: "poetry version $RELEASE_VERSION"
152+
- name: "Run Poetry Build"
153+
run: "poetry build"
152154
- name: "Upload binaries to release"
153155
uses: "svenstaro/upload-release-action@v2"
154156
with:
@@ -176,6 +178,8 @@ jobs:
176178
run: "echo RELEASE_VERSION=${GITHUB_REF:10} >> $GITHUB_ENV"
177179
- name: "Run Poetry Version"
178180
run: "poetry version $RELEASE_VERSION"
181+
- name: "Run Poetry Build"
182+
run: "poetry build"
179183
- name: "Push to PyPI"
180184
uses: "pypa/gh-action-pypi-publish@release/v1"
181185
with:

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# Changelog
22

3+
## v2.2.1 - 2023-01-17
4+
5+
### Changed
6+
7+
- #197 - Updated Equinix parser: Adding support for additional impact statement and notification types.
8+
- #192 - Updated Cogent parser: Adding subject and text parser.
9+
- #186 - Updated Telia Carrier as Arelion (while keeping Telia for backwards compatibility).
10+
11+
### Fixed
12+
13+
- #198 - Fixed Verizon parser: use European-style day-first date parsing
14+
- #187 - Fixed Zayo parser: adds chardet.detect method before decoding data_part.content.
15+
316
## v2.2.0 - 2022-10-25
417

518
### Added

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,10 @@ By default, there is a `GenericProvider` that support a `SimpleProcessor` using
5858

5959
#### Supported providers using the BCOP standard
6060

61+
- Arelion (previously Telia)
6162
- EuNetworks
6263
- NTT
6364
- PacketFabric
64-
- Telia
6565
- Telstra
6666

6767
#### Supported providers based on other parsers

circuit_maintenance_parser/parsers/cogent.py

Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,145 @@
66
from pytz import timezone, UTC
77
from bs4.element import ResultSet # type: ignore
88

9-
from circuit_maintenance_parser.parser import Html, Impact, CircuitImpact, Status
9+
from circuit_maintenance_parser.parser import CircuitImpact, EmailSubjectParser, Html, Impact, Status, Text
1010

1111
logger = logging.getLogger(__name__)
1212

1313
# pylint: disable=too-many-branches
1414

1515

16+
class SubjectParserCogent1(EmailSubjectParser):
17+
"""Subject parser for Cogent nofifications."""
18+
19+
def parse_subject(self, subject: str):
20+
"""Parse subject.
21+
22+
Example:
23+
11/19/2022 Circuit Provider Maintenance - Edina, MN 1-300123456
24+
Correction 06/11/2021 AB987654321-1 Planned Network Maintenance - San Jose, CA 1-123456789
25+
"""
26+
data: Dict = {"circuits": []}
27+
28+
subject = subject.lower()
29+
30+
if subject.startswith("correction") or "rescheduled" in subject:
31+
data["status"] = Status("RE-SCHEDULED")
32+
elif "cancellation" in subject:
33+
data["status"] = Status("CANCELLED")
34+
elif "planned" in subject or "provider" in subject or "emergency" in subject:
35+
data["status"] = Status("CONFIRMED")
36+
elif "completed" in subject:
37+
data["status"] = Status("COMPLETED")
38+
else:
39+
data["status"] = Status("NO-CHANGE")
40+
41+
match = re.search(r".* ([\d-]+)", subject)
42+
if match:
43+
circuit_id = match.group(1)
44+
data["circuits"].append(CircuitImpact(impact=Impact("OUTAGE"), circuit_id=circuit_id.strip()))
45+
46+
return [data]
47+
48+
49+
class TextParserCogent1(Text):
50+
"""Parse text body of Cogent emails."""
51+
52+
def parse_text(self, text):
53+
"""Execute parsing of text.
54+
55+
Example:
56+
CIRCUIT PROVIDER MAINTENANCE
57+
58+
Dear Cogent Customer,
59+
60+
As a valued customer, Cogent is committed to keeping you informed about any changes in the status of your service with us. This email is to alert you regarding a circuit provider maintenance which will affect your connection to Cogent:
61+
62+
Start time: 10:00pm CT 11/19/2022
63+
End time: 5:00am CT 11/20/2022
64+
Work order number: VN16123
65+
Order ID(s) impacted: 1-300123456
66+
Expected Outage/Downtime: 7 hours
67+
68+
Cogent customers receiving service in Edina, MN will be affected by this outage. This outage has been scheduled by Zayo. The purpose of this maintenance is to repair damaged fiber. Only the Cogent Order ID(s) above will be impacted.
69+
70+
During this maintenance window, you will experience an interruption in service while Zayo completes the maintenance activities; the interruption is expected to be less than 7 hours; however, due to the complexity of the work, your downtime may be longer.
71+
72+
Our network operations engineers closely monitor the work and will do everything possible to minimize any inconvenience to you. If you have any problems with your connection after this time, or if you have any questions regarding the maintenance at any point, please call Customer Support at 1-877-7-COGENT and refer to this Maintenance Ticket: VN16123.
73+
74+
"""
75+
data = {
76+
# "circuits": [],
77+
"summary": "Cogent circuit maintenance",
78+
}
79+
80+
lines = text.splitlines()
81+
82+
for line in lines:
83+
if line.startswith("Dear"):
84+
match = re.search(r"Dear (.*),", line)
85+
if match:
86+
data["account"] = match.group(1)
87+
elif line.startswith("Start time:"):
88+
match = re.search(r"Start time: ([A-Za-z\d: ]*) [()A-Za-z\s]+ (\d+/\d+/\d+)", line)
89+
if match:
90+
start_str = " ".join(match.groups())
91+
elif line.startswith("End time:"):
92+
match = re.search(r"End time: ([A-Za-z\d: ]*) [()A-Za-z\s]+ (\d+/\d+/\d+)", line)
93+
if match:
94+
end_str = " ".join(match.groups())
95+
elif line.startswith("Cogent customers receiving service"):
96+
data["summary"] = line
97+
match = re.search(r"[^Cogent].*?((\b[A-Z][a-z\s-]+)+, ([A-Za-z-]+[\s-]))", line)
98+
if match:
99+
local_timezone = timezone(self._geolocator.city_timezone(match.group(1).strip()))
100+
101+
# set start time using the local city timezone
102+
try:
103+
start = datetime.strptime(start_str, "%I:%M %p %d/%m/%Y")
104+
except ValueError:
105+
start = datetime.strptime(start_str, "%I:%M%p %d/%m/%Y")
106+
local_time = local_timezone.localize(start)
107+
# set start time to UTC
108+
utc_start = local_time.astimezone(UTC)
109+
data["start"] = self.dt2ts(utc_start)
110+
logger.info(
111+
"Mapped start time %s at %s (%s), to %s (UTC)",
112+
start_str,
113+
match.group(1).strip(),
114+
local_timezone,
115+
utc_start,
116+
)
117+
# set end time using the local city timezone
118+
try:
119+
end = datetime.strptime(end_str, "%I:%M %p %d/%m/%Y")
120+
except ValueError:
121+
end = datetime.strptime(end_str, "%I:%M%p %d/%m/%Y")
122+
local_time = local_timezone.localize(end)
123+
# set end time to UTC
124+
utc_end = local_time.astimezone(UTC)
125+
data["end"] = self.dt2ts(utc_end)
126+
logger.info(
127+
"Mapped end time %s at %s (%s), to %s (UTC)",
128+
end_str,
129+
match.group(1).strip(),
130+
local_timezone,
131+
utc_end,
132+
)
133+
elif line.startswith("Work order number:"):
134+
match = re.search("Work order number: (.*)", line)
135+
if match:
136+
data["maintenance_id"] = match.group(1)
137+
elif line.startswith("Order ID(s) impacted:"):
138+
data["circuits"] = []
139+
match = re.search(r"Order ID\(s\) impacted: (.*)", line)
140+
if match:
141+
for circuit_id in match.group(1).split(","):
142+
data["circuits"].append(CircuitImpact(impact=Impact("OUTAGE"), circuit_id=circuit_id.strip()))
143+
elif line.startswith("During this maintenance"):
144+
data["summary"] = line
145+
return [data]
146+
147+
16148
class HtmlParserCogent1(Html):
17149
"""Notifications Parser for Cogent notifications."""
18150

circuit_maintenance_parser/parsers/equinix.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ def _parse_b(self, b_elements, data):
8686
impact = Impact.OUTAGE
8787
elif "Loss of redundancy" in impact_line:
8888
impact = Impact.REDUCED_REDUNDANCY
89+
elif "Traffic will be re-routed" in impact_line:
90+
impact = Impact.REDUCED_REDUNDANCY
8991
return impact
9092

9193
def _parse_table(self, theader_elements, data, impact): # pylint: disable=no-self-use
@@ -97,7 +99,31 @@ def _parse_table(self, theader_elements, data, impact): # pylint: disable=no-se
9799
continue
98100
circuit_info = list(tr_elem.find_all("td"))
99101
if circuit_info:
100-
account, _, circuit = circuit_info # pylint: disable=unused-variable
102+
if len(circuit_info) == 4:
103+
# Equinix Connect notifications contain the IBX name
104+
account, _, _, circuit = circuit_info # pylint: disable=unused-variable
105+
elif len(circuit_info) == 14:
106+
# Equinix Fabric notifications include a lot of additional detail on seller and subscriber ID's
107+
(
108+
account,
109+
_,
110+
_,
111+
circuit,
112+
_,
113+
_,
114+
_,
115+
_,
116+
_,
117+
_,
118+
_,
119+
_,
120+
_,
121+
_,
122+
) = circuit_info # pylint: disable=unused-variable
123+
elif len(circuit_info) == 3:
124+
account, _, circuit = circuit_info # pylint: disable=unused-variable
125+
else:
126+
return
101127
data["circuits"].append(
102128
{
103129
"circuit_id": circuit.text,

circuit_maintenance_parser/parsers/verizon.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def parse_tables(self, tables: ResultSet, data: Dict): # pylint: disable=too-ma
5757
data["start"] = self.dt2ts(start)
5858
data["end"] = self.dt2ts(end)
5959

60-
for row in circuit_table.find("tbody").find_all("tr"):
60+
for row in circuit_table.find_all("tr"):
6161
cells = row.find_all("td")
6262
cells_text = [cell.string.strip() for cell in cells if cell.string]
6363
if not cells_text or cells_text[0].startswith("Company Name"):

circuit_maintenance_parser/provider.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import traceback
55

66
from typing import Iterable, List, Dict
7+
import chardet
78

89
from pydantic import BaseModel
910

@@ -19,7 +20,7 @@
1920
from circuit_maintenance_parser.parsers.aquacomms import HtmlParserAquaComms1, SubjectParserAquaComms1
2021
from circuit_maintenance_parser.parsers.aws import SubjectParserAWS1, TextParserAWS1
2122
from circuit_maintenance_parser.parsers.bso import HtmlParserBSO1
22-
from circuit_maintenance_parser.parsers.cogent import HtmlParserCogent1
23+
from circuit_maintenance_parser.parsers.cogent import HtmlParserCogent1, TextParserCogent1, SubjectParserCogent1
2324
from circuit_maintenance_parser.parsers.colt import CsvParserColt1, SubjectParserColt1, SubjectParserColt2
2425
from circuit_maintenance_parser.parsers.equinix import HtmlParserEquinix, SubjectParserEquinix
2526
from circuit_maintenance_parser.parsers.gtt import HtmlParserGTT1
@@ -95,7 +96,8 @@ def filter_check(filter_dict: Dict, data: NotificationData, filter_type: str) ->
9596
if filter_data_type not in filter_dict:
9697
continue
9798

98-
data_part_content = data_part.content.decode().replace("\r", "").replace("\n", "")
99+
data_part_encoding = chardet.detect(data_part.content).get("encoding", "utf-8")
100+
data_part_content = data_part.content.decode(data_part_encoding).replace("\r", "").replace("\n", "")
99101
if any(re.search(filter_re, data_part_content) for filter_re in filter_dict[filter_data_type]):
100102
logger.debug("Matching %s filter expression for %s.", filter_type, data_part_content)
101103
return True
@@ -172,6 +174,14 @@ class AquaComms(GenericProvider):
172174
_default_organizer = "[email protected]"
173175

174176

177+
class Arelion(GenericProvider):
178+
"""Arelion (formerly Telia Carrier) provider custom class."""
179+
180+
_exclude_filter = {EMAIL_HEADER_SUBJECT: ["Disturbance Information"]}
181+
182+
_default_organizer = "[email protected]"
183+
184+
175185
class AWS(GenericProvider):
176186
"""AWS provider custom class."""
177187

@@ -195,6 +205,7 @@ class Cogent(GenericProvider):
195205

196206
_processors: List[GenericProcessor] = [
197207
CombinedProcessor(data_parsers=[EmailDateParser, HtmlParserCogent1]),
208+
CombinedProcessor(data_parsers=[EmailDateParser, TextParserCogent1, SubjectParserCogent1]),
198209
]
199210
_default_organizer = "[email protected]"
200211

@@ -308,12 +319,10 @@ class Sparkle(GenericProvider):
308319
_default_organizer = "[email protected]"
309320

310321

311-
class Telia(GenericProvider):
322+
class Telia(Arelion):
312323
"""Telia provider custom class."""
313324

314-
_exclude_filter = {EMAIL_HEADER_SUBJECT: ["Disturbance Information"]}
315-
316-
_default_organizer = "[email protected]"
325+
# Kept for compatibility purposes, but Telia is renamed Arelion
317326

318327

319328
class Telstra(GenericProvider):

0 commit comments

Comments
 (0)