eric6/Helpviewer/OpenSearch/OpenSearchEngine.py

changeset 6942
2602857055c5
parent 6645
ad476851d7e0
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2009 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the open search engine.
8 """
9
10 from __future__ import unicode_literals
11
12 import re
13 import json
14
15 from PyQt5.QtCore import pyqtSignal, pyqtSlot, QLocale, QUrl, QByteArray, \
16 QBuffer, QIODevice, QObject
17 from PyQt5.QtGui import QImage
18 from PyQt5.QtNetwork import QNetworkRequest, QNetworkAccessManager, \
19 QNetworkReply
20
21 from UI.Info import Program
22
23 import Preferences
24 import Utilities
25 from Globals import qVersionTuple
26
27
28 class OpenSearchEngine(QObject):
29 """
30 Class implementing the open search engine.
31
32 @signal imageChanged() emitted after the icon has been changed
33 @signal suggestions(list of strings) emitted after the suggestions have
34 been received
35 """
36 imageChanged = pyqtSignal()
37 suggestions = pyqtSignal(list)
38
39 def __init__(self, parent=None):
40 """
41 Constructor
42
43 @param parent reference to the parent object (QObject)
44 """
45 super(OpenSearchEngine, self).__init__(parent)
46
47 self.__suggestionsReply = None
48 self.__networkAccessManager = None
49 self._name = ""
50 self._description = ""
51 self._searchUrlTemplate = ""
52 self._suggestionsUrlTemplate = ""
53 self._searchParameters = [] # list of two tuples
54 self._suggestionsParameters = [] # list of two tuples
55 self._imageUrl = ""
56 self.__image = QImage()
57 self.__iconMoved = False
58 self.__searchMethod = "get"
59 self.__suggestionsMethod = "get"
60 self.__requestMethods = {
61 "get": QNetworkAccessManager.GetOperation,
62 "post": QNetworkAccessManager.PostOperation,
63 }
64
65 self.__replies = []
66
67 @classmethod
68 def parseTemplate(cls, searchTerm, searchTemplate):
69 """
70 Class method to parse a search template.
71
72 @param searchTerm term to search for (string)
73 @param searchTemplate template to be parsed (string)
74 @return parsed template (string)
75 """
76 locale = QLocale(Preferences.getHelp("SearchLanguage"))
77 language = locale.name().split("_")[0]
78 country = language.lower()
79
80 result = searchTemplate
81 result = result.replace("{count}", "20")
82 result = result.replace("{startIndex}", "0")
83 result = result.replace("{startPage}", "0")
84 result = result.replace("{language}", language)
85 result = result.replace("{country}", country)
86 result = result.replace("{inputEncoding}", "UTF-8")
87 result = result.replace("{outputEncoding}", "UTF-8")
88 result = result.replace(
89 "{searchTerms}",
90 bytes(QUrl.toPercentEncoding(searchTerm)).decode())
91 result = re.sub(r"""\{([^\}]*:|)source\??\}""", Program, result)
92
93 return result
94
95 @pyqtSlot(result=str)
96 def name(self):
97 """
98 Public method to get the name of the engine.
99
100 @return name of the engine (string)
101 """
102 return self._name
103
104 def setName(self, name):
105 """
106 Public method to set the engine name.
107
108 @param name name of the engine (string)
109 """
110 self._name = name
111
112 def description(self):
113 """
114 Public method to get the description of the engine.
115
116 @return description of the engine (string)
117 """
118 return self._description
119
120 def setDescription(self, description):
121 """
122 Public method to set the engine description.
123
124 @param description description of the engine (string)
125 """
126 self._description = description
127
128 def searchUrlTemplate(self):
129 """
130 Public method to get the search URL template of the engine.
131
132 @return search URL template of the engine (string)
133 """
134 return self._searchUrlTemplate
135
136 def setSearchUrlTemplate(self, searchUrlTemplate):
137 """
138 Public method to set the engine search URL template.
139
140 The URL template is processed according to the specification:
141 <a
142 href="http://www.opensearch.org/Specifications/OpenSearch/1.1#OpenSearch_URL_template_syntax">
143 http://www.opensearch.org/Specifications/OpenSearch/1.1#OpenSearch_URL_template_syntax</a>
144
145 A list of template parameters currently supported and what they are
146 replaced with:
147 <table>
148 <tr><td><b>Parameter</b></td><td><b>Value</b></td></tr>
149 <tr><td>{count}</td><td>20</td></tr>
150 <tr><td>{startIndex}</td><td>0</td></tr>
151 <tr><td>{startPage}</td><td>0</td></tr>
152 <tr><td>{language}</td>
153 <td>the default language code (RFC 3066)</td></tr>
154 <tr><td>{country}</td>
155 <td>the default language code (RFC 3066) converted to lower
156 case</td></tr>
157 <tr><td>{inputEncoding}</td><td>UTF-8</td></tr>
158 <tr><td>{outputEncoding}</td><td>UTF-8</td></tr>
159 <tr><td>{searchTerms}</td><td>the string supplied by the user</td></tr>
160 <tr><td>{*:source}</td>
161 <td>application name, QCoreApplication::applicationName()</td></tr>
162 </table>
163
164 @param searchUrlTemplate search URL template of the engine (string)
165 """
166 self._searchUrlTemplate = searchUrlTemplate
167
168 def searchUrl(self, searchTerm):
169 """
170 Public method to get a URL ready for searching.
171
172 @param searchTerm term to search for (string)
173 @return URL (QUrl)
174 """
175 if not self._searchUrlTemplate:
176 return QUrl()
177
178 ret = QUrl.fromEncoded(
179 self.parseTemplate(searchTerm, self._searchUrlTemplate)
180 .encode("utf-8"))
181
182 if self.__searchMethod != "post":
183 if qVersionTuple() >= (5, 0, 0):
184 from PyQt5.QtCore import QUrlQuery
185 urlQuery = QUrlQuery(ret)
186 for parameter in self._searchParameters:
187 urlQuery.addQueryItem(
188 parameter[0],
189 self.parseTemplate(searchTerm, parameter[1]))
190 ret.setQuery(urlQuery)
191 else:
192 for parameter in self._searchParameters:
193 ret.addQueryItem(
194 parameter[0],
195 self.parseTemplate(searchTerm, parameter[1]))
196
197 return ret
198
199 def providesSuggestions(self):
200 """
201 Public method to check, if the engine provides suggestions.
202
203 @return flag indicating suggestions are provided (boolean)
204 """
205 return self._suggestionsUrlTemplate != ""
206
207 def suggestionsUrlTemplate(self):
208 """
209 Public method to get the search URL template of the engine.
210
211 @return search URL template of the engine (string)
212 """
213 return self._suggestionsUrlTemplate
214
215 def setSuggestionsUrlTemplate(self, suggestionsUrlTemplate):
216 """
217 Public method to set the engine suggestions URL template.
218
219 @param suggestionsUrlTemplate suggestions URL template of the
220 engine (string)
221 """
222 self._suggestionsUrlTemplate = suggestionsUrlTemplate
223
224 def suggestionsUrl(self, searchTerm):
225 """
226 Public method to get a URL ready for suggestions.
227
228 @param searchTerm term to search for (string)
229 @return URL (QUrl)
230 """
231 if not self._suggestionsUrlTemplate:
232 return QUrl()
233
234 ret = QUrl.fromEncoded(QByteArray(self.parseTemplate(
235 searchTerm, self._suggestionsUrlTemplate).encode("utf-8")))
236
237 if self.__searchMethod != "post":
238 if qVersionTuple() >= (5, 0, 0):
239 from PyQt5.QtCore import QUrlQuery
240 urlQuery = QUrlQuery(ret)
241 for parameter in self._suggestionsParameters:
242 urlQuery.addQueryItem(
243 parameter[0],
244 self.parseTemplate(searchTerm, parameter[1]))
245 ret.setQuery(urlQuery)
246 else:
247 for parameter in self._suggestionsParameters:
248 ret.addQueryItem(
249 parameter[0],
250 self.parseTemplate(searchTerm, parameter[1]))
251
252 return ret
253
254 def searchParameters(self):
255 """
256 Public method to get the search parameters of the engine.
257
258 @return search parameters of the engine (list of two tuples)
259 """
260 return self._searchParameters[:]
261
262 def setSearchParameters(self, searchParameters):
263 """
264 Public method to set the engine search parameters.
265
266 @param searchParameters search parameters of the engine
267 (list of two tuples)
268 """
269 self._searchParameters = searchParameters[:]
270
271 def suggestionsParameters(self):
272 """
273 Public method to get the suggestions parameters of the engine.
274
275 @return suggestions parameters of the engine (list of two tuples)
276 """
277 return self._suggestionsParameters[:]
278
279 def setSuggestionsParameters(self, suggestionsParameters):
280 """
281 Public method to set the engine suggestions parameters.
282
283 @param suggestionsParameters suggestions parameters of the
284 engine (list of two tuples)
285 """
286 self._suggestionsParameters = suggestionsParameters[:]
287
288 def searchMethod(self):
289 """
290 Public method to get the HTTP request method used to perform search
291 requests.
292
293 @return HTTP request method (string)
294 """
295 return self.__searchMethod
296
297 def setSearchMethod(self, method):
298 """
299 Public method to set the HTTP request method used to perform search
300 requests.
301
302 @param method HTTP request method (string)
303 """
304 requestMethod = method.lower()
305 if requestMethod not in self.__requestMethods:
306 return
307
308 self.__searchMethod = requestMethod
309
310 def suggestionsMethod(self):
311 """
312 Public method to get the HTTP request method used to perform
313 suggestions requests.
314
315 @return HTTP request method (string)
316 """
317 return self.__suggestionsMethod
318
319 def setSuggestionsMethod(self, method):
320 """
321 Public method to set the HTTP request method used to perform
322 suggestions requests.
323
324 @param method HTTP request method (string)
325 """
326 requestMethod = method.lower()
327 if requestMethod not in self.__requestMethods:
328 return
329
330 self.__suggestionsMethod = requestMethod
331
332 def imageUrl(self):
333 """
334 Public method to get the image URL of the engine.
335
336 @return image URL of the engine (string)
337 """
338 return self._imageUrl
339
340 def setImageUrl(self, imageUrl):
341 """
342 Public method to set the engine image URL.
343
344 @param imageUrl image URL of the engine (string)
345 """
346 self._imageUrl = imageUrl
347
348 def setImageUrlAndLoad(self, imageUrl):
349 """
350 Public method to set the engine image URL.
351
352 @param imageUrl image URL of the engine (string)
353 """
354 self.setImageUrl(imageUrl)
355 self.__iconMoved = False
356 self.loadImage()
357
358 def loadImage(self):
359 """
360 Public method to load the image of the engine.
361 """
362 if self.__networkAccessManager is None or not self._imageUrl:
363 return
364
365 reply = self.__networkAccessManager.get(
366 QNetworkRequest(QUrl.fromEncoded(self._imageUrl.encode("utf-8"))))
367 reply.finished.connect(lambda: self.__imageObtained(reply))
368 self.__replies.append(reply)
369
370 def __imageObtained(self, reply):
371 """
372 Private slot to receive the image of the engine.
373
374 @param reply reference to the network reply
375 @type QNetworkReply
376 """
377 response = reply.readAll()
378
379 reply.close()
380 if reply in self.__replies:
381 self.__replies.remove(reply)
382 reply.deleteLater()
383
384 if response.isEmpty():
385 return
386
387 if response.startsWith(b"<html>") or response.startsWith(b"HTML"):
388 self.__iconMoved = True
389 self.__image = QImage()
390 else:
391 self.__image.loadFromData(response)
392 self.imageChanged.emit()
393
394 def image(self):
395 """
396 Public method to get the image of the engine.
397
398 @return image of the engine (QImage)
399 """
400 if not self.__iconMoved and self.__image.isNull():
401 self.loadImage()
402
403 return self.__image
404
405 def setImage(self, image):
406 """
407 Public method to set the image of the engine.
408
409 @param image image to be set (QImage)
410 """
411 if not self._imageUrl:
412 imageBuffer = QBuffer()
413 imageBuffer.open(QIODevice.ReadWrite)
414 if image.save(imageBuffer, "PNG"):
415 self._imageUrl = "data:image/png;base64,{0}".format(
416 bytes(imageBuffer.buffer().toBase64()).decode())
417
418 self.__image = QImage(image)
419 self.imageChanged.emit()
420
421 def isValid(self):
422 """
423 Public method to check, if the engine is valid.
424
425 @return flag indicating validity (boolean)
426 """
427 return self._name and self._searchUrlTemplate
428
429 def __eq__(self, other):
430 """
431 Special method implementing the == operator.
432
433 @param other reference to an open search engine (OpenSearchEngine)
434 @return flag indicating equality (boolean)
435 """
436 if not isinstance(other, OpenSearchEngine):
437 return NotImplemented
438
439 return self._name == other._name and \
440 self._description == other._description and \
441 self._imageUrl == other._imageUrl and \
442 self._searchUrlTemplate == other._searchUrlTemplate and \
443 self._suggestionsUrlTemplate == other._suggestionsUrlTemplate and \
444 self._searchParameters == other._searchParameters and \
445 self._suggestionsParameters == other._suggestionsParameters
446
447 def __lt__(self, other):
448 """
449 Special method implementing the < operator.
450
451 @param other reference to an open search engine (OpenSearchEngine)
452 @return flag indicating less than (boolean)
453 """
454 if not isinstance(other, OpenSearchEngine):
455 return NotImplemented
456
457 return self._name < other._name
458
459 def requestSuggestions(self, searchTerm):
460 """
461 Public method to request suggestions.
462
463 @param searchTerm term to get suggestions for (string)
464 """
465 if not searchTerm or not self.providesSuggestions():
466 return
467
468 if self.__networkAccessManager is None:
469 return
470
471 if self.__suggestionsReply is not None:
472 self.__suggestionsReply.abort()
473 self.__suggestionsReply.deleteLater()
474 self.__suggestionsReply = None
475
476 if self.__suggestionsMethod not in self.__requestMethods:
477 # ignore
478 return
479
480 if self.__suggestionsMethod == "get":
481 self.__suggestionsReply = self.networkAccessManager().get(
482 QNetworkRequest(self.suggestionsUrl(searchTerm)))
483 else:
484 parameters = []
485 for parameter in self._suggestionsParameters:
486 parameters.append(parameter[0] + "=" + parameter[1])
487 data = "&".join(parameters)
488 self.__suggestionsReply = self.networkAccessManager().post(
489 QNetworkRequest(self.suggestionsUrl(searchTerm)), data)
490 self.__suggestionsReply.finished.connect(
491 self.__suggestionsObtained)
492
493 def __suggestionsObtained(self):
494 """
495 Private slot to receive the suggestions.
496 """
497 if self.__suggestionsReply.error() == QNetworkReply.NoError:
498 buffer = bytes(self.__suggestionsReply.readAll())
499 response = Utilities.decodeBytes(buffer)
500 response = response.strip()
501
502 self.__suggestionsReply.close()
503 self.__suggestionsReply.deleteLater()
504 self.__suggestionsReply = None
505
506 if len(response) == 0:
507 return
508
509 try:
510 result = json.loads(response)
511 except ValueError:
512 return
513
514 try:
515 suggestions = result[1]
516 except IndexError:
517 return
518
519 self.suggestions.emit(suggestions)
520
521 def networkAccessManager(self):
522 """
523 Public method to get a reference to the network access manager object.
524
525 @return reference to the network access manager object
526 (QNetworkAccessManager)
527 """
528 return self.__networkAccessManager
529
530 def setNetworkAccessManager(self, networkAccessManager):
531 """
532 Public method to set the reference to the network access manager.
533
534 @param networkAccessManager reference to the network access manager
535 object (QNetworkAccessManager)
536 """
537 self.__networkAccessManager = networkAccessManager

eric ide

mercurial