This is a raw view of the Python source code due to an error in generating the documentation.
Date of Conversion: 2025-03-07 18:22:52
# -*- coding: utf-8 -*-
"""
chemspipy.search
~~~~~~~~~~~~~~~~
A wrapper for asynchronous search requests.
"""
from __future__ import print_function
from __future__ import unicode_literals
from __future__ import division
import datetime
import logging
import threading
import time
from six.moves import range
from . import errors, objects, utils
log = logging.getLogger(__name__)
# TODO: Use Sequence abc metaclass?
class Results(object):
"""Container class to perform a search on a background thread and hold the results when ready."""
def __init__(self, cs, searchfunc, searchargs, raise_errors=False, max_requests=40):
"""Generally shouldn't be instantiated directly. See :meth:`~chemspipy.api.ChemSpider.search` instead.
:param ChemSpider cs: ``ChemSpider`` session.
:param function searchfunc: Search function that returns a transaction ID.
:param tuple searchargs: Arguments for the search function.
:param bool raise_errors: If True, raise exceptions. If False, store on ``exception`` property.
:param int max_requests: Maximum number of times to check if search results are ready.
"""
log.debug('Results init')
self._cs = cs
self._raise_errors = raise_errors
self._max_requests = max_requests
self._status = 'Created'
self._exception = None
self._qid = None
self._message = None
self._start = None
self._end = None
self._results = []
self._searchthread = threading.Thread(name='SearchThread', target=self._search, args=(cs, searchfunc, searchargs))
self._searchthread.start()
def _search(self, cs, searchfunc, searchargs):
"""Perform the search and retrieve the results."""
log.debug('Searching in background thread')
self._start = datetime.datetime.utcnow()
try:
self._qid = searchfunc(*searchargs)
log.debug('Setting qid: %s' % self._qid)
for _ in range(self._max_requests):
log.debug('Checking status: %s' % self._qid)
status = cs.filter_status(self._qid)
self._status = status['status']
self._message = status.get('message', '')
log.debug(status)
time.sleep(0.2)
if status['status'] == 'Complete':
break
elif status['status'] in {'Failed', 'Unknown', 'Suspended', 'Not Found'}:
raise errors.ChemSpiPyServerError('Search Failed: %s' % status.get('message', ''))
else:
raise errors.ChemSpiPyTimeoutError('Search took too long')
log.debug('Search success!')
self._end = datetime.datetime.utcnow()
if status['count'] > 0:
self._results = [objects.Compound(cs, csid) for csid in cs.filter_results(self._qid)]
log.debug('Results: %s', self._results)
elif not self._message:
self._message = 'No results found'
except Exception as e:
# Catch and store exception so we can raise it in the main thread
self._exception = e
self._end = datetime.datetime.utcnow()
if self._status == 'Created':
self._status = 'Failed'
def ready(self):
"""Return True if the search finished.
:rtype: bool
"""
return not self._searchthread.is_alive()
def success(self):
"""Return True if the search finished with no errors.
:rtype: bool
"""
return self.ready() and not self._exception
def wait(self):
"""Block until the search has completed and optionally raise any resulting exception."""
log.debug('Waiting for search to finish')
self._searchthread.join()
if self._exception and self._raise_errors:
raise self._exception
@property
def status(self):
"""Current status string returned by ChemSpider.
:return: 'Unknown', 'Created', 'Scheduled', 'Processing', 'Suspended', 'PartialResultReady', 'ResultReady'
:rtype: string
"""
return self._status
@property
def exception(self):
"""Any Exception raised during the search. Blocks until the search is finished."""
self.wait() # TODO: If raise_errors=True this will raise the exception when trying to access it?
return self._exception
@property
def qid(self):
"""Search query ID.
:rtype: string
"""
return self._qid
@property
def message(self):
"""A contextual message about the search. Blocks until the search is finished.
:rtype: string
"""
self.wait()
return self._message
@property
def count(self):
"""The number of search results. Blocks until the search is finished.
:rtype: int
"""
return len(self)
@property
def duration(self):
"""The time taken to perform the search. Blocks until the search is finished.
:rtype: :py:class:`datetime.timedelta`
"""
self.wait()
return self._end - self._start
@utils.memoized_property
def sdf(self):
"""Get an SDF containing all the search results.
:return: SDF containing the search results.
:rtype: bytes
"""
self.wait()
return self._cs.filter_results_sdf(self._qid)
def __getitem__(self, index):
"""Get a single result or a slice of results. Blocks until the search is finished.
This means a Results instance can be treated like a normal Python list. For example::
cs.search('glucose')[2]
cs.search('glucose')[0:2]
An IndexError will be raised if the index is greater than the total number of results.
"""
self.wait()
return self._results.__getitem__(index)
def __len__(self):
self.wait()
return self._results.__len__()
def __iter__(self):
self.wait()
return iter(self._results)
def __repr__(self):
if self.success():
return 'Results(%s)' % self._results
else:
return 'Results(%s)' % self.status