Skip to content

Commit a9854bb

Browse files
authored
Merge pull request #397 from networktocode/release-v2.11.0
Release v2.11.0
2 parents 6336493 + 4f66af7 commit a9854bb

35 files changed

Lines changed: 1260 additions & 209 deletions

.cookiecutter.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"cookiecutter": {
3+
"codeowner_github_usernames": "@chadell @glennmatthews @pke11y @scetron @jvanderaa",
4+
"full_name": "Network to Code, LLC",
5+
"email": "info@networktocode.com",
6+
"github_org": "networktocode",
7+
"description": "Python library to parse circuit maintenances from network service providers.",
8+
"project_name": "circuit-maintenance-parser",
9+
"project_slug": "circuit-maintenance-parser",
10+
"repo_url": "https://github.com/networktocode/circuit-maintenance-parser",
11+
"base_url": "circuit-maintenance-parser",
12+
"project_python_name": "circuit_maintenance_parser",
13+
"project_python_base_version": "3.10",
14+
"project_with_config_settings": "no",
15+
"generate_docs": "yes",
16+
"version": "2.10.0",
17+
"original_publish_year": "2021"
18+
}
19+
}

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ jobs:
9595
strategy:
9696
fail-fast: true
9797
matrix:
98-
python-version: ["3.10", "3.11", "3.12"]
98+
python-version: ["3.10", "3.11", "3.12", "3.13"]
9999
env:
100100
PYTHON_VER: "${{ matrix.python-version }}"
101101
steps:
@@ -131,7 +131,7 @@ jobs:
131131
strategy:
132132
fail-fast: true
133133
matrix:
134-
python-version: ["3.10", "3.11", "3.12"]
134+
python-version: ["3.10", "3.11", "3.12", "3.13"]
135135
runs-on: "ubuntu-24.04"
136136
env:
137137
PYTHON_VER: "${{ matrix.python-version }}"

circuit_maintenance_parser/parser.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,9 +208,9 @@ def parse_html(
208208
def clean_line(line):
209209
"""Clean up of undesired characters from Html."""
210210
try:
211-
return line.text.strip()
211+
return line.text.strip().replace("\r\n", "\n").replace("\r", "\n")
212212
except AttributeError:
213-
return line.strip()
213+
return line.strip().replace("\r\n", "\n").replace("\r", "\n")
214214

215215

216216
class EmailDateParser(Parser):

circuit_maintenance_parser/parsers/google.py

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import re
55
from datetime import datetime
66

7-
from circuit_maintenance_parser.parser import CircuitImpact, Html, Impact, Status
7+
from circuit_maintenance_parser.parser import CircuitImpact, EmailSubjectParser, Html, Impact, Status
88

99
# pylint: disable=too-many-nested-blocks, too-many-branches
1010

@@ -18,7 +18,7 @@ def parse_html(self, soup):
1818
"""Execute parsing."""
1919
data = {}
2020
data["circuits"] = []
21-
data["status"] = Status.CONFIRMED
21+
end_time_explicit = False
2222

2323
for span in soup.find_all("span"):
2424
if span.string is None:
@@ -29,6 +29,7 @@ def parse_html(self, soup):
2929
elif span.string.strip() == "End Time:":
3030
dt_str = span.next_sibling.string.strip()
3131
data["end"] = self.dt2ts(datetime.strptime(dt_str, "%Y-%m-%d %H:%M:%S %z UTC"))
32+
end_time_explicit = True
3233
elif span.string.strip() == "Peer ASN:":
3334
data["account"] = span.parent.next_sibling.string.strip()
3435
elif span.string.strip() == "Google Neighbor Address(es):":
@@ -37,9 +38,42 @@ def parse_html(self, soup):
3738
cid = googleaddr + "-" + span.parent.next_sibling.string.strip()
3839
data["circuits"].append(CircuitImpact(circuit_id=cid, impact=Impact.OUTAGE))
3940

40-
summary = list(soup.find("div").find("div").strings)[-1].strip()
41-
match = re.search(r" - Reference (.*)$", summary)
42-
data["summary"] = summary
43-
data["maintenance_id"] = match[1]
41+
# Google sometimes send notifications without End Time specified
42+
if not end_time_explicit and data["start"]:
43+
# Since start and end times cannot be equal, manufacturing end date by adding 1hr to start date
44+
end_time_delta = 3600
45+
data["end"] = data["start"] + end_time_delta
46+
47+
return [data]
48+
49+
50+
class SubjectParserGoogle1(EmailSubjectParser):
51+
"""Subject Parser for Google notifications."""
52+
53+
def parse_subject(self, subject):
54+
"""Parse the subject line."""
55+
data = {}
56+
57+
# Example subject format - "[Scheduled] Google Planned Network Maintenance Notification - Reference PCR/123456"
58+
# Group 1: Status (e.g., Scheduled, Completed, Canceled)
59+
# Group 2: Maintenance ID (e.g., PCR/123456)
60+
match = re.search(r"(\[\S+\]).*Reference\s+(\S+)", subject, re.IGNORECASE | re.DOTALL)
61+
match_2 = re.search(r"\[\S+\]\s+(.*)", subject, re.IGNORECASE | re.DOTALL)
62+
63+
if match:
64+
status_str = match.group(1).upper()
65+
data["maintenance_id"] = match.group(2).strip()
66+
if "COMPLETED" in status_str:
67+
data["status"] = Status.COMPLETED
68+
# To handle both Cancelled and Canceled spelling options just in case
69+
elif "CANCEL" in status_str:
70+
data["status"] = Status.CANCELLED
71+
elif "SCHEDULED" in status_str:
72+
data["status"] = Status.CONFIRMED
73+
# If unable to match, we fallback to default confirmed
74+
else:
75+
data["status"] = Status.CONFIRMED
76+
if match_2:
77+
data["summary"] = match_2.group(1)
4478

4579
return [data]

circuit_maintenance_parser/parsers/lumen.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,10 @@ def parse_spans(self, spans: ResultSet, data: Dict):
6060
for sibling in line.next_siblings:
6161
text_sibling = sibling.text.strip() if isinstance(sibling, bs4.element.Tag) else sibling.strip()
6262
if text_sibling != "":
63-
if (
64-
"This maintenance is scheduled" in text_sibling
65-
or "The scheduled maintenance work has begun" in text_sibling
66-
):
63+
if "The scheduled maintenance work has begun" in text_sibling:
6764
data["status"] = Status("IN-PROCESS")
65+
elif "This maintenance is scheduled" in text_sibling:
66+
data["status"] = Status("CONFIRMED")
6867
if "GMT" in text_sibling:
6968
stamp = parser.parse(text_sibling.split(" GMT")[0])
7069
data["stamp"] = self.dt2ts(stamp)

circuit_maintenance_parser/provider.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from circuit_maintenance_parser.parsers.crowncastle import HtmlParserCrownCastle1
2525
from circuit_maintenance_parser.parsers.equinix import HtmlParserEquinix, SubjectParserEquinix
2626
from circuit_maintenance_parser.parsers.globalcloudxchange import HtmlParserGcx1, SubjectParserGcx1
27-
from circuit_maintenance_parser.parsers.google import HtmlParserGoogle1
27+
from circuit_maintenance_parser.parsers.google import HtmlParserGoogle1, SubjectParserGoogle1
2828
from circuit_maintenance_parser.parsers.gtt import HtmlParserGTT1
2929
from circuit_maintenance_parser.parsers.hgc import HtmlParserHGC1, HtmlParserHGC2, SubjectParserHGC1
3030
from circuit_maintenance_parser.parsers.lumen import HtmlParserLumen1
@@ -130,12 +130,6 @@ def get_maintenances(self, data: NotificationData) -> Iterable[Maintenance]:
130130
logger.debug("Skipping notification %s due filtering policy for %s.", data, self.__class__.__name__)
131131
return []
132132

133-
if os.getenv("PARSER_OPENAI_API_KEY"):
134-
self._processors.append(CombinedProcessor(data_parsers=[EmailDateParser, OpenAIParser]))
135-
136-
# Add subject to all html or text/* data_parts if not already present.
137-
self.add_subject_to_text(data)
138-
139133
for processor in self._processors:
140134
try:
141135
return processor.process(data, self.get_extended_data())
@@ -150,6 +144,22 @@ def get_maintenances(self, data: NotificationData) -> Iterable[Maintenance]:
150144
related_exceptions.append(exc)
151145
continue
152146

147+
# Use OpenAI parser as a last resort if all other processors failed.
148+
if os.getenv("PARSER_OPENAI_API_KEY"):
149+
self.add_subject_to_text(data)
150+
openai_processor = CombinedProcessor(data_parsers=[EmailDateParser, OpenAIParser])
151+
try:
152+
return openai_processor.process(data, self.get_extended_data())
153+
except ProcessorError as exc:
154+
process_error_message = (
155+
f"- Processor {openai_processor.__class__.__name__} from {provider_name} failed due to: %s\n"
156+
)
157+
logger.debug(process_error_message, traceback.format_exc())
158+
159+
related_exc = rgetattr(exc, "__cause__")
160+
error_message += process_error_message % related_exc
161+
related_exceptions.append(exc)
162+
153163
raise ProviderError(
154164
(f"Failed creating Maintenance notification for {provider_name}.\nDetails:\n{error_message}"),
155165
related_exceptions=related_exceptions,
@@ -165,7 +175,7 @@ def add_subject_to_text(self, data: NotificationData):
165175
if subject:
166176
new_data_parts = []
167177
for part in data.data_parts:
168-
if part.type.startswith("text/") or part.type.startswith("html"):
178+
if (part.type.startswith("text/") or part.type.startswith("html")) and part.type != "text/calendar":
169179
content_str = part.content.decode(errors="ignore")
170180
if subject not in content_str:
171181
# Append subject and update content
@@ -364,7 +374,7 @@ class Google(GenericProvider):
364374

365375
_processors: List[GenericProcessor] = PrivateAttr(
366376
[
367-
CombinedProcessor(data_parsers=[EmailDateParser, HtmlParserGoogle1]),
377+
CombinedProcessor(data_parsers=[EmailDateParser, HtmlParserGoogle1, SubjectParserGoogle1]),
368378
]
369379
)
370380
_default_organizer = PrivateAttr("noc-noreply@google.com")

docs/release_notes.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,37 @@ This document describes all new features and changes in the release. The format
44

55
<!-- towncrier release notes start -->
66

7+
# v2.11 Release Notes
8+
9+
This document describes all new features and changes in the release. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
10+
11+
## Release Overview
12+
13+
- Major features or milestones
14+
- Changes to compatibility with Nautobot and/or other apps, libraries etc.
15+
16+
## [v2.11.0 (2026-04-15)](https://github.com/networktocode/circuit-maintenance-parser/releases/tag/v2.11.0)
17+
18+
### Added
19+
20+
- [#375](https://github.com/networktocode/circuit-maintenance-parser/issues/375) - Added Google subject parser with support for multiple status types and notifications without end times.
21+
22+
### Fixed
23+
24+
- [#372](https://github.com/networktocode/circuit-maintenance-parser/issues/372) - Refactored OpenAI parser to be used as a fallback and fixed subject injection to skip text/calendar parts.
25+
- [#377](https://github.com/networktocode/circuit-maintenance-parser/issues/377) - Fixed Lumen parser incorrectly marking scheduled future maintenance events as IN-PROCESS instead of CONFIRMED. Bug introduced in commit 737aa4e9 (Aug 2021).
26+
27+
### Dependencies
28+
29+
- [#391](https://github.com/networktocode/circuit-maintenance-parser/issues/391) - Upgraded lxml to version 6.0.2 and normalized CRLF line endings in HTML parser output.
30+
- [#393](https://github.com/networktocode/circuit-maintenance-parser/issues/393) - Pinned lxml version to support >=4.6.2,<7.
31+
32+
### Housekeeping
33+
34+
- [#384](https://github.com/networktocode/circuit-maintenance-parser/issues/384) - Added Python 3.13 to supported versions and CI matrix.
35+
- Added required cookiecutter json file for drift management setup.
36+
- Fixed incorrectly set cookiecutter project_slug.
37+
738
## [v2.10.0 (2026-01-27)](https://github.com/networktocode/circuit-maintenance-parser/releases/tag/v2.10.0)
839

940
### Added
@@ -14,7 +45,7 @@ This document describes all new features and changes in the release. The format
1445

1546
- [#360](https://github.com/networktocode/circuit-maintenance-parser/issues/360) - Updated lxml to include version 6
1647
- [#361](https://github.com/networktocode/circuit-maintenance-parser/issues/361) - Updated timezonefinder to v8.2.0
17-
- [#345](https://github.com/networktocode/circuit_maintenance_parser/issues/345) - Updated minimum Python version to 3.10 and upgraded dependencies including pytest (9.0), pylint (4.0), towncrier (25.8), backoff (2.2), and type stubs
48+
- [#345](https://github.com/networktocode/circuit-maintenance-parser/issues/345) - Updated minimum Python version to 3.10 and upgraded dependencies including pytest (9.0), pylint (4.0), towncrier (25.8), backoff (2.2), and type stubs
1849

1950
### Housekeeping
2051

0 commit comments

Comments
 (0)