Repository: EdinburghGenomics/pyclarity-lims

Issue:
Queue Class does not have an easily acessible "queue-time" member variable
In the Queue Class I feel as if the "queue-time" member variable should exist under artifacts just like location does. However, I have to traverse artifacts._elems[0]._children[0].text to get the queue time. Can we make this so something like artifacts[0].queue_time works? Thanks.

Comments:

Patch:
diff --git a/pyclarity_lims/descriptors.py b/pyclarity_lims/descriptors.py
index b33666f..a5a4c28 100644
--- a/pyclarity_lims/descriptors.py
+++ b/pyclarity_lims/descriptors.py
@@ -721,6 +721,48 @@ class ExternalidList(XmlList):
         list.append(self, (element.attrib.get('id'), element.attrib.get('uri')))
 
 
+class QueuedArtifactList(TagXmlList):
+    """This is a list of Artifact associated with the time they spent in the queue and their location on a plate.
+    The list contains tuples organise as follow:
+        (A, B, (C, D)) where
+         A is an artifact
+         B is a datetime object,
+         C is a container
+         D is a string specifying the location such as "1:1"
+        """
+
+    def __init__(self, instance, *args, **kwargs):
+        TagXmlList.__init__(self, instance, tag='artifact', nesting=['artifacts'], *args, **kwargs)
+
+    def _parse_element(self, element, lims, **kwargs):
+        from pyclarity_lims.entities import Artifact, Container
+        input_art = Artifact(lims, uri=element.attrib['uri'])
+        loc = element.find('location')
+        location = (None, None)
+        if loc:
+            location = (
+                Container(lims, uri=loc.find('container').attrib['uri']),
+                loc.find('value').text
+            )
+        qt = element.find('queue-time')
+        queue_date = None
+        if qt is not None:
+            h, s, t = qt.text.rpartition(':')
+            qt = h + t
+            microsec = ''
+            if '.' in qt:
+                microsec = '.%f'
+            date_format = '%Y-%m-%dT%H:%M:%S' + microsec
+            try:
+                queue_date = datetime.datetime.strptime(qt, date_format + '%z')
+            except ValueError:
+                # support for python 2.7 ignore time zone
+                # use python 3 for timezone support
+                qt = qt.split('+')[0]
+                queue_date = datetime.datetime.strptime(qt, date_format)
+        list.append(self, (input_art, queue_date, location))
+
+
 # Descriptors: This section contains the object that can be used in entities
 class BaseDescriptor(XmlElement):
     """Abstract base descriptor for an instance attribute."""
diff --git a/pyclarity_lims/entities.py b/pyclarity_lims/entities.py
index 4a51878..c51eb7c 100644
--- a/pyclarity_lims/entities.py
+++ b/pyclarity_lims/entities.py
@@ -5,7 +5,7 @@ from pyclarity_lims.descriptors import StringDescriptor, UdfDictionaryDescriptor
     InputOutputMapList, LocationDescriptor, IntegerAttributeDescriptor, \
     StringAttributeDescriptor, EntityListDescriptor, StringListDescriptor, PlacementDictionaryDescriptor, \
     ReagentLabelList, AttributeListDescriptor, StringDictionaryDescriptor, OutputPlacementListDescriptor, \
-    XmlActionList, MutableDescriptor, XmlPooledInputDict
+    XmlActionList, MutableDescriptor, XmlPooledInputDict, QueuedArtifactList
 
 try:
     from urllib.parse import urlsplit, urlparse, parse_qs, urlunparse
@@ -1041,13 +1041,28 @@ class ReagentType(Entity):
                         self.sequence = child.attrib.get("value")
 
 class Queue(Entity):
-    """Queue of a given step"""
+    """Queue of a given workflow stage"""
     _URI = "queues"
     _TAG= "queue"
     _PREFIX = "que"
 
-    artifacts = EntityListDescriptor("artifact", Artifact, nesting=["artifacts"])
-    """List of :py:class:`artifacts <pyclarity_lims.entities.Artifact>` associated with this workflow."""
+    queued_artifacts = MutableDescriptor(QueuedArtifactList)
+    """
+    List of :py:class:`artifacts <pyclarity_lims.entities.Artifact>` associated with this workflow stage.
+    alongside the time the've been added to that queue and the container they're in.
+    The list contains tuples organise as follow:
+        (A, B, (C, D)) where
+         A is an :py:class:`artifacts <pyclarity_lims.entities.Artifact>`
+         B is a :py:class:`datetime <datetime.datetime>` object,
+         C is a :py:class:`container <pyclarity_lims.entities.Container>`
+         D is a string specifying the location such as "1:1"
+    """
+
+    @property
+    def artifacts(self):
+        """List of :py:class:`artifacts <pyclarity_lims.entities.Artifact>` associated with this workflow stage."""
+        return [i[0] for i in self.queued_artifacts]
+
 
 Sample.artifact = EntityDescriptor('artifact', Artifact)
 StepActions.step = EntityDescriptor('step', Step)


Test patch:
diff --git a/tests/test_descriptors.py b/tests/test_descriptors.py
index 7cfea0a..be7c731 100644
--- a/tests/test_descriptors.py
+++ b/tests/test_descriptors.py
@@ -3,14 +3,15 @@ from sys import version_info
 from unittest import TestCase
 from xml.etree import ElementTree
 
+import datetime
 import pytest
 
 from pyclarity_lims.constants import nsmap
 from pyclarity_lims.descriptors import StringDescriptor, StringAttributeDescriptor, StringListDescriptor, \
     StringDictionaryDescriptor, IntegerDescriptor, BooleanDescriptor, UdfDictionary, EntityDescriptor, \
     InputOutputMapList, EntityListDescriptor, PlacementDictionary, EntityList, SubTagDictionary, ExternalidList,\
-    XmlElementAttributeDict, XmlAttributeList, XmlReagentLabelList, XmlPooledInputDict, XmlAction
-from pyclarity_lims.entities import Artifact, ProtocolStep
+    XmlElementAttributeDict, XmlAttributeList, XmlReagentLabelList, XmlPooledInputDict, XmlAction, QueuedArtifactList
+from pyclarity_lims.entities import Artifact, ProtocolStep, Container
 from pyclarity_lims.lims import Lims
 from tests import elements_equal
 
@@ -728,3 +729,58 @@ class TestXmlAction(TestCase):
 
         with pytest.raises(KeyError):
             action['whatever'] = 'youwant'
+
+
+class TestQueuedArtifactList(TestCase):
+    def setUp(self):
+        et = ElementTree.fromstring('''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+                <test-entry>
+                <artifacts>
+                <artifact uri="{url}/artifacts/a1">
+                <queue-time>2011-12-25T01:10:10.050+00:00</queue-time>
+                <location>
+                <container uri="{url}/containers/c1"/>
+                <value>A:1</value>
+                </location>
+                </artifact>
+                <artifact uri="{url}/artifacts/a2">
+                <queue-time>2011-12-25T01:10:10.200+01:00</queue-time>
+                <location>
+                <container uri="{url}/containers/c1"/>
+                <value>A:2</value>
+                </location>
+                </artifact>
+                </artifacts>
+                </test-entry>'''.format(url='http://testgenologics.com:4040/api/v2'))
+
+        self.lims = Lims('http://testgenologics.com:4040', username='test', password='password')
+        self.instance1 = Mock(root=et, lims=self.lims)
+
+    def get_queue_art(self, art_id, pos, microsec, time_delta):
+        if version_info[0] == 2:
+            return (
+                Artifact(self.lims, id=art_id),
+                datetime.datetime(2011, 12, 25, 1, 10, 10, microsec),
+                (Container(self.lims, id='c1'), pos)
+            )
+        else:
+            return (
+                Artifact(self.lims, id=art_id),
+                datetime.datetime(2011, 12, 25, 1, 10, 10, microsec, tzinfo=datetime.timezone(time_delta)),
+                (Container(self.lims, id='c1'), pos)
+            )
+
+    def test_parse(self):
+        queued_artifacts = QueuedArtifactList(self.instance1)
+        qart = self.get_queue_art('a1', 'A:1', 50000, datetime.timedelta(0, 0))
+        assert queued_artifacts[0] == qart
+        qart = self.get_queue_art('a2', 'A:2', 200000, datetime.timedelta(0, 3600))
+        assert queued_artifacts[1] == qart
+
+    def test_set(self):
+        queued_artifacts = QueuedArtifactList(self.instance1)
+        qart = self.get_queue_art('a1', 'A:3',  50000, datetime.timedelta(0, 0))
+        with pytest.raises(NotImplementedError):
+            queued_artifacts.append(qart)
+
+