class HistWindowSelect(QGroupBox): """An extension of the QGroupBox widget. It allows the user to select a start date and end date for the historical window in the Monte Carlo VaR calculation application. The end date of the historical window is set to 22 days from the start date as a minimum. This restriction is implemented through the QDateEdit's set minimum date function. """ end_signal = QtCore.pyqtSignal(dt.datetime) def __init__(self, parent=None): super(HistWindowSelect, self).__init__(parent) self.setTitle('Historical Window Select') grid = QGridLayout() lbl_start = QLabel('Start Date: ') lbl_end = QLabel('End Date: ') self.deStart = QDateEdit() self.deStart.setCalendarPopup(True) self.deStart.setMinimumDateTime(dt.datetime(2015, 1, 1)) self.deStart.setMaximumDateTime( self.getWeekdaysBack(dt.datetime.now(), 22)) self.deStart.dateChanged.connect(self.onStartDateChange) self.deEnd = QDateEdit() self.deEnd.setCalendarPopup(True) self.deEnd.setMaximumDateTime(dt.datetime.now()) self.deEnd.dateChanged.connect(self.onEndDateChange) self.setEndDateMinimum() lbl_disc = QLabel( 'Note: A minimum historical window length <br>of 22 business days has been imposed.' ) grid.addWidget(lbl_start, 0, 0) grid.addWidget(lbl_end, 1, 0) grid.addWidget(self.deStart, 0, 1) grid.addWidget(self.deEnd, 1, 1) grid.addWidget(lbl_disc, 2, 1) self.setLayout(grid) def onStartDateChange(self): """Checks if new start date is a weekend. If so, it adds the appropriate number of days to bring the start date to the following Monday. After, it updates the minimum value of the end date, which must be 22 business days after the start date. """ deStart = dt.datetime.combine(self.deStart.date().toPyDate(), dt.datetime.min.time()) if deStart.weekday() == 5: deStart = deStart + timedelta(days=2) elif deStart.weekday() == 6: deStart = deStart + timedelta(days=1) self.deStart.setDate(deStart) self.setEndDateMinimum() def setEndDateMinimum(self): """Updates the minimum value of the end date, which must be 22 business days after the start date. """ deStart = dt.datetime.combine(self.deStart.date().toPyDate(), dt.datetime.min.time()) end_min = self.getWeekdaysOut(deStart, 22) self.deEnd.setMinimumDateTime(end_min) if self.deEnd.date() < end_min: self.deEnd.setDate(end_min) self.test_signal.emit(end_min) def onEndDateChange(self): """Checks if new end date is a weekend. If so, it adds the appropriate number of days to bring the end date to the following Monday. After, it emits a signal containing the new end date to be picked up by a slot in the PredWindowSelect widget. """ deEnd = dt.datetime.combine(self.deEnd.date().toPyDate(), dt.datetime.min.time()) if deEnd.weekday() == 5: deEnd = deEnd + timedelta(days=2) elif deEnd.weekday() == 6: deEnd = deEnd + timedelta(days=1) self.deEnd.setDate(deEnd) self.end_signal.emit(deEnd) def getWeekdaysOut(self, start_date, days_out): """Finds the date that is x weekdays later than the start date (where x = days_out). Args: start_date (datetime.datetime) - Start date from which to count weekdays out. days_out (int) - Number of days out. Returns: curr_date (datetime.datetime) - the date that is days_out weekdays later than start_date. Raises Error: None """ if start_date.weekday() == 5: start_date += timedelta(days=2) if start_date.weekday() == 6: start_date += timedelta(days=1) for i in range(days_out): if i == 0: curr_date = start_date next_date = curr_date + timedelta(days=1) if next_date.weekday() == 5: next_date = curr_date + timedelta(days=3) curr_date = next_date return (curr_date) def getWeekdaysBack(self, start_date, days_back): """Finds the date that is x weekdays earlier than the start date (where x = days_back). Args: start_date (datetime.datetime) - Start date from which to count weekdays out. days_back (int) - Number of days back. Returns: curr_date (datetime.datetime) - the date that is days_back weekdays earlier than start_date. Raises Error: None """ if start_date.weekday() == 5: start_date -= timedelta(days=1) if start_date.weekday() == 6: start_date -= timedelta(days=2) for i in range(days_back): if i == 0: curr_date = start_date next_date = curr_date - timedelta(days=1) if next_date.weekday() == 6: next_date = curr_date - timedelta(days=3) curr_date = next_date return (curr_date) def getDateWindow(self): """Getter function for the date window selected by the user. """ start_date = dt.datetime.combine(self.deStart.date().toPyDate(), dt.datetime.min.time()) end_date = dt.datetime.combine(self.deEnd.date().toPyDate(), dt.datetime.min.time()) return start_date, end_date
class EgimDownloaderFrame(QFrame): """Frame to download data from EMSODEV servers""" # Signals msg2Statusbar = pyqtSignal(str) wf2plotSplitter = pyqtSignal(WaterFrame) class DownloadParameterThread(QThread): """ The process to download data from the API is very slow. We are going to use this thread to download data without block the app. """ def __init__(self, downloader): QThread.__init__(self) self.downloader = downloader def __del__(self): self.wait() def run(self): if self.downloader.instrumentList.currentItem().text() \ == "icListen-1636": date = datetime.datetime.strptime( self.downloader.dateList.currentItem().text(), "%Y-%m-%d").strftime("%d/%m/%Y") self.downloader.downloadAcoustic( date, self.downloader.hourMinuteList.currentItem().text()) else: parameters = [item.text() for item in self.downloader.parameterList.selectedItems()] for parameter in parameters: self.downloader.downloadParameter(parameter) def __init__(self): super().__init__() # Instance variables self.downloader = EGIM() self.wf = WaterFrame() self.metadata = dict() self.dates = [] self.myThread = None # Save the login of the EMSODEV API self.downloader.login = "******" self.downloader.password = "" self.initUI() def initUI(self): # Buttons downloadButton = QPushButton("Download", self) downloadButton.clicked.connect(self.downloadClick) downloadButton.setEnabled(False) closeButton = QPushButton("Close", self) closeButton.clicked.connect(self.hide) # Lists self.egimList = QListWidget(self) self.egimList.itemClicked.connect(self.loadInstruments) self.egimList.setMaximumWidth(200) self.instrumentList = QListWidget(self) self.instrumentList.itemClicked.connect(self.loadParameters) self.instrumentList.setMaximumWidth(290) self.metadataList = QListWidget(self) self.parameterList = QListWidget(self) self.parameterList.setSelectionMode( QAbstractItemView.ExtendedSelection) self.parameterList.itemClicked.connect( lambda: downloadButton.setEnabled(True)) self.dateList = QListWidget(self) self.dateList.itemClicked.connect(self.loadTimes) self.dateList.setMaximumWidth(150) self.hourMinuteList = QListWidget(self) self.hourMinuteList.itemClicked.connect( lambda: downloadButton.setEnabled(True)) self.hourMinuteList.setMaximumWidth(150) # Labels egimLabel = QLabel("EGIM", self) instrumentLabel = QLabel("Instrument", self) metadataLabel = QLabel("Metadata", self) parameterLabel = QLabel("Parameter", self) startDateLabel = QLabel("Start date", self) endDateLabel = QLabel("End date", self) limitLabel = QLabel("Get last X values", self) hourLabel = QLabel("Hour and minute (HHMM)", self) dateLabel = QLabel("Available dates", self) # Date edit self.startDateEdit = QDateEdit(self) self.startDateEdit.setCalendarPopup(True) self.startDateEdit.setDateTime(QDateTime(QDate(2017, 1, 27), QTime(0, 0, 0))) self.startDateEdit.setMinimumDateTime(QDateTime(QDate(2017, 1, 27), QTime(0, 0, 0))) self.endDateEdit = QDateEdit(self) self.endDateEdit.setCalendarPopup(True) self.endDateEdit.setDateTime(QDateTime(QDate(2017, 1, 27), QTime(0, 0, 0))) self.endDateEdit.setMinimumDateTime(QDateTime(QDate(2017, 1, 27), QTime(0, 0, 0))) # Spin box self.limitSpinBox = QSpinBox(self) self.limitSpinBox.setMinimum(0) self.limitSpinBox.setMaximum(9999999999) self.limitSpinBox.setSingleStep(100) self.limitSpinBox.valueChanged.connect(self.enableDate) # Custom Widgets # Widget for dates of the acoustic data self.acousticDateWidget = QWidget(self) # - Layout vAcousticDate = QVBoxLayout() vAcousticDate.addWidget(dateLabel) vAcousticDate.addWidget(self.dateList) vAcousticDate.addWidget(hourLabel) vAcousticDate.addWidget(self.hourMinuteList) self.acousticDateWidget.setLayout(vAcousticDate) self.acousticDateWidget.setMaximumWidth(175) self.acousticDateWidget.setEnabled(False) # Widget for dates of parameters self.parameterDateWidget = QWidget(self) # - Layout vparameterDate = QVBoxLayout() vparameterDate.addWidget(startDateLabel) vparameterDate.addWidget(self.startDateEdit) vparameterDate.addWidget(endDateLabel) vparameterDate.addWidget(self.endDateEdit) vparameterDate.addWidget(limitLabel) vparameterDate.addWidget(self.limitSpinBox) vparameterDate.addStretch() self.parameterDateWidget.setLayout(vparameterDate) self.parameterDateWidget.setEnabled(False) # Layout # - Vertical layout for EGIM -- vEgim = QVBoxLayout() vEgim.addWidget(egimLabel) vEgim.addWidget(self.egimList) # -- Vertical layout for instruments - vInstrument = QVBoxLayout() vInstrument.addWidget(instrumentLabel) vInstrument.addWidget(self.instrumentList) # - Vertical layout for parameters - vParameter = QVBoxLayout() vParameter.addWidget(metadataLabel) vParameter.addWidget(self.metadataList) vParameter.addWidget(parameterLabel) vParameter.addWidget(self.parameterList) # - Vertical layout for dates and buttons vButton = QVBoxLayout() vButton.addWidget(downloadButton) vButton.addWidget(closeButton) vButton.addStretch() # - Layout of the frame - hFrame = QHBoxLayout() hFrame.addLayout(vEgim) hFrame.addLayout(vInstrument) hFrame.addLayout(vParameter) hFrame.addWidget(self.parameterDateWidget) hFrame.addWidget(self.acousticDateWidget) hFrame.addLayout(vButton) self.setLayout(hFrame) def loadObservatories(self): """ It asks for the available EGIM observatories and write its names into self.egimList """ # Send a message for the statusbar self.msg2Statusbar.emit("Loading observatories") # Clear self.egimList self.egimList.clear() # Ask for the observatories code, observatoryList = self.downloader.observatories() if code: if code == 200: # It means that you are going good self.egimList.addItems(observatoryList) # Send a message for the statusbar self.msg2Statusbar.emit("Ready") elif code == 401: self.msg2Statusbar.emit( "Unauthorized to use the EMSODEV DMP API") self.downloader.password = None self.reload() elif code == 404: self.msg2Statusbar.emit("Not Found") elif code == 403: self.msg2Statusbar.emit("Forbidden") elif code == 500: self.msg2Statusbar.emit("EMSODEV API internal error") else: self.msg2Statusbar.emit("Unknown EMSODEV DMP API error") else: self.msg2Statusbar.emit( "Impossible to connect to the EMSODEV DMP API") def loadInstruments(self, observatory): """ It asks for the available instruments and write its names into self.instrumentList Parameters ---------- observatory: item item from self.observatoryList """ # Send a message for the statusbar self.msg2Statusbar.emit("Loading instruments") # Clear self.instrumentList self.instrumentList.clear() # Ask for instruments code, instrumentList_ = self.downloader.instruments(observatory.text()) if code: if code == 200: # It means that you are going good # Obtain all sensor names of instrumentList_ sensorType = [ instrument['name'] for instrument in instrumentList_] self.instrumentList.addItems(sensorType) # Add tooltip for i in range(self.instrumentList.count()): self.instrumentList.item(i).setToolTip( '<p><b>Sensor Type</b><br>' + '{}</p><p>'.format(instrumentList_[i]['sensorType']) + '<b>Long Name</b><br>' + '{}</p>'.format(instrumentList_[i]['sensorLongName']) + '<p></p><p><b>S/N</b><br>' + '{}</p>'.format(instrumentList_[i]['sn'])) # Send a message for the statusbar self.msg2Statusbar.emit("Ready") elif code == 401: self.msg2Statusbar.emit( "Unauthorized to use the EMSODEV DMP API") self.downloader.password = None self.reload() elif code == 404: self.msg2Statusbar.emit("Not Found") elif code == 403: self.msg2Statusbar.emit("Forbidden") elif code == 500: self.msg2Statusbar.emit("EMSODEV API internal error") else: self.msg2Statusbar.emit("Unknown EMSODEV DMP API error") else: self.msg2Statusbar.emit( "Impossible to connect to the EMSODEV DMP API") def loadParameters(self, instrument): """ It asks for the available parameters and metadata and write them into self.parameterList and self.metadataList """ # Send a message for the statusbar self.msg2Statusbar.emit("Loading parameters") # Clear self.parameterList and self.metadataList self.parameterList.clear() self.metadataList.clear() self.parameterDateWidget.setEnabled(False) self.acousticDateWidget.setEnabled(False) # If instrument is an icListener, check times if instrument.text() == "icListen-1636": self.acousticDateWidget.setEnabled(True) # Ask for dates code, self.dates = self.downloader.acoustic_date( self.egimList.currentItem().text(), instrument.text()) if code == 200: dateList = [ date['acousticObservationDate'] for date in self.dates] self.dateList.addItems(dateList) else: self.msg2Statusbar.emit( "Impossible to connect to the EMSODEV DMP API") return return self.parameterDateWidget.setEnabled(True) # Ask for metadata code, self.metadata = self.downloader.metadata( self.egimList.currentItem().text(), instrument.text()) if code == 200: items = [] for key, value in self.metadata.items(): items.append("{}: {}".format(key, value)) self.metadataList.addItems(items) else: self.msg2Statusbar.emit( "Impossible to connect to the EMSODEV DMP API") return # Ask for parameters code, parameterList_ = self.downloader.parameters( self.egimList.currentItem().text(), instrument.text()) if code: if code == 200: # It means that you are going good # Obtain all parameter names of parameterList_ names = [parameter['name'] for parameter in parameterList_] self.parameterList.addItems(names) self.parameterList.sortItems() # Add tooltip for i in range(self.parameterList.count()): self.parameterList.item(i).setToolTip( '<b>Units:</b> {}'.format(parameterList_[i]['uom'])) # Send a message for the statusbar self.msg2Statusbar.emit("Ready") elif code == 401: self.msg2Statusbar.emit( "Unauthorized to use the EMSODEV DMP API") self.downloader.password = None self.reload() elif code == 404: self.msg2Statusbar.emit("Not Found") elif code == 403: self.msg2Statusbar.emit("Forbidden") elif code == 500: self.msg2Statusbar.emit("EMSODEV API internal error") else: self.msg2Statusbar.emit("Unknown EMSODEV DMP API error") else: self.msg2Statusbar.emit( "Impossible to connect to the EMSODEV DMP API") def loadTimes(self, date_item): """ Write items into self.hourMinuteList QListWidget """ for date in self.dates: if date['acousticObservationDate'] == date_item.text(): timeList = [] for time in date['observationsHourMinuteList']: timeList.append(time['acousticObservationHourMinute']) self.hourMinuteList.addItems(timeList) def reload(self): """It clear all lists and load again the observatories.""" # Check the password of the API if self.downloader.password is None: self.msg2Statusbar.emit( "Password is required to download data from EMSODEV") text, ok = QInputDialog.getText(self, "Attention", "Password", QLineEdit.Password) if ok: self.downloader.password = text else: return self.loadObservatories() def downloadClick(self): """Function when user click download""" self.myThread = self.DownloadParameterThread(self) self.myThread.start() def downloadParameter(self, parameter): """It download data with the observation function of EGIM""" # Send a message for the statusbar self.msg2Statusbar.emit("Downloading {}".format(parameter)) code, df = self.downloader.observation( observatory=self.egimList.currentItem().text(), instrument=self.instrumentList.currentItem().text(), parameter=parameter, startDate=self.startDateEdit.text(), endDate=self.endDateEdit.text(), limit=self.limitSpinBox.text()) if code: if code == 200: self.msg2Statusbar.emit("Waterframe creation") # It means that you are going good wf = self.downloader.to_waterframe(data=df, metadata=self.metadata) # print(wf.data.head()) # Send a signal with the new WaterFrame self.wf2plotSplitter.emit(wf) self.msg2Statusbar.emit("Ready") elif code == 401: self.msg2Statusbar.emit( "Unauthorized to use the EMSODEV DMP API") self.downloader.password = None self.reload() elif code == 404: self.msg2Statusbar.emit("Not Found") elif code == 403: self.msg2Statusbar.emit("Forbidden") elif code == 500: self.msg2Statusbar.emit("EMSODEV API internal error") else: self.msg2Statusbar.emit("Unknown EMSODEV DMP API error") else: self.msg2Statusbar.emit( "Impossible to connect to the EMSODEV DMP API") def downloadAcoustic(self, date, time): # Send a message for the statusbar self.msg2Statusbar.emit( "Downloading acoustic file from {}, {}".format(date, time)) code, df, metadata = self.downloader.acoustic_observation( observatory=self.egimList.currentItem().text(), instrument=self.instrumentList.currentItem().text(), date=date, hour_minute=time) if code: if code == 200: self.msg2Statusbar.emit("Waterframe creation") # It means that you are going good wf = self.downloader.to_waterframe(data=df, metadata=metadata) # Send a signal with the new WaterFrame self.wf2plotSplitter.emit(wf) self.msg2Statusbar.emit("Ready") elif code == 401: self.msg2Statusbar.emit( "Unauthorized to use the EMSODEV DMP API") self.downloader.password = None self.reload() elif code == 404: self.msg2Statusbar.emit("Not Found") elif code == 403: self.msg2Statusbar.emit("Forbidden") elif code == 500: self.msg2Statusbar.emit("EMSODEV API internal error") else: self.msg2Statusbar.emit("Unknown EMSODEV DMP API error") else: self.msg2Statusbar.emit( "Impossible to connect to the EMSODEV DMP API") def enableDate(self): """Enable or disable date elements""" if int(self.limitSpinBox.text()) > 0: self.startDateEdit.setEnabled(False) self.endDateEdit.setEnabled(False) else: self.startDateEdit.setEnabled(True) self.endDateEdit.setEnabled(True)
class PredWindowSelect(QGroupBox): """An extension of the QGroupBox widget. It allows the user to select a start date and end date for the prediction window in the Monte Carlo VaR calculation application. The start date of the prediction window is set to the end date of the historical window. The end date of the prediction window is five days later than the start date of the prediction window as a minimum. These restrictions are implemented through the QDateEdit widget's maximum and minimum date functions. """ def __init__(self, parent=None): super(PredWindowSelect, self).__init__(parent) self.setTitle('Prediction Window Select') grid = QGridLayout() lbl_start = QLabel('Start Date: ') lbl_end = QLabel('End Date: ') self.deStart = QDateEdit() self.deStart.setEnabled(False) self.deStart.setCalendarPopup(True) self.deStart.dateChanged.connect(self.setEndDateMinimum) self.deEnd = QDateEdit() self.deEnd.setCalendarPopup(True) self.deEnd.dateChanged.connect(self.onEndDateChange) self.setEndDateMinimum() lbl_disc = QLabel( 'Note: A minimum prediction window length <br>of 5 business days has been imposed.' ) grid.addWidget(lbl_start, 0, 0) grid.addWidget(lbl_end, 1, 0) grid.addWidget(self.deStart, 0, 1) grid.addWidget(self.deEnd, 1, 1) grid.addWidget(lbl_disc, 2, 1) self.setLayout(grid) @QtCore.pyqtSlot(dt.datetime) def setStart(self, date): """Slotted function to set the start date based on end date of the historical window. """ self.deStart.setDate(date) def onEndDateChange(self): """Checks if new end date is a weekend. If so, it adds the appropriate number of days to bring the end date to the following Monday. """ deEnd = dt.datetime.combine(self.deEnd.date().toPyDate(), dt.datetime.min.time()) if deEnd.weekday() == 5: deEnd = deEnd + timedelta(days=2) elif deEnd.weekday() == 6: deEnd = deEnd + timedelta(days=1) self.deEnd.setDate(deEnd) def setEndDateMinimum(self): """Updates the minimum value of the end date, which must be 5 business days after the start date. """ deStart = dt.datetime.combine(self.deStart.date().toPyDate(), dt.datetime.min.time()) end_min = self.getWeekdaysOut(deStart, 5) self.deEnd.setMinimumDateTime(end_min) if self.deEnd.date() < end_min: self.deEnd.setDate(end_min) def getWeekdaysOut(self, start_date, days_out): """Finds the date that is x weekdays later than the start date (where x = days_out). Args: start_date (datetime.datetime) - Start date from which to count weekdays out. days_out (int) - Number of days out. Returns: curr_date (datetime.datetime) - the date that is days_out weekdays later than start_date. Raises Error: None """ if start_date.weekday() == 5: start_date += timedelta(days=2) if start_date.weekday() == 6: start_date += timedelta(days=1) for i in range(days_out): if i == 0: curr_date = start_date next_date = curr_date + timedelta(days=1) if next_date.weekday() == 5: next_date = curr_date + timedelta(days=3) curr_date = next_date return (curr_date) def getDateWindow(self): """Getter function for the date window selected by the user. """ start_date = dt.datetime.combine(self.deStart.date().toPyDate(), dt.datetime.min.time()) end_date = dt.datetime.combine(self.deEnd.date().toPyDate(), dt.datetime.min.time()) return start_date, end_date def getNumDays(self): """Getter function for the number of weekdays between the start and end dates.""" start_date = dt.datetime.combine(self.deStart.date().toPyDate(), dt.datetime.min.time()) end_date = dt.datetime.combine(self.deEnd.date().toPyDate(), dt.datetime.min.time()) if start_date.weekday() == 5: start_date += timedelta(days=2) if start_date.weekday() == 6: start_date += timedelta(days=1) days = 0 curr_date = start_date while curr_date != end_date: curr_date += timedelta(days=1) if curr_date.weekday() == 5: continue elif curr_date.weekday() == 6: continue else: days += 1 return days