Skip to content

Commit baba76e

Browse files
authored
Merge pull request #270 from networktocode/release-v2.4.0
Release v2.4.0
2 parents 5232369 + 415a7cb commit baba76e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+4584
-525
lines changed

CHANGELOG.md

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

3+
## v2.4.0 - 2024-02-20
4+
5+
### Added
6+
7+
- [#260](https://github.com/networktocode/circuit-maintenance-parser/pull/260) - Add Google parser
8+
- [#259](https://github.com/networktocode/circuit-maintenance-parser/pull/259) - Add Crown Castle fiber parser
9+
- [#258](https://github.com/networktocode/circuit-maintenance-parser/pull/258) - Add Netflix parser
10+
11+
### Changed
12+
13+
- [#264](https://github.com/networktocode/circuit-maintenance-parser/pull/264) - Adopt Pydantic 2.0
14+
- [#256](https://github.com/networktocode/circuit-maintenance-parser/pull/256) - Improved Equinix parser
15+
16+
### Fixed
17+
18+
- [#257](https://github.com/networktocode/circuit-maintenance-parser/pull/257) - Update incorrect file comment
19+
- [#255](https://github.com/networktocode/circuit-maintenance-parser/pull/255) -
20+
Properly process Amazon emergency maintenance notifications
21+
322
## v2.3.0 - 2023-12-15
423

524
### Added

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ You can leverage this library in your automation framework to process circuit ma
2424
- **provider**: identifies the provider of the service that is the subject of the maintenance notification.
2525
- **account**: identifies an account associated with the service that is the subject of the maintenance notification.
2626
- **maintenance_id**: contains text that uniquely identifies (at least within the context of a specific provider) the maintenance that is the subject of the notification.
27-
- **circuits**: list of circuits affected by the maintenance notification and their specific impact. Note that in a maintenance canceled notification, some providers omit the circuit list, so this may be blank for maintenance notifications with a status of CANCELLED.
27+
- **circuits**: list of circuits affected by the maintenance notification and their specific impact. Note that in a maintenance canceled or completed notification, some providers omit the circuit list, so this may be blank for maintenance notifications with a status of CANCELLED or COMPLETED.
2828
- **start**: timestamp that defines the starting date/time of the maintenance in GMT.
2929
- **end**: timestamp that defines the ending date/time of the maintenance in GMT.
3030
- **stamp**: timestamp that defines the update date/time of the maintenance in GMT.
@@ -71,12 +71,15 @@ By default, there is a `GenericProvider` that supports a `SimpleProcessor` using
7171
- BSO
7272
- Cogent
7373
- Colt
74+
- Crown Castle Fiber
7475
- Equinix
7576
- EXA (formerly GTT)
7677
- HGC
78+
- Google
7779
- Lumen
7880
- Megaport
7981
- Momentum
82+
- Netflix (AS2906 only)
8083
- Seaborn
8184
- Sparkle
8285
- Telstra

circuit_maintenance_parser/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,16 @@
1212
BSO,
1313
Cogent,
1414
Colt,
15+
CrownCastle,
1516
Equinix,
1617
EUNetworks,
1718
GTT,
19+
Google,
1820
HGC,
1921
Lumen,
2022
Megaport,
2123
Momentum,
24+
Netflix,
2225
NTT,
2326
PacketFabric,
2427
Seaborn,
@@ -38,13 +41,16 @@
3841
BSO,
3942
Cogent,
4043
Colt,
44+
CrownCastle,
4145
Equinix,
4246
EUNetworks,
47+
Google,
4348
GTT,
4449
HGC,
4550
Lumen,
4651
Megaport,
4752
Momentum,
53+
Netflix,
4854
NTT,
4955
PacketFabric,
5056
Seaborn,
@@ -80,7 +86,6 @@ def get_provider_class(provider_name: str) -> Type[GenericProvider]:
8086
if provider_parser.get_provider_type() == provider_name:
8187
break
8288
else:
83-
8489
raise NonexistentProviderError(
8590
f"{provider_name} is not a currently supported provider. Only {', '.join(SUPPORTED_PROVIDER_NAMES)}"
8691
)
@@ -90,7 +95,6 @@ def get_provider_class(provider_name: str) -> Type[GenericProvider]:
9095

9196
def get_provider_class_from_sender(email_sender: str) -> Type[GenericProvider]:
9297
"""Returns the notification parser class for an email sender address."""
93-
9498
for provider_parser in SUPPORTED_PROVIDERS:
9599
if provider_parser.get_default_organizer() == email_sender:
96100
break

circuit_maintenance_parser/data.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from typing import List, NamedTuple, Optional, Type, Set
44

55
import email
6-
from pydantic import BaseModel, Extra
6+
from pydantic import BaseModel
77
from circuit_maintenance_parser.constants import EMAIL_HEADER_SUBJECT, EMAIL_HEADER_DATE
88

99

@@ -18,7 +18,7 @@ class DataPart(NamedTuple):
1818
content: bytes
1919

2020

21-
class NotificationData(BaseModel, extra=Extra.forbid):
21+
class NotificationData(BaseModel, extra="forbid"):
2222
"""Base class for Notification Data types."""
2323

2424
data_parts: List[DataPart] = []

circuit_maintenance_parser/output.py

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from typing import List
1010

11-
from pydantic import BaseModel, validator, StrictStr, StrictInt, Extra, PrivateAttr
11+
from pydantic import field_validator, BaseModel, StrictStr, StrictInt, PrivateAttr
1212

1313

1414
class Impact(str, Enum):
@@ -52,7 +52,7 @@ class Status(str, Enum):
5252
NO_CHANGE = "NO-CHANGE"
5353

5454

55-
class CircuitImpact(BaseModel, extra=Extra.forbid):
55+
class CircuitImpact(BaseModel, extra="forbid"):
5656
"""CircuitImpact class.
5757
5858
Each Circuit Maintenance can contain multiple affected circuits, and each one can have a different level of impact.
@@ -73,23 +73,31 @@ class CircuitImpact(BaseModel, extra=Extra.forbid):
7373
... )
7474
Traceback (most recent call last):
7575
...
76-
pydantic.error_wrappers.ValidationError: 1 validation error for CircuitImpact
76+
pydantic_core._pydantic_core.ValidationError: 1 validation error for CircuitImpact
7777
impact
78-
value is not a valid enumeration member; permitted: 'NO-IMPACT', 'REDUCED-REDUNDANCY', 'DEGRADED', 'OUTAGE' (type=type_error.enum; enum_values=[<Impact.NO_IMPACT: 'NO-IMPACT'>, <Impact.REDUCED_REDUNDANCY: 'REDUCED-REDUNDANCY'>, <Impact.DEGRADED: 'DEGRADED'>, <Impact.OUTAGE: 'OUTAGE'>])
78+
Input should be 'NO-IMPACT', 'REDUCED-REDUNDANCY', 'DEGRADED' or 'OUTAGE' [type=enum, input_value='wrong impact', input_type=str]
7979
"""
8080

8181
circuit_id: StrictStr
8282
# Optional Attributes
8383
impact: Impact = Impact.OUTAGE
8484

8585
# pylint: disable=no-self-argument
86-
@validator("impact")
86+
@field_validator("impact")
87+
@classmethod
8788
def validate_impact_type(cls, value):
8889
"""Validate Impact type."""
8990
if value not in Impact:
9091
raise ValueError("Not a valid impact type")
9192
return value
9293

94+
def to_json(self):
95+
"""Return a JSON serializable dict."""
96+
return {
97+
"circuit_id": self.circuit_id,
98+
"impact": self.impact.value,
99+
}
100+
93101

94102
class Metadata(BaseModel):
95103
"""Metadata class to provide context about the Maintenance object."""
@@ -100,15 +108,16 @@ class Metadata(BaseModel):
100108
generated_by_llm: bool = False
101109

102110

103-
class Maintenance(BaseModel, extra=Extra.forbid):
111+
class Maintenance(BaseModel, extra="forbid"):
104112
"""Maintenance class.
105113
106114
Mandatory attributes:
107115
provider: identifies the provider of the service that is the subject of the maintenance notification
108116
account: identifies an account associated with the service that is the subject of the maintenance notification
109117
maintenance_id: contains text that uniquely identifies the maintenance that is the subject of the notification
110118
circuits: list of circuits affected by the maintenance notification and their specific impact. Note this can be
111-
an empty list for notifications with a CANCELLED status if the provider does not populate the circuit list.
119+
an empty list for notifications with a CANCELLED or COMPLETED status if the provider does not populate the
120+
circuit list.
112121
status: defines the overall status or confirmation for the maintenance
113122
start: timestamp that defines the start date of the maintenance in GMT
114123
end: timestamp that defines the end date of the maintenance in GMT
@@ -163,34 +172,40 @@ class Maintenance(BaseModel, extra=Extra.forbid):
163172

164173
def __init__(self, **data):
165174
"""Initialize the Maintenance object."""
166-
self._metadata = data.pop("_metadata")
175+
metadata = data.pop("_metadata")
167176
super().__init__(**data)
177+
self._metadata = metadata
168178

169-
# pylint: disable=no-self-argument
170-
@validator("status")
179+
@field_validator("status")
180+
@classmethod
171181
def validate_status_type(cls, value):
172182
"""Validate Status type."""
173183
if value not in Status:
174184
raise ValueError("Not a valid status type")
175185
return value
176186

177-
@validator("provider", "account", "maintenance_id", "organizer")
187+
@field_validator("provider", "account", "maintenance_id", "organizer")
188+
@classmethod
178189
def validate_empty_strings(cls, value):
179190
"""Validate emptry strings."""
180191
if value in ["", "None"]:
181192
raise ValueError("String is empty or 'None'")
182193
return value
183194

184-
@validator("circuits")
195+
@field_validator("circuits")
196+
@classmethod
185197
def validate_empty_circuits(cls, value, values):
186198
"""Validate non-cancel notifications have a populated circuit list."""
187-
if len(value) < 1 and values["status"] != "CANCELLED":
199+
values = values.data
200+
if len(value) < 1 and str(values["status"]) in ("CANCELLED", "COMPLETED"):
188201
raise ValueError("At least one circuit has to be included in the maintenance")
189202
return value
190203

191-
@validator("end")
204+
@field_validator("end")
205+
@classmethod
192206
def validate_end_time(cls, end, values):
193207
"""Validate that End time happens after Start time."""
208+
values = values.data
194209
if "start" not in values:
195210
raise ValueError("Start time is a mandatory attribute.")
196211
start = values["start"]
@@ -208,6 +223,6 @@ def to_json(self) -> str:
208223
return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=2)
209224

210225
@property
211-
def metadata(self):
226+
def metadata(self) -> Metadata:
212227
"""Get Maintenance Metadata."""
213228
return self._metadata

circuit_maintenance_parser/parser.py

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import bs4 # type: ignore
1313
from bs4.element import ResultSet # type: ignore
1414

15-
from pydantic import BaseModel
15+
from pydantic import BaseModel, PrivateAttr
1616
from icalendar import Calendar # type: ignore
1717

1818
from circuit_maintenance_parser.errors import ParserError
@@ -34,15 +34,15 @@ class Parser(BaseModel):
3434
"""
3535

3636
# _data_types are used to match the Parser to to each type of DataPart
37-
_data_types = ["text/plain", "plain"]
37+
_data_types = PrivateAttr(["text/plain", "plain"])
3838

3939
# TODO: move it to where it is used, Cogent parser
4040
_geolocator = Geolocator()
4141

4242
@classmethod
4343
def get_data_types(cls) -> List[str]:
4444
"""Return the expected data type."""
45-
return cls._data_types
45+
return cls._data_types.get_default()
4646

4747
@classmethod
4848
def get_name(cls) -> str:
@@ -92,7 +92,7 @@ class ICal(Parser):
9292
Reference: https://tools.ietf.org/html/draft-gunter-calext-maintenance-notifications-00
9393
"""
9494

95-
_data_types = ["text/calendar", "ical", "icalendar"]
95+
_data_types = PrivateAttr(["text/calendar", "ical", "icalendar"])
9696

9797
def parser_hook(self, raw: bytes, content_type: str):
9898
"""Execute parsing."""
@@ -164,7 +164,7 @@ def parse_ical(gcal: Calendar) -> List[Dict]:
164164
class Html(Parser):
165165
"""Html parser."""
166166

167-
_data_types = ["text/html", "html"]
167+
_data_types = PrivateAttr(["text/html", "html"])
168168

169169
@staticmethod
170170
def remove_hex_characters(string):
@@ -201,7 +201,11 @@ def clean_line(line):
201201
class EmailDateParser(Parser):
202202
"""Parser for Email Date."""
203203

204-
_data_types = [EMAIL_HEADER_DATE]
204+
_data_types = PrivateAttr(
205+
[
206+
EMAIL_HEADER_DATE,
207+
]
208+
)
205209

206210
def parser_hook(self, raw: bytes, content_type: str):
207211
"""Execute parsing."""
@@ -214,7 +218,11 @@ def parser_hook(self, raw: bytes, content_type: str):
214218
class EmailSubjectParser(Parser):
215219
"""Parse data from subject or email."""
216220

217-
_data_types = [EMAIL_HEADER_SUBJECT]
221+
_data_types = PrivateAttr(
222+
[
223+
EMAIL_HEADER_SUBJECT,
224+
]
225+
)
218226

219227
def parser_hook(self, raw: bytes, content_type: str):
220228
"""Execute parsing."""
@@ -236,7 +244,7 @@ def bytes_to_string(string):
236244
class Csv(Parser):
237245
"""Csv parser."""
238246

239-
_data_types = ["application/csv", "text/csv", "application/octet-stream"]
247+
_data_types = PrivateAttr(["application/csv", "text/csv", "application/octet-stream"])
240248

241249
def parser_hook(self, raw: bytes, content_type: str):
242250
"""Execute parsing."""
@@ -255,7 +263,11 @@ def parse_csv(raw: bytes) -> List[Dict]:
255263
class Text(Parser):
256264
"""Text parser."""
257265

258-
_data_types = ["text/plain"]
266+
_data_types = PrivateAttr(
267+
[
268+
"text/plain",
269+
]
270+
)
259271

260272
def parser_hook(self, raw: bytes, content_type: str):
261273
"""Execute parsing."""
@@ -278,7 +290,7 @@ def parse_text(self, text) -> List[Dict]:
278290
class LLM(Parser):
279291
"""LLM parser."""
280292

281-
_data_types = ["text/html", "html", "text/plain"]
293+
_data_types = PrivateAttr(["text/html", "html", "text/plain"])
282294

283295
_llm_question = """Please, could you extract a JSON form without any other comment,
284296
with the following JSON schema (timestamps in EPOCH and taking into account the GMT offset):

circuit_maintenance_parser/parsers/aws.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""AquaComms parser."""
1+
"""AWS parser."""
22
import hashlib
33
import logging
44
import quopri
@@ -65,7 +65,7 @@ def parse_text(self, text):
6565
maintenace_id = ""
6666
status = Status.CONFIRMED
6767
for line in text.splitlines():
68-
if "planned maintenance" in line.lower():
68+
if "planned maintenance" in line.lower() or "maintenance has been scheduled" in line.lower():
6969
data["summary"] = line
7070
search = re.search(
7171
r"([A-Z][a-z]{2}, [0-9]{1,2} [A-Z][a-z]{2,9} [0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2} [A-Z]{2,3}) to ([A-Z][a-z]{2}, [0-9]{1,2} [A-Z][a-z]{2,9} [0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2} [A-Z]{2,3})",

0 commit comments

Comments
 (0)