Skip to content

Commit 0bf1a3c

Browse files
Merge pull request #123 from networktocode/develop
Release 2.0.7
2 parents 8d6e825 + 5358ee7 commit 0bf1a3c

17 files changed

+713
-34
lines changed

CHANGELOG.md

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

3+
## v2.0.7 - 2021-12-01
4+
5+
### Fixed
6+
7+
- #120 - Improve handling of Zayo notifications.
8+
- #121 - Defer loading of `tzwhere` data until it's needed, to reduce memory overhead.
9+
310
## v2.0.6 - 2021-11-30
411

512
### Added

circuit_maintenance_parser/parsers/zayo.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Zayo parser."""
22
import logging
3+
import re
34
from typing import Dict
45

56
import bs4 # type: ignore
@@ -22,15 +23,19 @@ class SubjectParserZayo1(EmailSubjectParser):
2223
END OF WINDOW NOTIFICATION***Customer Inc.***ZAYO TTN-0000123456 Planned***
2324
***Customer Inc***ZAYO TTN-0001234567 Emergency MAINTENANCE NOTIFICATION***
2425
RESCHEDULE NOTIFICATION***Customer Inc***ZAYO TTN-0005423873 Planned***
26+
27+
Some degenerate examples have been seen as well:
28+
[notices] CANCELLED NOTIFICATION***Customer,inc***ZAYO TTN-0005432100 Planned**
29+
[notices] Rescheduled Maintenance***ZAYO TTN-0005471719 MAINTENANCE NOTIFICATION***
2530
"""
2631

2732
def parse_subject(self, subject):
2833
"""Parse subject of email message."""
2934
data = {}
30-
tokens = subject.split("***")
35+
tokens = re.split(r"\*+", subject)
3136
if len(tokens) == 4:
3237
data["account"] = tokens[1]
33-
data["maintenance_id"] = tokens[2].split(" ")[1]
38+
data["maintenance_id"] = tokens[-2].split(" ")[1]
3439
return [data]
3540

3641

@@ -48,7 +53,7 @@ def parse_html(self, soup):
4853
text = soup.get_text()
4954
if "will be commencing momentarily" in text:
5055
data["status"] = Status("IN-PROCESS")
51-
elif "has been completed" in text:
56+
elif "has been completed" in text or "has closed" in text:
5257
data["status"] = Status("COMPLETED")
5358

5459
return [data]

circuit_maintenance_parser/provider.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,11 @@ class Verizon(GenericProvider):
335335
class Zayo(GenericProvider):
336336
"""Zayo provider custom class."""
337337

338+
_include_filter = {
339+
"text/html": ["Maintenance Ticket #"],
340+
"html": ["Maintenance Ticket #"],
341+
}
342+
338343
_processors: List[GenericProcessor] = [
339344
CombinedProcessor(data_parsers=[EmailDateParser, SubjectParserZayo1, HtmlParserZayo1]),
340345
]

circuit_maintenance_parser/utils.py

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,36 +16,46 @@
1616
dirname = os.path.dirname(__file__)
1717

1818

19+
class classproperty: # pylint: disable=invalid-name,too-few-public-methods
20+
"""Simple class-level equivalent of an @property."""
21+
22+
def __init__(self, method):
23+
"""Wrap a method."""
24+
self.getter = method
25+
26+
def __get__(self, _, cls):
27+
"""Call the wrapped method."""
28+
return self.getter(cls)
29+
30+
1931
class Geolocator:
2032
"""Class to obtain Geo Location coordinates."""
2133

2234
# Keeping caching of local DB and timezone in the class
23-
db_location: Dict[Union[Tuple[str, str], str], Tuple[float, float]] = {}
24-
timezone = None
35+
_db_location: Dict[Union[Tuple[str, str], str], Tuple[float, float]] = {}
36+
_timezone = None
2537

26-
def __init__(self):
27-
"""Initialize instance."""
28-
self.load_db_location()
29-
self.load_timezone()
30-
31-
@classmethod
32-
def load_timezone(cls):
38+
@classproperty
39+
def timezone(cls): # pylint: disable=no-self-argument
3340
"""Load the timezone resolver."""
34-
if cls.timezone is None:
35-
cls.timezone = tzwhere.tzwhere()
41+
if cls._timezone is None:
42+
cls._timezone = tzwhere.tzwhere()
3643
logger.info("Loaded local timezone resolver.")
37-
38-
@classmethod
39-
def load_db_location(cls):
40-
"""Load the localtions DB from CSV into a Dict."""
41-
with open(os.path.join(dirname, "data", "worldcities.csv")) as csvfile:
42-
reader = csv.DictReader(csvfile)
43-
for row in reader:
44-
# Index by city and country
45-
cls.db_location[(row["city_ascii"], row["country"])] = (float(row["lat"]), float(row["lng"]))
46-
# Index by city (first entry wins if duplicated names)
47-
if row["city_ascii"] not in cls.db_location:
48-
cls.db_location[row["city_ascii"]] = (float(row["lat"]), float(row["lng"]))
44+
return cls._timezone
45+
46+
@classproperty
47+
def db_location(cls): # pylint: disable=no-self-argument
48+
"""Load the locations DB from CSV into a Dict."""
49+
if not cls._db_location:
50+
with open(os.path.join(dirname, "data", "worldcities.csv")) as csvfile:
51+
reader = csv.DictReader(csvfile)
52+
for row in reader:
53+
# Index by city and country
54+
cls._db_location[(row["city_ascii"], row["country"])] = (float(row["lat"]), float(row["lng"]))
55+
# Index by city (first entry wins if duplicated names)
56+
if row["city_ascii"] not in cls._db_location:
57+
cls._db_location[row["city_ascii"]] = (float(row["lat"]), float(row["lng"]))
58+
return cls._db_location
4959

5060
def get_location(self, city: str) -> Tuple[float, float]:
5161
"""Get location."""
@@ -64,7 +74,9 @@ def get_location_from_local_file(self, city: str) -> Tuple[float, float]:
6474
city_name = city.split(", ")[0]
6575
country = city.split(", ")[-1]
6676

67-
lat, lng = self.db_location.get((city_name, country), self.db_location.get(city_name, (None, None)))
77+
lat, lng = self.db_location.get( # pylint: disable=no-member
78+
(city_name, country), self.db_location.get(city_name, (None, None)) # pylint: disable=no-member
79+
)
6880
if lat and lng:
6981
logger.debug("Resolved %s to lat %s, lon %sfrom local locations DB.", city, lat, lng)
7082
return (lat, lng)
@@ -92,12 +104,12 @@ def city_timezone(self, city: str) -> str:
92104
if self.timezone is not None:
93105
try:
94106
latitude, longitude = self.get_location(city)
95-
timezone = self.timezone.tzNameAt(latitude, longitude)
107+
timezone = self.timezone.tzNameAt(latitude, longitude) # pylint: disable=no-member
96108
if not timezone:
97109
# In some cases, given a latitued and longitued, the tzwhere library returns
98110
# an empty timezone, so we try with the coordinates from the API as an alternative
99111
latitude, longitude = self.get_location_from_api(city)
100-
timezone = self.timezone.tzNameAt(latitude, longitude)
112+
timezone = self.timezone.tzNameAt(latitude, longitude) # pylint: disable=no-member
101113

102114
if timezone:
103115
logger.debug("Matched city %s to timezone %s", city, timezone)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "circuit-maintenance-parser"
3-
version = "2.0.6"
3+
version = "2.0.7"
44
description = "Python library to parse Circuit Maintenance notifications and return a structured data back"
55
authors = ["Network to Code <[email protected]>"]
66
license = "Apache-2.0"

0 commit comments

Comments
 (0)