1from datetime import datetime, timedelta, date
2
3from AnyQt.QtCore import Qt
4from AnyQt.QtWidgets import QApplication, QFormLayout
5
6from Orange.data import StringVariable
7from Orange.widgets.credentials import CredentialManager
8from Orange.widgets.settings import Setting
9from Orange.widgets.widget import OWWidget, Msg, Output
10from orangecontrib.text.corpus import Corpus
11from orangecontrib.text.nyt import NYT, MIN_DATE
12from orangecontrib.text.widgets.utils import CheckListLayout, DatePickerInterval, QueryBox, \
13    gui_require, asynchronous
14
15try:
16    from orangewidget import gui
17except ImportError:
18    from Orange.widgets import gui
19
20
21class OWNYT(OWWidget):
22    class APICredentialsDialog(OWWidget):
23        name = "New York Times API key"
24        want_main_area = False
25        resizing_enabled = False
26        cm_key = CredentialManager('NY Times API Key')
27        key_input = ''
28
29        class Error(OWWidget.Error):
30            invalid_credentials = Msg('This credentials are invalid. '
31                                      'Check the key and your internet connection.')
32
33        def __init__(self, parent):
34            super().__init__()
35            self.parent = parent
36            self.api = None
37
38            form = QFormLayout()
39            form.setContentsMargins(5, 5, 5, 5)
40            self.key_edit = gui.lineEdit(self, self, 'key_input', controlWidth=400)
41            form.addRow('Key:', self.key_edit)
42            self.controlArea.layout().addLayout(form)
43            self.submit_button = gui.button(self.controlArea, self, "OK", self.accept)
44
45            self.load_credentials()
46
47        def load_credentials(self):
48            self.key_edit.setText(self.cm_key.key)
49
50        def save_credentials(self):
51            self.cm_key.key = self.key_input
52
53        def check_credentials(self):
54            api = NYT(self.key_input)
55            if api.api_key_valid():
56                self.save_credentials()
57            else:
58                api = None
59            self.api = api
60
61        def accept(self, silent=False):
62            if not silent: self.Error.invalid_credentials.clear()
63            self.check_credentials()
64            if self.api:
65                self.parent.update_api(self.api)
66                super().accept()
67            elif not silent:
68                self.Error.invalid_credentials()
69
70    name = "NY Times"
71    description = "Fetch articles from the New York Times search API."
72    icon = "icons/NYTimes.svg"
73    priority = 130
74
75    class Outputs:
76        corpus = Output("Corpus", Corpus)
77
78    want_main_area = False
79    resizing_enabled = False
80
81    recent_queries = Setting([])
82    date_from = Setting((datetime.now().date() - timedelta(365)))
83    date_to = Setting(datetime.now().date())
84
85    attributes = [feat.name for feat, _ in NYT.metas if isinstance(feat, StringVariable)]
86    text_includes = Setting([feat.name for feat in NYT.text_features])
87
88    class Warning(OWWidget.Warning):
89        no_text_fields = Msg('Text features are inferred when none are selected.')
90
91    class Error(OWWidget.Error):
92        no_api = Msg('Please provide a valid API key.')
93        no_query = Msg('Please provide a query.')
94        offline = Msg('No internet connection.')
95        api_error = Msg('API error: {}')
96        rate_limit = Msg('Rate limit exceeded. Please try again later.')
97
98    def __init__(self):
99        super().__init__()
100        self.corpus = None
101        self.nyt_api = None
102        self.output_info = ''
103        self.num_retrieved = 0
104        self.num_all = 0
105
106        # API key
107        self.api_dlg = self.APICredentialsDialog(self)
108        self.api_dlg.accept(silent=True)
109        gui.button(self.controlArea, self, 'Article API Key', callback=self.api_dlg.exec_,
110                   focusPolicy=Qt.NoFocus)
111
112        # Query
113        query_box = gui.widgetBox(self.controlArea, 'Query', addSpace=True)
114        self.query_box = QueryBox(query_box, self, self.recent_queries,
115                                  callback=self.new_query_input)
116
117        # Year box
118        date_box = gui.hBox(query_box)
119        DatePickerInterval(date_box, self, 'date_from', 'date_to',
120                           min_date=MIN_DATE, max_date=date.today(),
121                           margin=(0, 3, 0, 0))
122
123        # Text includes features
124        self.controlArea.layout().addWidget(
125            CheckListLayout('Text includes', self, 'text_includes', self.attributes,
126                            cols=2, callback=self.set_text_features))
127
128        # Output
129        info_box = gui.hBox(self.controlArea, 'Output')
130        gui.label(info_box, self, 'Articles: %(output_info)s')
131
132        # Buttons
133        self.button_box = gui.hBox(self.controlArea)
134
135        self.search_button = gui.button(self.button_box, self, 'Search', self.start_stop,
136                                        focusPolicy=Qt.NoFocus)
137
138    def new_query_input(self):
139        self.search.stop()
140        self.run_search()
141
142    def start_stop(self):
143        if self.search.running:
144            self.search.stop()
145        else:
146            self.query_box.synchronize(silent=True)
147            self.run_search()
148
149    @gui_require('nyt_api', 'no_api')
150    @gui_require('recent_queries', 'no_query')
151    def run_search(self):
152        self.search()
153
154    @asynchronous
155    def search(self):
156        return self.nyt_api.search(self.recent_queries[0], self.date_from, self.date_to,
157                                   on_progress=self.progress_with_info,
158                                   should_break=self.search.should_break)
159
160    @search.callback(should_raise=False)
161    def progress_with_info(self, n_retrieved, n_all):
162        self.progressBarSet(100 * (n_retrieved / n_all if n_all else 1))  # prevent division by 0
163        self.num_all = n_all
164        self.num_retrieved = n_retrieved
165        self.update_info_label()
166
167    @search.on_start
168    def on_start(self):
169        self.Error.api_error.clear()
170        self.Error.rate_limit.clear()
171        self.Error.offline.clear()
172        self.num_all, self.num_retrieved = 0, 0
173        self.update_info_label()
174        self.progressBarInit()
175        self.search_button.setText('Stop')
176        self.Outputs.corpus.send(None)
177
178    @search.on_result
179    def on_result(self, result):
180        self.search_button.setText('Search')
181        self.corpus = result
182        self.set_text_features()
183        self.progressBarFinished()
184
185    def update_info_label(self):
186        self.output_info = '{}/{}'.format(self.num_retrieved, self.num_all)
187
188    def set_text_features(self):
189        self.Warning.no_text_fields.clear()
190        if not self.text_includes:
191            self.Warning.no_text_fields()
192
193        if self.corpus is not None:
194            vars_ = [var for var in self.corpus.domain.metas if var.name in self.text_includes]
195            self.corpus.set_text_features(vars_ or None)
196            self.Outputs.corpus.send(self.corpus)
197
198    def update_api(self, api):
199        self.nyt_api = api
200        self.Error.no_api.clear()
201        self.nyt_api.on_error = self.Error.api_error
202        self.nyt_api.on_rate_limit = self.Error.rate_limit
203        self.nyt_api.on_no_connection = self.Error.offline
204
205    def send_report(self):
206        self.report_items([
207            ('Query', self.recent_queries[0] if self.recent_queries else ''),
208            ('Date from', self.date_from),
209            ('Date to', self.date_to),
210            ('Text includes', ', '.join(self.text_includes)),
211            ('Output', self.output_info or 'Nothing'),
212        ])
213
214
215if __name__ == '__main__':
216    app = QApplication([])
217    widget = OWNYT()
218    widget.show()
219    app.exec()
220