#!/usr/bin/env python2
# coding: utf-8
# VENI, SANCTE SPIRITUS
from PySide import QtCore
from PySide import QtNetwork
[docs]class QClient(QtCore.QObject):
""" Esta clase realiza las peticiones a servidores HTTP
Ejemplo de uso:
>>> from qtr import QClient
>>> from PySide import QtGui
>>> QtGui.QApplication([])
>>> client = QClient()
>>> client.addstep_seturl("http://www.gnu.org")
>>> client.addstep_setscraper(scraper)
>>> client.do()
>>> results = client.getinternaldata()
Revise la documentación para descubrir que otros pasos más tiene disponible.
"""
nextstep = QtCore.Signal()
finished = QtCore.Signal()
abortexecution = QtCore.Signal(str, int)
accumulate = QtCore.Signal()
def __init__(self, qnamattr=None, parent=None):
super(QClient, self).__init__(parent=parent)
# logging.debug("QClient instace started!")
self.__stack = []
self._url = QtCore.QUrl()
self.__stepindex = None
self.__request = QtNetwork.QNetworkRequest(self._url)
self.__request.setOriginatingObject(self)
self.___reply = None
self.__data = QtCore.QByteArray()
self.__internaldata = None
self.__accumdata = []
self._eventloop = QtCore.QEventLoop()
self._finished = False
if not parent:
self.__qnm = QtNetwork.QNetworkAccessManager(self)
else:
# get the QNetworkAcessManager from the parent givin
# its attribute name
self.__qnm = getattr(self.parent(), qnamattr)
self.nextstep.connect(self.step)
self.__qnm.finished[QtNetwork.QNetworkReply].connect(self._endrequest)
self.abortexecution.connect(self.abort)
self.accumulate.connect(self._accumulatedata)
def __add_step(self, **kwords):
""" Método de bajo nivel que añade pasos dentro de la pila de pasos
:param dict kwords: debe contener al menos el la llave ``method``,\
puede ser una cadena o un método o función de Python. Actualmente\
la única cadena que tiene significado para este método es ``get``,\
el verbo para hacer peticiones a servidores web.
"""
if "method" in kwords:
method = kwords.get("method")
if isinstance(method, str):
# Is indicating the name of a predefined method to add
if method == "get":
kwords["verb"] = "get"
# FIXME: Agregar el verbo post
kwords["method"] = self._executerequest
self.__stack.append(kwords)
else:
self.abortexecution.emit("No 'method' argument was giving!", 1)
[docs] def _seturl(self, **kwords):
""" Método privado que estable la URL
:param dict kwords: debe contener las llaves ``fromscraped``,\
``url`` y ``query``.
:keyword bool fromscraped: indica si la URL a establecer se\
encuentra en el almacenamiento interno\
:attr:`qtr.QClient.__internaldata`.
:keyword str url: La URL a usar.
:keyword dict query: los *queries* a establecer en la URL.
"""
fromscraped = kwords.get("fromscraped")
if fromscraped:
url = self.__internaldata
else:
url = kwords.get("url")
query = kwords.get("query")
if not url:
self.abortexecution.emit("Setting empty urls is bad idea!", 1)
return
else:
self._url.setUrl(url)
if not self._url.isValid():
self.abortexecution.emit(
"This url \"{}\" is not valid!".format(url), 1)
return
# Usually we would want to
# overwrite the queries in the URL
if query:
for key in query.keys():
self._url.removeQueryItem(key)
for key, value in query.iteritems():
self._url.addQueryItem(key, value)
[docs] def _executerequest(self, **kwords):
""" Ejecuta la petición al servidor web.
:param dict kwords: debe contener la llave ``verb``\
que indica el verbo HTTP que se usara para la\
petición. Este método usa :attr:`qtr.QClient.__request`\
previamente modificado para realizar la petición.
Si la instancia QClient es hija de un objeto QtObject, establece
como origen de la petición a sí misma.
"""
# setting the URL
self.__request.setUrl(self._url)
# Sets the object origin of the request
if self.parent():
self.__request.setOriginatingObject(self)
# doing the request
if "verb" not in kwords:
self.abortexecution.emit(
"there is no HTTP verb in the arguments", 1)
return
else:
verb = getattr(self.__qnm, kwords.get("verb"))
verb(self.__request)
def __execute_step(self):
""" Ejecuta el paso actual directamente de la pila
cada paso debe contener la llave ``method``. Este método busca también
por los siguientes llaves:
:keyword bool block: indica si este paso bloquea el flujo de ejecución\
hasta que algo le indique terminar. ``False`` por defecto.
:keyword bool store: indica que lo devuelto por este paso, debe ser\
almacenado en ``__internaldata``. ``False`` por defecto.
:keyword bool isScrap: indica que este paso es un raspador. ``False``\
por defecto.
:keyword tuple args: indica que el paso contiene una llave con\
una tupla dentro. ``tuple()`` por defecto.
:keyword bool passargs: indica que hay que pasar ``args`` a\
``method``. ``False`` por defecto.
:keyword bool passkwords: indica que hay que pasar ``step`` a\
``method``. ``True`` por defecto.
:keyword bool passselfattribute: indica que hay que pasar un atributo\
de la instancia del objeto ``QClient``. ``False`` por defecto.
:keyword str selfattributename: indica el nombre del atributo de la\
instancia del objeto ``QClient``. ``None`` por defecto.
:keyword classmethod selfattribute: contiene el atributo tomado de la\
instancia del objeto ``QClient``. No existe si\
``passselfattribute`` es ``False``
:keyword bool selfattributeismethod: indica que el atributo no es\
un atributo, sino un método que retorna algo. ``False`` por defecto.
"""
# logging.debug("executing step {}...".format(self.__stepindex))
# get the step from the stack
if self.__stepindex + 1 > len(self.__stack):
return True
index = self.__stepindex
step = self.__stack[index]
# every step is just a key=value dictionary
method = step.get("method") # required, the method to execute
block = step.get("block", False) # block in this step?
store = step.get("store", False) # store what method has returned?
# should we scrap what is inside self.__data?
stepisscrap = step.get("isScrap", False)
args = step.get("args", tuple()) # get the tuple, if any.
# should we pass the tuple to the method?
passargs = step.get("passargs", False)
# should we pass the dict to the method?
passkwords = step.get("passkwords", True)
# should we pass an attribute of this object
passselfattribute = step.get("passselfattribute", False)
# what's the name of that attribute?
selfattributename = step.get("selfattribute", None)
# the attribute itself
if passselfattribute:
selfattribute = getattr(self, selfattributename)
# this attribute is not an attribute but a method!
selfattributeismethod = step.get("selfattributeibuteismethod", False)
# execute it
if passkwords:
tmpinternaldata = method(**step)
elif passargs:
tmpinternaldata = method(*args)
elif passargs and passkwords:
tmpinternaldata = method(*args, **step)
elif passselfattribute:
if selfattributeismethod:
tmpinternaldata = method(selfattribute())
else:
tmpinternaldata = method(selfattribute)
else:
if stepisscrap:
tmpinternaldata = method(self.__data.data())
else:
tmpinternaldata = method()
# should we store the results?
if store or stepisscrap:
self.__internaldata = tmpinternaldata
# blocks on this step
# until something else makes exit the loop
if block:
self._eventloop.exec_()
# set the next step
self.__stepindex += 1
# logging.debug(self.__stepindex)
@QtCore.Slot()
[docs] def getinternaldata(self, accumulated=False):
""" Retorna los datos internos.
:param bool accumulated: si es ``True``, retorna los datos\
acumulados internos en vez de los internos.
:return: una cadena, o una lista.
"""
if accumulated:
return self.__accumdata
else:
return self.__internaldata
@QtCore.Slot(str, int)
[docs] def abort(self, message, exception):
""" Aborta la ejecución de la secuencia de ordenes.
"""
self._finished = True
self.finished.emit()
QtCore.QTimer.singleShot(5 * 1000, self._eventloop.quit)
if exception == 1:
raise RuntimeError(message)
@QtCore.Slot(QtNetwork.QNetworkReply)
[docs] def _endrequest(self, reply):
""" Almacena los datos devueltos por el servidor web.
Este método hace que el loop de eventos se rompa y el flujo
de la ejecución pueda continuar.
"""
if self.parent() is None or\
(self.parent() and reply.request().originatingObject() == self):
self.__data = reply.readAll()
self.__reply = reply
if self._eventloop.isRunning():
# exits the eventloop, if any
self._eventloop.quit()
@QtCore.Slot()
[docs] def _accumulatedata(self):
""" Acumula los datos para guardarlos de forma segura y evitar
que sean sobre-escritos.
"""
self.__accumdata.extend(self.__internaldata)
[docs] def do(self):
""" Inicia la secuencia de ordenes pre-programadas.
"""
# logging.debug("Starting!")
self.nextstep.emit()
[docs] def step(self):
""" Emite una señal para que se ejecute un paso.
Este método es llamado de forma recursiva hasta que
se alcanza el final de la pila.
"""
if self.__stepindex is None:
self.__stepindex = 0
self._finished = self.__execute_step()
if not self._finished:
self.nextstep.emit()
else:
self.finished.emit()
[docs] def addstep_seturl(self, url, query={}):
""" Establecerá la URL sobre la cual realizar la petición web.
Se debe usar este método antes que
:meth:`qtr.QClient.addstep_setrequest`.
"""
if url is None or url == "":
fromscraped = True
else:
fromscraped = False
self.__add_step(method=self.__data.clear, passkwords=False)
self.__add_step(method=self._seturl, url=url,
fromscraped=fromscraped, query=query)
[docs] def addstep_setrequest(self, verb):
""" Establecerá el verbo de la petición web
"""
self.__add_step(method=verb, block=True)
[docs] def addstep_setscraper(self, method):
""" Establece el método que raspara el contenido web
"""
self.__add_step(method=method, isScrap=True,
store=True, passkwords=False)
[docs] def addstep_setscraperpageurls(self, method, perpagescraper):
""" Establece el raspador que extraerá la lista de URLs en una lista
y la función que repasara cada enlace raspando su contenido.
:param classmethod method: el raspador de URLs.
:param classmethod perpagescraper: el raspador que raspa el\
contenido de cada URL
"""
self.__add_step(method=method, isScrap=True,
store=True, passkwords=False)
self.__add_step(method=self._setpagescraper,
scraper=perpagescraper)
[docs] def _setpagescraper(self, **kwords):
""" Establece el raspador que raspara el contenido de cada enlace
Este método es usado por :meth:`qtr.QClient.addstep_setscraperpageurls`
y no hay razón para usarlo directamente.
"""
scraper = kwords.get("scraper")
for link in self.__internaldata:
# for every link to a page scraped by addstep_setscraperpageurls
self.addstep_seturl(url=link)
self.addstep_setrequest("get")
self.addstep_setscraper(scraper)
self.__add_step(method=self.accumulate.emit, passkwords=False)
[docs] def status(self):
""" Retorna el código HTTP de la petición realizada
:return: código HTTP
:rtype: int
"""
return self.__reply.attribute(
QtNetwork.QNetworkRequest.HttpStatusCodeAttribute)
[docs] def error(self):
""" Retorna el código de error de la petición realizada
:return: código de error
:rtype: int
"""
return self.__reply.error()
[docs] def geturl(self):
""" Retorna la URL actual
:return: url
:rtype: unicode
"""
self._url.toString()