Repository URL to install this package:
|
Version:
0.2.1 ▾
|
xpd-mails-connect
/
xpd_mails_connect.py
|
|---|
__version__="0.2.1"
import datetime
import tempfile
import logging
from io import StringIO
import pandas as pd
from gmr_underway_connect.underway_data import UnderwayExpeditionDataHandler
from trident_helix4_parser.helix4_parser import TridentHelix4MessageParser
logger = logging.getLogger(__name__)
def get_xpd_mail_data(days_in_the_past=1):
"""
Gets curret data (i.e. mails from today) from known expedition mailboxes
:param days_in_the_past: Days in the past to search maisl for, default=1, i.e. today only
:return: pandas dataframe suitable for import into geoserver
"""
# add aditional mailboxes here if needed
# DST disabled
#dst_mc = DstMailsConnector()
#df_dsc = dst_mc.get_data(days_in_the_past)
argo_mc = ArgoRecoveryMailConnector()
df_argo = argo_mc.get_data(days_in_the_past)
return df_argo
class ArgoRecoveryMailConnector:
"""Handles mails sent by ARGO Float ARGO_01 (WMO: 7900561) during recovery mode"""
xpd_mail_folder = 'XPD_EugenSeibold_ARGO_01'
platform_shortname = 'ARGO_01'
def __init__(self):
self.xpd_h = UnderwayExpeditionDataHandler(self.xpd_mail_folder)
self.mail_ids = []
self.trident_helix_4_messages = {}
def get_data(self, days_in_the_past:int) -> pd.DataFrame:
"""
Extracts position data ARGO_01 from mails received by dm-data.
:param days_in_the_past: Number if days in the past to consider when looking for mails
:return: pandas dataframe suitable for import into geoserver
"""
ret = None
if days_in_the_past >= 0: # allow passing -1 resulting in empty df for test purposes
data = self.get_data_from_mails(days_in_the_past)
ret = pd.DataFrame(data.values())
ret['platform_shortname'] = ArgoRecoveryMailConnector.platform_shortname
if ret is not None and len(ret) > 0:
return ret
else: # no mails w/ attachment found in specified time period
logger.warning(f'no mails with attachment found in the last {days_in_the_past} days. '
f'Returning empty dataset.')
return pd.DataFrame(columns=['platform_shortname','obs_timestamp','lat', 'lon'])
def get_data_from_mails(self, days_in_past=0):
"""
Connects to mailserver, gets mails from mission folder, extracts and parses data string from each mail
:param days_in_past: number of days to look in the past, default:0->all mails
:return: dict with mail_id as key, data-dict as value
"""
ret = {}
all_textblocks = self.xpd_h.get_all_text_blocks(days_in_the_past=days_in_past)
if not all_textblocks:
logger.warning(f'Cannot get data string from mail, no mails found in the last {days_in_past} day(s) ')
return {}
for mail_id, textblocks in all_textblocks.items():
for tb in textblocks:
try:
data_dict = self._parse_data_string(tb)
assert 'obs_timestamp' in data_dict.keys()
assert 'lat' in data_dict.keys()
assert 'lon' in data_dict.keys()
ret[mail_id] = data_dict
except:
# we hit a text block which cannot be parsed. Probably harmless.
pass
return ret
@staticmethod
def _parse_data_string(log_str):
"""
Parses content of data string returns dict
:param log_str:
:return:
"""
ret = pd.read_csv(StringIO(log_str), header=None, names=['ignoreme_1',
'obs_timestamp',
'lat', 'lon',
'ignoreme_2'])
# drop unused
ret.drop(['ignoreme_1', 'ignoreme_2'], inplace=True, axis=1)
# convert to timestamp object
ret['obs_timestamp'] = ret['obs_timestamp'].apply(ArgoRecoveryMailConnector._parse_timestamp)
return ret.to_dict('records')[0]
@staticmethod
def _parse_timestamp(ts):
"""
Parses timstamp string, returns datetime.datetime object
:param ts:
:return:
"""
ret = datetime.datetime.strptime(ts, r'%m/%d/%Y %H:%M:%S')
# add timezone info
ret = ret.replace(tzinfo=datetime.timezone.utc)
return ret
class DstMailsConnector:
"""Handles mails sent by drifting sediment trap"""
xpd_mail_folder = 'XPD_M160_DST_01'
platform_shortname = 'DST_01'
# real world example of line in email:
# > Time of Session (UTC): Thu Oct 3 14:52:49 2019
# the double whitespace above seems to be a decoding artifact, is unpacked to '=A' in python:
# > Time of Session (UTC): Thu Oct =A03 14:52:49 2019
time_marker = 'Time of Session (UTC):' # line w/ datetime starts w/ this str
time_format = '%a %b %d %H:%M:%S %Y'
def __init__(self):
self.xpd_h = UnderwayExpeditionDataHandler(self.xpd_mail_folder)
self.mail_ids = []
self.trident_helix_4_messages = {}
def get_data(self, days_in_the_past:int) -> pd.DataFrame:
"""
Extracts position data for the Drifting Sediment Traf DST_01 from mails received by dm-data.
:param days_in_the_past: Number if days in the past to consider when looking for mails
:return: pandas dataframe suitable for import into geoserver
"""
data = []
if days_in_the_past >= 0: # allow passing -1 resulting in empty df for test purposes
all_messages = []
for mail_id, message in self.get_trident_helix_4_messages(days_in_the_past).items():
all_messages.append(message)
for msg in all_messages:
for loc in msg.locations:
data.append({
'platform_shortname': DstMailsConnector.platform_shortname,
'obs_timestamp': loc.timestamp,
'lat': loc.latitude,
'lon': loc.longitude
})
ret = pd.DataFrame(data)
if len(ret) > 0:
return ret
else: # no mails w/ attachment found in specified time period
logger.warning(f'no mails with attachment found in the last {days_in_the_past} days. '
f'Returning empty dataset.')
return pd.DataFrame(columns=['platform_shortname','obs_timestamp','lat', 'lon'])
def get_trident_helix_4_messages(self, days_in_past):
"""
Parses date from Mail body, detaches sbd files and parses them
:param days_in_past: number of days in the past to consider
:return: dict with mail_ids as key, TridentHelix4Message as value
"""
dates = self.get_dates_from_mails(days_in_past)
ret = {}
udh = UnderwayExpeditionDataHandler(xpd_folder=DstMailsConnector.xpd_mail_folder)
with tempfile.TemporaryDirectory() as tmpdirname:
detached = udh.detach_data(days_in_past, tmpdirname)
if not detached: # no mails found
return {} # --> return empty result set
for mail_id, detached_files in detached.items():
for detached_file in detached_files:
if detached_file.endswith('.sbd'):
assert dates.get(mail_id), f'Did not find mail_id {mail_id} in dates from mail!'
date = dates.get(mail_id)
parser = TridentHelix4MessageParser(date=date)
ret[mail_id] = parser.parse_file(detached_file)
# attention, if there are more than one .sbd file attached, only the *last* one is kept
return ret
def get_dates_from_mails(self, days_in_past=0):
"""
Connects to mailserver, gets mails from mission folder and extracts date from each mail
:param days_in_past: number of days to look in the past, default:0->all mails
:return: dict with mail_id as key, datetime.datetime object as value
"""
ret = {}
textblocks = self.xpd_h.get_all_text_blocks(days_in_the_past=days_in_past)
if not textblocks:
logger.warning(f'Cannot get dates from mail, no mails found in the last {days_in_past} day(s) ')
return {}
for mail_id, textblocks in textblocks.items():
ret[mail_id] = self._parse_date(textblocks)
return ret
def _parse_date(self, texblocks: [str]) -> datetime.datetime:
"""
Looks through passed list with texts to find and parse the date contained
:param texblocks:
:return: datetime.datetime object or None
"""
# separate lines from text blocks, put in one large list
lines = []
for tb in texblocks:
lines.extend(tb.splitlines())
for line in lines:
if DstMailsConnector.time_marker in line:
# split
date_substring = DstMailsConnector._split_date_substring(line)
# clean up
date_substring = DstMailsConnector._scrub_datestr(date_substring)
# parse
ret = DstMailsConnector._parse_date_substring(date_substring, DstMailsConnector.time_format)
if ret:
return ret
@staticmethod
def _scrub_datestr(datestr):
"""
Returns a cleaned copy of the suspected datestring
:param datestr:
:return:
"""
date_substring = datestr.strip()
date_substring = date_substring.replace(' ', ' ')
date_substring = date_substring.replace('=A0', '')
return date_substring
@staticmethod
def _split_date_substring(line):
"""Splits a line along the designated time marker, returning the last element which should be the datestring"""
return line.split(DstMailsConnector.time_marker)[-1]
@staticmethod
def _parse_date_substring(substring:str, time_format:str) -> datetime.datetime:
"""
Attempts to parse a date from a given string using the given format. Fails silently
:param substring: the substring to parse
:param time_format: the strptime format to use
:return: datetime object or None
"""
try:
ret = datetime.datetime.strptime(substring, time_format)
assert 2018 < ret.year, f'Date {datetime} not in expected time period'
return ret
except Exception:
return None