diff --git a/docs/api.rst b/docs/api.rst index 3b87fc70..e5323676 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -2,7 +2,31 @@ API === +Connecting to ISPyB +=================== + .. automodule:: ispyb :members: :show-inheritance: +Accessing records using the object model +======================================== + +The connection object offers the following accessor functions to get +object-like representations of database entries: + +.. autoclass:: ispyb.model.interface.ObjectModelMixIn + :members: + +DataCollection +============== + +.. automodule:: ispyb.model.datacollection + :members: + +ProcessingJob +============= + +.. automodule:: ispyb.model.processingjob + :members: + diff --git a/docs/conf.py b/docs/conf.py index b5b1f087..4ec82e08 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -83,7 +83,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'alabaster' +html_theme = 'classic' # Theme options are theme-specific and customize the look and feel of a # theme further. For a list of options available for each theme, see the diff --git a/ispyb/__init__.py b/ispyb/__init__.py index cd7f165f..56a58d01 100644 --- a/ispyb/__init__.py +++ b/ispyb/__init__.py @@ -11,7 +11,13 @@ _log = logging.getLogger('ispyb') def open(configuration_file): - '''Create an ISPyB connection using settings from a configuration file.''' + '''Create an ISPyB connection using settings from a configuration file. + This can be used either as a function call or as a context manager. + + :param configuration_file: Full path to a file containing database + credentials + :return: ISPyB connection object + ''' config = configparser.RawConfigParser(allow_no_value=True) if not config.read(configuration_file): raise AttributeError('No configuration found at %s' % configuration_file) diff --git a/ispyb/interface/acquisition.py b/ispyb/interface/acquisition.py index 54785d16..5be68280 100644 --- a/ispyb/interface/acquisition.py +++ b/ispyb/interface/acquisition.py @@ -28,4 +28,6 @@ def upsert_data_collection(self, cursor, values): def get_data_collection(self, dcid): '''Return a DataCollection object representing the information about the selected data collection''' + import warnings + warnings.warn("Object model getter call on the data area is deprecated and will be removed in the next release. Call the function on connection object instead.", DeprecationWarning) return ispyb.model.datacollection.DataCollection(dcid, self) diff --git a/ispyb/interface/connection.py b/ispyb/interface/connection.py index 9117b7f3..8712cbc5 100644 --- a/ispyb/interface/connection.py +++ b/ispyb/interface/connection.py @@ -1,9 +1,13 @@ import abc import ispyb.interface.factory +import ispyb.model.interface ABC = abc.ABCMeta('ABC', (object,), {'__slots__': ()}) # compatible with Python 2 *and* 3 -class IF(ABC, ispyb.interface.factory.factory_mixin): +class IF( + ABC, + ispyb.interface.factory.FactoryMixIn, + ispyb.model.interface.ObjectModelMixIn): '''ISPyB connection interface definition object.''' @abc.abstractmethod diff --git a/ispyb/interface/factory.py b/ispyb/interface/factory.py index 4dd2f29e..d414ca57 100644 --- a/ispyb/interface/factory.py +++ b/ispyb/interface/factory.py @@ -2,7 +2,7 @@ import importlib -class factory_mixin(): +class FactoryMixIn(): def _get_data_area(self, module, classname): '''Helper function to instantiate a data area or return a cached instance.''' if hasattr(self, '_cache_' + module): diff --git a/ispyb/interface/processing.py b/ispyb/interface/processing.py index 3fab45ed..b2adfc69 100644 --- a/ispyb/interface/processing.py +++ b/ispyb/interface/processing.py @@ -25,4 +25,6 @@ def get_quality_indicators_params(self): def get_processing_job(self, jobid): '''Return a ProcessingJob object representing the information about the selected processing job''' + import warnings + warnings.warn("Object model getter call on the data area is deprecated and will be removed in the next release. Call the function on connection object instead.", DeprecationWarning) return ispyb.model.processingjob.ProcessingJob(jobid, self) diff --git a/ispyb/model/datacollection.py b/ispyb/model/datacollection.py index c1e2ac90..75cb3caf 100644 --- a/ispyb/model/datacollection.py +++ b/ispyb/model/datacollection.py @@ -17,6 +17,7 @@ def __init__(self, dcid, db_area, preload=None): :return: A DataCollection object representing the database entry for the specified DataCollectionID ''' + self._cache_group = None self._db = db_area self._dcid = int(dcid) if preload: @@ -31,6 +32,13 @@ def dcid(self): '''Returns the DataCollectionID.''' return self._dcid + @property + def group(self): + '''Returns a DataCollectionGroup object''' + if self._cache_group is None: + self._cache_group = DataCollectionGroup(self.dcgid, self._db.conn) + return self._cache_group + def __repr__(self): '''Returns an object representation, including the DataCollectionID, the database connection interface object, and the cache status.''' @@ -48,12 +56,68 @@ def __str__(self): 'DataCollection #{0.dcid}', ' Started : {0.time_start}', ' Finished : {0.time_end}', + ' DC group : {0.dcgid}', ))).format(self) for key, internalkey in ( + ('dcgid', 'groupId'), ('time_start', 'startTime'), ('time_end', 'endTime'), ('image_count', 'noImages'), ('image_start_number', 'startImgNumber'), ): setattr(DataCollection, key, property(lambda self, k=internalkey: self._data[k])) + +class DataCollectionGroup(ispyb.model.DBCache): + '''An object representing a DataCollectionGroup database entry. The object + lazily accesses the underlying database when necessary and exposes record + data as python attributes. + ''' + + def __init__(self, dcgid, db_conn, preload=None): + '''Create a DataCollectionGroup object for a defined DCGID. Requires + a database connection object exposing further data access methods. + + :param dcgid: DataCollectionGroupID + :param db_conn: ISPyB database connection object + :return: A DataCollectionGroup object representing the database entry for + the specified DataCollectionGroupID + ''' + self._cache_gridinfo = None + self._db = db_conn + self._dcgid = int(dcgid) + if preload: + self._data = preload + + def reload(self): + '''Load/update information from the database.''' + raise NotImplementedError("TODO: Loading not yet supported") + + @property + def dcgid(self): + '''Returns the DataCollectionGroupID.''' + return self._dcgid + + @property + def gridinfo(self): + '''Returns a GridInfo object.''' + if self._cache_gridinfo is None: + self._cache_gridinfo = GridInfo(self.dcgid, self._db) + return self._cache_gridinfo + + def __repr__(self): + '''Returns an object representation, including the DataCollectionGroupID, + the database connection interface object, and the cache status.''' + return '' % ( + self._dcgid, + 'cached' if self.cached else 'uncached', + self._db + ) + + def __str__(self): + '''Returns a pretty-printed object representation.''' + if not self.cached: + return 'DataCollectionGroup #%d (not yet loaded from database)' % self._dcgid + return ('\n'.join(( + 'DataCollectionGroup #{0.dcgid}', + ))).format(self) diff --git a/ispyb/model/gridinfo.py b/ispyb/model/gridinfo.py new file mode 100644 index 00000000..e60268f6 --- /dev/null +++ b/ispyb/model/gridinfo.py @@ -0,0 +1,49 @@ +from __future__ import absolute_import, division, print_function + +import ispyb.model + +class GridInfo(ispyb.model.DBCache): + '''An object representing a GridInfo database entry. The object + lazily accesses the underlying database when necessary and exposes record + data as python attributes. + ''' + + def __init__(self, dcgid, db_conn, preload=None): + '''Create a GridInfo object for a defined DCGID. Requires + a database connection object exposing further data access methods. + + :param dcgid: DataCollectionGroupID + :param db_conn: ISPyB database connection object + :return: A GridInfo object representing the database entry for + the specified DataCollectionGroupID + ''' + self._db = db_conn + self._dcgid = int(dcgid) + if preload: + self._data = preload + + def reload(self): + '''Load/update information from the database.''' + raise NotImplementedError('TODO: Not implemented yet') + + @property + def dcgid(self): + '''Returns the DataCollectionGroupID.''' + return self._dcgid + + def __repr__(self): + '''Returns an object representation, including the DataCollectionGroupID, + the database connection interface object, and the cache status.''' + return '' % ( + self._dcgid, + 'cached' if self.cached else 'uncached', + self._db + ) + + def __str__(self): + '''Returns a pretty-printed object representation.''' + if not self.cached: + return 'GridInfo #%d (not yet loaded from database)' % self._dcid + return ('\n'.join(( + 'GridInfo #{0.dcgid}', + ))).format(self) diff --git a/ispyb/model/interface.py b/ispyb/model/interface.py new file mode 100644 index 00000000..8a006054 --- /dev/null +++ b/ispyb/model/interface.py @@ -0,0 +1,31 @@ +from __future__ import absolute_import, division, print_function + +import ispyb.model.datacollection +import ispyb.model.processingjob + +class ObjectModelMixIn(): + '''Object model accessor functions for Connector classes.''' + + def get_data_collection(self, dcid): + '''Return a DataCollection object representing the information + about the selected data collection''' + return ispyb.model.datacollection.DataCollection( + dcid, + self.mx_acquisition, + ) + + def get_data_collection_group(self, dcgid): + '''Return a DataCollectionGroup object representing the information + about the selected data collection group''' + return ispyb.model.datacollection.DataCollectionGroup( + dcgid, + self, + ) + + def get_processing_job(self, jobid): + '''Return a ProcessingJob object representing the information + about the selected processing job.''' + return ispyb.model.processingjob.ProcessingJob( + jobid, + self.mx_processing, + ) diff --git a/tests/test_mxacquisition.py b/tests/test_mxacquisition.py index 1f986288..502552ff 100644 --- a/tests/test_mxacquisition.py +++ b/tests/test_mxacquisition.py @@ -37,8 +37,10 @@ def test_mxacquisition_methods(testconfig): rs = mxacquisition.retrieve_data_collection_main(id1) assert rs[0]['groupId'] == dcgid - dc = mxacquisition.get_data_collection(id1) + dc = conn.get_data_collection(id1) assert dc.image_count == 360 + assert dc.dcgid == dcgid + assert dc.group.dcgid == dcgid params = mxacquisition.get_image_params() params['parentid'] = id1