Repository: EdinburghGenomics/pyclarity-lims

Issue:
Pooling samples
Hi all, (Edited from my original post, earlier today, because I was using the wrong data structure for StepPools.pooled_inputs)
Not an issue for you, rather for me! I'm trying to pool 2 plates into 2 tubes. First I create the two pools, set up the step and add the Illumina indexes to the tubes, I think I'm all set here, so you can probably ignore this:

```
    pool1 = Container.create(lims, type=tube_ct)
    pool2 = Container.create(lims, type=tube_ct)
    pools = [pool1, pool2]

    step = Step.create(lims, protocol_step=protocol_step, inputs=artifacts,
                       container_type_name=tube_ct.name)

    process = Process(lims, id=step.actions.step.id)

    # Assign the correct Illumina UDI indexes
<snip>
...
        sample.artifact.reagent_labels = [label]
        sample.artifact.put()
```
The issue comes when I try and pool the plates. I create a data structure and try and assign this to step.pools.pooled_inputs, thus:

```
    output_pools = {}
    for idx, plate in enumerate(plates):
        input_arts = []
        plate.get(force=True)
        out_container = pools[idx]
        pool_name = out_container.name
        placement_dict = plate.get_placements()
        for placekey in placement_dict.keys():
            art = placement_dict[placekey]
            print 'Artifact:', art
            input_arts.append(art)
        art_tuple = tuple(input_arts)  # tuble of input arts to add to a tube
        output_pools[pool_name] = tuple((out_container, art_tuple))
        out_container.get(force=True)
        for art in input_arts:
            art.put()
        plate.put()

    step.pools.pooled_inputs = output_pools

    step.advance()

```

The error I get is:

```
    step.advance()
  File "/usr/local/lib/python2.7/dist-packages/pyclarity_lims/entities.py", line 843, in advance
    data=self.lims.tostring(ElementTree.ElementTree(self.root))
  File "/usr/local/lib/python2.7/dist-packages/pyclarity_lims/lims.py", line 172, in post
    return self.parse_response(r, accept_status_codes=[200, 201, 202])
  File "/usr/local/lib/python2.7/dist-packages/pyclarity_lims/lims.py", line 214, in parse_response
    self.validate_response(response, accept_status_codes)
  File "/usr/local/lib/python2.7/dist-packages/pyclarity_lims/lims.py", line 207, in validate_response
    raise requests.exceptions.HTTPError(message)
requests.exceptions.HTTPError: 400: Unpooled inputs exist

```
I *think* I've got the data structure correct for StepPools.pooled_inputs. I'm thinking it must be the caching of the artifacts. Can anyone see where I've gone wrong?

Thanks!



Discussion:

Patch:
diff --git a/.travis.yml b/.travis.yml
index 429bf09..3fc825d 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -6,6 +6,7 @@ python:
 install:
   - "pip install -r requirements.txt"
   - "pip install python-coveralls pytest-cov"
+  - "pip install coverage --upgrade"
   - "python setup.py install"
 script: PYTHONPATH=. py.test tests/ --doctest-modules  -v --cov pyclarity_lims --cov-report term-missing
 after_success:
diff --git a/docs/PracticalExamples.rst b/docs/PracticalExamples.rst
index 3f0e5d8..4226dea 100644
--- a/docs/PracticalExamples.rst
+++ b/docs/PracticalExamples.rst
@@ -123,4 +123,35 @@ Here is an example with one sample placed into a tube.
         s.actions.put()
 
         # Complete the step
-        s.advance()
\ No newline at end of file
+        s.advance()
+
+
+Mix samples in a pool using the api
+-----------------------------------
+
+Some step will allow you to mix multiple input :py:class:`artifacts <pyclarity_lims.entities.Artifact>` into a pool also
+represented by an :py:class:`artifact <pyclarity_lims.entities.Artifact>`. This can be performed using the
+:py:class:`StepPools <pyclarity_lims.entities.StepPools>` entities.
+
+Because the pool :py:class:`artifact <pyclarity_lims.entities.Artifact>` needs to be created in the LIMS, we only
+need to provide the pool name and we need to provide `None` in place of the pool
+
+.. code::
+
+        # Assuming a Step in the pooling stage
+        s = Step(l, id='122-12345')
+        # This provides a list of all the artifacts available to pool
+        s.pools.available_inputs
+        # The pooled_inputs is a dict where the key is the name of the pool
+        # the value is a Tuple with first element is the pool artifact and the second if the pooled input
+        # here we're not specifying the pool and will let the LIMS create it.
+        s.pools.pooled_inputs['Pool1'] = (None, tuple(s.pools.available_inputs))
+        # then upload
+        s.pools.put()
+        # There no more input artifacts available
+        assert s.pools.available_inputs == []
+        # There is a pool artifact created
+        assert type(s.pools.pooled_inputs['Pool1'][0]).__name__ == 'Artifact'
+
+        # Now we can advance the step
+        s.advance()
diff --git a/pyclarity_lims/descriptors.py b/pyclarity_lims/descriptors.py
index 19243cd..5f0ae65 100644
--- a/pyclarity_lims/descriptors.py
+++ b/pyclarity_lims/descriptors.py
@@ -399,7 +399,8 @@ class XmlPooledInputDict(XmlDictionary, Nestable):
         self._delitem(key)
         node = ElementTree.SubElement(self.rootnode(self.instance), 'pool')
         node.attrib['name'] = key
-        node.attrib['uri'] = pool.uri
+        if pool and pool.uri:
+            node.attrib['output-uri'] = pool.uri
         for inart in list_input:
             sub = ElementTree.Element('input')
             sub.attrib['uri'] = inart.uri
@@ -413,11 +414,15 @@ class XmlPooledInputDict(XmlDictionary, Nestable):
 
     def _parse_element(self, element, **kwargs):
         from pyclarity_lims.entities import Artifact
+        if 'output-uri' in element.attrib:
+            pool = Artifact(self.instance.lims, uri=element.attrib.get('output-uri'))
+        else:
+            pool = None
         dict.__setitem__(
             self,
             element.attrib.get('name'),
             (
-                Artifact(self.instance.lims, uri=element.attrib.get('output-uri')),
+                pool,
                 tuple(Artifact(self.instance.lims, uri=sub.attrib.get('uri')) for sub in element.findall('input'))
             )
         )
diff --git a/pyclarity_lims/entities.py b/pyclarity_lims/entities.py
index 9d22d8d..30017cc 100644
--- a/pyclarity_lims/entities.py
+++ b/pyclarity_lims/entities.py
@@ -796,6 +796,15 @@ class StepPools(Entity):
         * an output Artifact containing the pool.
         * a tuple containing the input artifacts for that pool.
     """
+    available_inputs = EntityListDescriptor(tag='input', klass=Artifact, nesting=['available-inputs'])
+    """List of artifact available for pooling. 
+    
+    Note that adding artifacts to a pool will not remove them from this list until put() is run.
+    """
+
+    def put(self):
+        self.root = super(StepPools, self).put()
+        return self.root
 
 
 class Step(Entity):


Test patch:
diff --git a/tests/test_descriptors.py b/tests/test_descriptors.py
index c4972d9..cf2dfee 100644
--- a/tests/test_descriptors.py
+++ b/tests/test_descriptors.py
@@ -513,13 +513,24 @@ class TestXmlPooledInputDict(TestCase):
     def test___getitem__(self):
         assert self.dict1['pool1'] == (self.out1, (self.in1, self.in2))
 
-    def test___setitem__(self):
+    def test___setitem1__(self):
         assert len(self.dict1) == 2
         assert len(self.dict1.rootnode(self.dict1.instance)) == 2
+        # This works in the test but does not work in reality because
+        # the pool artifact needs to be creaated by the LIMS.
         self.dict1['pool3'] = (self.out1, (self.in1, self.in2))
         assert len(self.dict1) == 3
         assert len(self.dict1.rootnode(self.dict1.instance)) == 3
 
+    def test___setitem2__(self):
+        assert len(self.dict1) == 2
+        assert len(self.dict1.rootnode(self.dict1.instance)) == 2
+
+        # This is the correct way of creating a pool from scratch
+        self.dict1['pool3'] = (None, (self.in1, self.in2))
+        assert len(self.dict1) == 3
+        assert len(self.dict1.rootnode(self.dict1.instance)) == 3
+
 
 class TestEntityList(TestDescriptor):
     def setUp(self):