source: orange/Orange/OrangeWidgets/OWDatabasesUpdate.py @ 11483:b82955e67844

Revision 11483:b82955e67844, 20.7 KB checked in by Ales Erjavec <ales.erjavec@…>, 12 months ago (diff)

Use new OWConcurrent classes, added a 'Cancel' button.

RevLine 
[11436]1from __future__ import with_statement
2
3import os
4import sys
5
6from datetime import datetime
[11483]7from functools import partial
[11436]8
[11483]9from PyQt4.QtCore import pyqtSignal as Signal, pyqtSlot as Slot
[11436]10
[11437]11from Orange.utils import serverfiles, environ
[11436]12from Orange.utils.serverfiles import sizeformat as sizeof_fmt
13
[8042]14from OWWidget import *
[11483]15
16from OWConcurrent import Task, ThreadExecutor, methodinvoke
[8042]17
[11436]18import OWGUIEx
[8042]19
20
21class ItemProgressBar(QProgressBar):
[11437]22    """Progress Bar with and `advance()` slot.
[11436]23    """
[8042]24    @pyqtSignature("advance()")
25    def advance(self):
26        self.setValue(self.value() + 1)
[11436]27
28
[11437]29_icons_dir = os.path.join(environ.canvas_install_dir, "icons")
[11436]30
31
32def icon(name):
33    return QIcon(os.path.join(_icons_dir, name))
34
35
[8042]36class UpdateOptionsWidget(QWidget):
[11437]37    """
38    A Widget with download/update/remove options.
39    """
[8042]40    def __init__(self, updateCallback, removeCallback, state, *args):
41        QWidget.__init__(self, *args)
42        self.updateCallback = updateCallback
43        self.removeCallback = removeCallback
44        layout = QHBoxLayout()
45        layout.setSpacing(1)
46        layout.setContentsMargins(1, 1, 1, 1)
47        self.updateButton = QToolButton(self)
[11436]48        self.updateButton.setIcon(icon("update.png"))
[8042]49        self.updateButton.setToolTip("Download")
[11436]50
[8042]51        self.removeButton = QToolButton(self)
[11436]52        self.removeButton.setIcon(icon("delete.png"))
[8042]53        self.removeButton.setToolTip("Remove from system")
[11436]54
55        self.connect(self.updateButton, SIGNAL("released()"),
56                     self.updateCallback)
57        self.connect(self.removeButton, SIGNAL("released()"),
58                     self.removeCallback)
59
[8042]60        self.setMaximumHeight(30)
61        layout.addWidget(self.updateButton)
62        layout.addWidget(self.removeButton)
63        self.setLayout(layout)
64        self.SetState(state)
65
66    def SetState(self, state):
67        self.state = state
68        if state == 0:
[11436]69            self.updateButton.setIcon(icon("update1.png"))
[8042]70            self.updateButton.setToolTip("Update")
71            self.updateButton.setEnabled(False)
72            self.removeButton.setEnabled(True)
73        elif state == 1:
[11436]74            self.updateButton.setIcon(icon("update1.png"))
[8042]75            self.updateButton.setToolTip("Update")
76            self.updateButton.setEnabled(True)
77            self.removeButton.setEnabled(True)
78        elif state == 2:
[11436]79            self.updateButton.setIcon(icon("update.png"))
[8042]80            self.updateButton.setToolTip("Download")
81            self.updateButton.setEnabled(True)
82            self.removeButton.setEnabled(False)
83        elif state == 3:
[11436]84            self.updateButton.setIcon(icon("update.png"))
[8042]85            self.updateButton.setToolTip("")
86            self.updateButton.setEnabled(False)
87            self.removeButton.setEnabled(True)
[11437]88        else:
89            raise ValueError("Invalid state %r" % state)
[8042]90
91
92class UpdateTreeWidgetItem(QTreeWidgetItem):
[11436]93    stateDict = {0: "up-to-date",
94                 1: "new version available",
95                 2: "not downloaded",
96                 3: "obsolete"}
97
98    def __init__(self, master, treeWidget, domain, filename, infoLocal,
99                 infoServer, *args):
[11437]100        dateServer = dateLocal = None
101        if infoServer:
102            dateServer = datetime.strptime(
103                infoServer["datetime"].split(".")[0], "%Y-%m-%d %H:%M:%S"
104            )
105        if infoLocal:
106            dateLocal = datetime.strptime(
107                infoLocal["datetime"].split(".")[0], "%Y-%m-%d %H:%M:%S"
108            )
[8042]109        if not infoLocal:
110            self.state = 2
111        elif not infoServer:
112            self.state = 3
113        else:
114            self.state = 0 if dateLocal >= dateServer else 1
[11436]115
[8042]116        title = infoServer["title"] if infoServer else (infoLocal["title"])
117        tags = infoServer["tags"] if infoServer else infoLocal["tags"]
[11436]118        specialTags = dict([tuple(tag.split(":"))
119                            for tag in tags
120                            if tag.startswith("#") and ":" in tag])
[8042]121        tags = ", ".join(tag for tag in tags if not tag.startswith("#"))
122        self.size = infoServer["size"] if infoServer else infoLocal["size"]
[11436]123
[8042]124        size = sizeof_fmt(float(self.size))
[11436]125        state = self.stateDict[self.state]
126        if self.state == 1:
127            state += dateServer.strftime(" (%Y, %b, %d)")
128
[8042]129        QTreeWidgetItem.__init__(self, treeWidget, ["", title, size])
[11437]130        if dateServer is not None:
131            self.setData(3, Qt.DisplayRole,
132                         dateServer.date().isoformat())
133
[11436]134        self.updateWidget = UpdateOptionsWidget(
135            self.StartDownload, self.Remove, self.state, treeWidget
136        )
137
[8042]138        self.treeWidget().setItemWidget(self, 0, self.updateWidget)
139        self.updateWidget.show()
140        self.master = master
141        self.title = title
142        self.tags = tags.split(", ")
143        self.specialTags = specialTags
144        self.domain = domain
145        self.filename = filename
[11483]146        self.task = None
[8042]147        self.UpdateToolTip()
148
149    def UpdateToolTip(self):
[11473]150        state = {0: "downloaded, current",
151                 1: "downloaded, needs update",
152                 2: "not downloaded",
[11436]153                 3: "obsolete"}
154        tooltip = "State: %s\nTags: %s" % (state[self.state],
155                                           ", ".join(self.tags))
[11473]156
[8042]157        if self.state != 2:
[11437]158            tooltip += ("\nFile: %s" %
159                        serverfiles.localpath(self.domain, self.filename))
[8042]160        for i in range(1, 5):
161            self.setToolTip(i, tooltip)
[11436]162
[8042]163    def StartDownload(self):
164        self.updateWidget.removeButton.setEnabled(False)
165        self.updateWidget.updateButton.setEnabled(False)
[11483]166        self.master.SubmitDownloadTask(self.domain, self.filename)
[11436]167
[8042]168    def Remove(self):
[11483]169        self.master.SubmitRemoveTask(self.domain, self.filename)
[8042]170
171    def __contains__(self, item):
[11436]172        return any(item.lower() in tag.lower()
173                   for tag in self.tags + [self.title])
174
[9305]175    def __lt__(self, other):
[11436]176        return getattr(self, "title", "") < getattr(other, "title", "")
177
[8042]178
179class UpdateItemDelegate(QItemDelegate):
180    def sizeHint(self, option, index):
181        size = QItemDelegate.sizeHint(self, option, index)
182        parent = self.parent()
183        item = parent.itemFromIndex(index)
184        widget = parent.itemWidget(item, 0)
185        if widget:
[11436]186            size = QSize(size.width(), widget.sizeHint().height() / 2)
[8042]187        return size
[11436]188
189
[8042]190def retrieveFilesList(serverFiles, domains=None, advance=lambda: None):
[11436]191    """
192    Retrieve and return serverfiles.allinfo for all domains.
193    """
[8042]194    domains = serverFiles.listdomains() if domains is None else domains
195    advance()
196    serverInfo = dict([(dom, serverFiles.allinfo(dom)) for dom in domains])
197    advance()
198    return serverInfo
[11436]199
200
[8042]201class OWDatabasesUpdate(OWWidget):
[11436]202    def __init__(self, parent=None, signalManager=None,
203                 name="Databases update", wantCloseButton=False,
204                 searchString="", showAll=True, domains=None,
205                 accessCode=""):
[8042]206        OWWidget.__init__(self, parent, signalManager, name)
207        self.searchString = searchString
208        self.accessCode = accessCode
209        self.showAll = showAll
210        self.domains = domains
[11437]211        self.serverFiles = serverfiles.ServerFiles()
[8042]212        box = OWGUI.widgetBox(self.mainArea, orientation="horizontal")
[11436]213
214        self.lineEditFilter = \
215            OWGUIEx.lineEditHint(box, self, "searchString", "Filter",
216                                 caseSensitive=False,
217                                 delimiters=" ",
218                                 matchAnywhere=True,
219                                 listUpdateCallback=self.SearchUpdate,
220                                 callbackOnType=True,
221                                 callback=self.SearchUpdate)
[8042]222
223        box = OWGUI.widgetBox(self.mainArea, "Files")
224        self.filesView = QTreeWidget(self)
[11437]225        self.filesView.setHeaderLabels(["Options", "Title", "Size",
226                                        "Last Updated"])
[8042]227        self.filesView.setRootIsDecorated(False)
228        self.filesView.setSelectionMode(QAbstractItemView.NoSelection)
229        self.filesView.setSortingEnabled(True)
230        self.filesView.setItemDelegate(UpdateItemDelegate(self.filesView))
[11436]231        self.connect(self.filesView.model(),
232                     SIGNAL("layoutChanged()"),
233                     self.SearchUpdate)
[8042]234        box.layout().addWidget(self.filesView)
235
236        box = OWGUI.widgetBox(self.mainArea, orientation="horizontal")
[11436]237        OWGUI.button(box, self, "Update all local files",
238                     callback=self.UpdateAll,
239                     tooltip="Update all updatable files")
240        OWGUI.button(box, self, "Download filtered",
241                     callback=self.DownloadFiltered,
242                     tooltip="Download all filtered files shown")
[11483]243        OWGUI.button(box, self, "Cancel", callback=self.Cancel,
244                     tooltip="Cancel scheduled downloads/updates.")
[8042]245        OWGUI.rubber(box)
[11436]246        OWGUI.lineEdit(box, self, "accessCode", "Access Code",
247                       orientation="horizontal",
248                       callback=self.RetrieveFilesList)
249        self.retryButton = OWGUI.button(box, self, "Retry",
250                                        callback=self.RetrieveFilesList)
[8042]251        self.retryButton.hide()
252        box = OWGUI.widgetBox(self.mainArea, orientation="horizontal")
253        OWGUI.rubber(box)
254        if wantCloseButton:
[11436]255            OWGUI.button(box, self, "Close",
256                         callback=self.accept,
257                         tooltip="Close")
[8042]258
259        self.infoLabel = QLabel()
260        self.infoLabel.setAlignment(Qt.AlignCenter)
[11436]261
[8042]262        self.mainArea.layout().addWidget(self.infoLabel)
263        self.infoLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
264
265        self.updateItems = []
266        self.allTags = []
[11436]267
[8042]268        self.resize(800, 600)
[11436]269
[11483]270        self.progress = ProgressState(self, maximum=3)
271        self.progress.valueChanged.connect(self._updateProgress)
272        self.progress.rangeChanged.connect(self._updateProgress)
273        self.executor = ThreadExecutor(
274            threadPool=QThreadPool(maxThreadCount=2)
275        )
276
277        task = Task(self, function=self.RetrieveFilesList)
278        task.exceptionReady.connect(self.HandleError)
279        task.start()
280
281        self._tasks = []
[11436]282
[8042]283    def RetrieveFilesList(self):
[11483]284        self.progress.setRange(0, 3)
[11437]285        self.serverFiles = serverfiles.ServerFiles(access_code=self.accessCode)
[11483]286
287        task = Task(function=partial(retrieveFilesList, self.serverFiles,
288                                     self.domains,
289                                     methodinvoke(self.progress, "advance")))
290
291        task.resultReady.connect(self.SetFilesList)
292        task.exceptionReady.connect(self.HandleError)
293
294        self.executor.submit(task)
[11436]295
[8042]296        self.setEnabled(False)
[11436]297
[8042]298    def SetFilesList(self, serverInfo):
299        self.setEnabled(True)
[11437]300        domains = serverInfo.keys() or serverfiles.listdomains()
301        localInfo = dict([(dom, serverfiles.allinfo(dom))
[11436]302                          for dom in domains])
[8042]303        items = []
[11436]304
[8042]305        self.allTags = set()
306        allTitles = set()
307        self.updateItems = []
[11436]308
309        for domain in set(domains) - set(["test", "demo"]):
310            local = localInfo.get(domain, {})
311            server = serverInfo.get(domain, {})
[8042]312            files = sorted(set(server.keys() + local.keys()))
[11436]313            for filename in files:
314                infoServer = server.get(filename, None)
315                infoLocal = local.get(filename, None)
316
317                items.append((self.filesView, domain, filename, infoLocal,
318                              infoServer))
319
[8042]320                displayInfo = infoServer if infoServer else infoLocal
321                self.allTags.update(displayInfo["tags"])
322                allTitles.update(displayInfo["title"].split())
[11436]323
324        for item in items:
[8042]325            self.updateItems.append(UpdateTreeWidgetItem(self, *item))
[11483]326        self.progress.advance()
327
[11437]328        for column in range(4):
329            whint = self.filesView.sizeHintForColumn(column)
330            width = min(whint, 400)
331            self.filesView.setColumnWidth(column, width)
332
[11436]333        self.lineEditFilter.setItems([hint for hint in sorted(self.allTags)
334                                      if not hint.startswith("#")])
[8042]335        self.SearchUpdate()
336        self.UpdateInfoLabel()
[11436]337
[11483]338        self.progress.setRange(0, 0)
339
340    def HandleError(self, exception):
341        if isinstance(exception, IOError):
[11436]342            self.error(0,
343                       "Could not connect to server! Press the Retry "
344                       "button to try again.")
[8042]345            self.SetFilesList({})
346        else:
[11483]347            sys.excepthook(type(exception), exception.args, None)
348            self.progress.setRange(0, 0)
[8042]349            self.setEnabled(True)
350
351    def UpdateInfoLabel(self):
352        local = [item for item in self.updateItems if item.state != 2]
353        onServer = [item for item in self.updateItems]
[11436]354        size = sum(float(item.specialTags.get("#uncompressed", item.size))
355                   for item in local)
356        sizeOnServer = sum(float(item.size) for item in self.updateItems)
357
[8042]358        if self.showAll:
[11436]359
360            text = ("%i items, %s (data on server: %i items, %s)" %
361                    (len(local),
362                     sizeof_fmt(size),
363                     len(onServer),
364                     sizeof_fmt(sizeOnServer)))
[8042]365        else:
[11436]366            text = "%i items, %s" % (len(local), sizeof_fmt(size))
367
368        self.infoLabel.setText(text)
369
[8042]370    def UpdateAll(self):
371        for item in self.updateItems:
372            if item.state == 1:
373                item.StartDownload()
[11436]374
[8042]375    def DownloadFiltered(self):
376        for item in self.updateItems:
377            if not item.isHidden() and item.state != 0:
378                item.StartDownload()
379
380    def SearchUpdate(self, searchString=None):
[11436]381        strings = unicode(self.lineEditFilter.text()).split()
[8042]382        tags = set()
383        for item in self.updateItems:
384            hide = not all(str(string) in item for string in strings)
385            item.setHidden(hide)
386            if not hide:
387                tags.update(item.tags)
[11436]388
[11483]389    def SubmitDownloadTask(self, domain, filename):
390        """
391        Submit the (domain, filename) to be downloaded/updated.
392        """
393        item = self._item(domain, filename)
394
395        if self.accessCode:
396            sf = serverfiles.ServerFiles(access_code=self.accessCode)
397        else:
398            sf = serverfiles.ServerFiles()
399
400        task = DownloadTask(domain, filename, sf)
401
402        future = self.executor.submit(task)
403
404#        watcher = FutureWatcher(future, parent=self)
405#        watcher.finished.connect(progress.finish)
406
407        self.progress.adjustRange(0, 100)
408
409        pb = ItemProgressBar(self.filesView)
410        pb.setRange(0, 100)
411        pb.setTextVisible(False)
412
413        task.advanced.connect(pb.advance)
414        task.advanced.connect(self.progress.advance)
415        task.finished.connect(pb.hide)
416        task.finished.connect(self.onDownloadFinished, Qt.QueuedConnection)
417        task.exception.connect(self.onDownloadError, Qt.QueuedConnection)
418
419        self.filesView.setItemWidget(item, 2, pb)
420
421        # Clear the text so it does not show behind the progress bar.
422        item.setData(2, Qt.DisplayRole, QVariant(""))
423        pb.show()
424
425        self._tasks.append(task)
426#        self._futures.append((future, watcher))
427
428    def EndDownloadTask(self, task):
429        future = task.future()
430        item = self._item(task.domain, task.filename)
431
432        self.filesView.removeItemWidget(item, 2)
433
434        if future.cancelled():
435            # Restore the previous state
436            item.updateWidget.SetState(item.state)
437            item.setData(2, Qt.DisplayRole,
438                         QVariant(sizeof_fmt(float(item.size))))
439
440        elif future.exception():
441            item.updateWidget.SetState(1)
442            item.setData(2, Qt.DisplayRole,
443                         QVariant("Error occurred while downloading:" +
444                                  str(future.exception())))
445#            item.setErrorText(str(exception))
446#            item.setState(UpdateTreeWidgetItem.Error)
447        else:
448            item.state = 0
449            item.updateWidget.SetState(item.state)
450            item.setData(2, Qt.DisplayRole,
451                         QVariant(sizeof_fmt(float(item.size))))
452            item.UpdateToolTip()
453            self.UpdateInfoLabel()
454
455#            item.setState(UpdateTreeWidgetItem.Updated)
456#            item.setInfo(serverfiles.info(task.domain, task.filename))
457
458    def SubmitRemoveTask(self, domain, filename):
459        serverfiles.remove(domain, filename)
460
461        item = self._item(domain, filename)
462        item.state = 2
463        item.updateWidget.SetState(item.state)
464
465        self.UpdateInfoLabel()
466        item.UpdateToolTip()
467
468    def Cancel(self):
469        """
470        Cancel all pending update/download tasks (that have not yet started).
471        """
472        print "Cancel"
473        for task in self._tasks:
474            print task, task.future().cancel()
475
476    def onDeleteWidget(self):
477        self.Cancel()
478        self.executor.shutdown(wait=False)
479        OWBaseWidget.onDeleteWidget(self)
480
481    def onDownloadFinished(self):
482        assert QThread.currentThread() is self.thread()
483        for task in list(self._tasks):
484            future = task.future()
485            if future.done():
486                self.EndDownloadTask(task)
487                self._tasks.remove(task)
488
489        if not self._tasks:
490            # Clear/reset the overall progress
491            self.progress.setRange(0, 0)
492
493        print "Download finished"
494
495    def onDownloadError(self, exc_info):
496        sys.excepthook(*exc_info)
497
498    def _item(self, domain, filename):
499        return [item for item in self.updateItems
500                if item.domain == domain and item.filename == filename].pop()
501
502    def _updateProgress(self, *args):
503        rmin, rmax = self.progress.range()
504        if rmin != rmax:
505            if self.progressBarValue <= 0:
506                self.progressBarInit()
507
508            self.progressBarSet(self.progress.ratioCompleted() * 100,
509                                processEventsFlags=None)
510        if rmin == rmax:
511            self.progressBarFinished()
512
513
514class ProgressState(QObject):
515    valueChanged = Signal(int)
516    rangeChanged = Signal(int, int)
517    textChanged = Signal(str)
518    started = Signal()
519    finished = Signal()
520
521    def __init__(self, parent=None, minimum=0, maximum=0, text="", value=0):
522        QObject.__init__(self, parent)
523
524        self._minimum = minimum
525        self._maximum = max(maximum, minimum)
526        self._text = text
527        self._value = value
528
529    @Slot(int, int)
530    def setRange(self, minimum, maximum):
531        maximum = max(maximum, minimum)
532
533        if self._minimum != minimum or self._maximum != maximum:
534            self._minimum = minimum
535            self._maximum = maximum
536            self.rangeChanged.emit(minimum, maximum)
537
538            # Adjust the value to fit in the range
539            newvalue = min(max(self._value, minimum), maximum)
540            if newvalue != self._value:
541                self.setValue(newvalue)
542
543    def range(self):
544        return self._minimum, self._maximum
545
546    @Slot(int)
547    def setValue(self, value):
548        if self._value != value and value >= self._minimum and \
549                value <= self._maximum:
550            self._value = value
551            self.valueChanged.emit(value)
552
553    def value(self):
554        return self._value
555
556    @Slot(str)
557    def setText(self, text):
558        if self._text != text:
559            self._text = text
560            self.textChanged.emit(text)
561
562    def text(self):
563        return self._text
564
565    @Slot()
566    @Slot(int)
567    def advance(self, value=1):
568        self.setValue(self._value + value)
569
570    def adjustRange(self, dmin, dmax):
571        self.setRange(self._minimum + dmin, self._maximum + dmax)
572
573    def ratioCompleted(self):
574        span = self._maximum - self._minimum
575        if span < 1e-3:
576            return 0.0
577
578        return min(max(float(self._value - self._minimum) / span, 0.0), 1.0)
579
580
581class DownloadTask(Task):
582    advanced = Signal()
583    exception = Signal(tuple)
584
585    def __init__(self, domain, filename, serverfiles, parent=None):
586        Task.__init__(self, parent)
587        self.filename = filename
588        self.domain = domain
589        self.serverfiles = serverfiles
590        self._interrupt = False
591
592    def interrupt(self):
593        """
594        Interrupt the download.
595        """
596        self._interrupt = True
597
598    def _advance(self):
599        self.advanced.emit()
600        if self._interrupt:
601            raise KeyboardInterrupt
602
603    def run(self):
604        print "Download task", QThread.currentThread()
605        try:
606            serverfiles.download(self.domain, self.filename, self.serverfiles,
607                                 callback=self._advance)
608        except Exception:
609            self.exception.emit(sys.exc_info())
610
611        print "Finished"
612
[11436]613
[8042]614if __name__ == "__main__":
615    app = QApplication(sys.argv)
616    w = OWDatabasesUpdate(wantCloseButton=True)
617    w.show()
618    w.exec_()
Note: See TracBrowser for help on using the repository browser.