Repository: SciLifeLab/genologics

Issue:
Create new samples
Hi guys!

I was wondering how it would be possible to create new samples using the REST API. I got it working using this package but it required some hacking. Perhaps I'm missing something.. Are you using the API to do something like that?

This is briefly my approach:

1. The API needs a "location" reference with a container on sample level so I borrow some code from `create` and add the extra stuff in the XML code:

```python
lims_sample = Sample(lims=lims, _create_new=True)
uhmmm = "{}:{}".format(Sample._PREFIX, Sample.__name__.lower())
lims_sample.root = ElementTree.Element(nsmap(uhmmm))

lims_sample.name = name
lims_sample.project = lims_project
location = ElementTree.SubElement(lims_sample.root, 'location')
ElementTree.SubElement(location, 'container', dict(uri=lims_container.uri))
position_element = ElementTree.SubElement(location, 'value')
position_element.text = position
```

2. To save a sample you need to use the "smp:samplecreation" tag (or whatever it's called) so I have to modify the data string before posting:

```python
data = lims.tostring(ElementTree.ElementTree(lims_sample.root))
data = data.decode('utf-8').replace('smp:sample', 'smp:samplecreation')
lims_sample.root = lims.post(uri=lims.get_uri(Sample._URI), data=data)
lims_sample._uri = lims_sample.root.attrib['uri']
```

Just wanted to share the research I did to make this work. If it's something you are interested in adding support for I'd be happy to work with you. Right now I don't feel like I understand the package well enough to suggest how best to add it tho.

Discussion:

Patch:
diff --git a/genologics/entities.py b/genologics/entities.py
index 3cdef28..89c4fd9 100644
--- a/genologics/entities.py
+++ b/genologics/entities.py
@@ -297,10 +297,12 @@ class Entity(object):
         self.lims.post(self.uri, data)
 
     @classmethod
-    def create(cls, lims, **kwargs):
-        """Create an instance from attributes then post it to the LIMS"""
+    def _create(cls, lims, creation_tag=None, **kwargs):
+        """Create an instance from attributes and return it"""
         instance = cls(lims, _create_new=True)
-        if cls._TAG:
+        if creation_tag:
+            instance.root = ElementTree.Element(nsmap(cls._PREFIX + ':' + creation_tag))
+        elif cls._TAG:
             instance.root = ElementTree.Element(nsmap(cls._PREFIX + ':' + cls._TAG))
         else:
             instance.root = ElementTree.Element(nsmap(cls._PREFIX + ':' + cls.__name__.lower()))
@@ -309,6 +311,13 @@ class Entity(object):
                 setattr(instance, attribute, kwargs.get(attribute))
             else:
                 raise TypeError("%s create: got an unexpected keyword argument '%s'" % (cls.__name__, attribute))
+
+        return instance
+
+    @classmethod
+    def create(cls, lims, creation_tag=None, **kwargs):
+        """Create an instance from attributes then post it to the LIMS"""
+        instance = cls._create(lims, creation_tag=None, **kwargs)
         data = lims.tostring(ElementTree.ElementTree(instance.root))
         instance.root = lims.post(uri=lims.get_uri(cls._URI), data=data)
         instance._uri = instance.root.attrib['uri']
@@ -412,6 +421,23 @@ class Sample(Entity):
     # biosource XXX
 
 
+    @classmethod
+    def create(cls, lims, container, position, **kwargs):
+        """Create an instance of Sample from attributes then post it to the LIMS"""
+        if not isinstance(container, Container):
+            raise TypeError('%s is not of type Container'%container)
+        instance = super(Sample, cls)._create(lims, creation_tag='samplecreation', **kwargs)
+
+        location = ElementTree.SubElement(instance.root, 'location')
+        ElementTree.SubElement(location, 'container', dict(uri=container.uri))
+        position_element = ElementTree.SubElement(location, 'value')
+        position_element.text = position
+        data = lims.tostring(ElementTree.ElementTree(instance.root))
+        instance.root = lims.post(uri=lims.get_uri(cls._URI), data=data)
+        instance._uri = instance.root.attrib['uri']
+        return instance
+
+
 class Containertype(Entity):
     "Type of container for analyte artifacts."
 
diff --git a/genologics/version.py b/genologics/version.py
index 613e29a..58caa73 100644
--- a/genologics/version.py
+++ b/genologics/version.py
@@ -1,1 +1,1 @@
-__version__="0.3.8"
+__version__="0.3.9"


Test patch:
diff --git a/tests/test_entities.py b/tests/test_entities.py
index 91dcbbd..933f708 100644
--- a/tests/test_entities.py
+++ b/tests/test_entities.py
@@ -1,9 +1,10 @@
+import operator
 from sys import version_info
 from unittest import TestCase
 from xml.etree import ElementTree
 
 from genologics.entities import StepActions, Researcher, Artifact, \
-    Step, StepPlacements, Container, Stage, ReagentKit, ReagentLot
+    Step, StepPlacements, Container, Stage, ReagentKit, ReagentLot, Sample, Project
 from genologics.lims import Lims
 
 if version_info[0] == 2:
@@ -126,6 +127,20 @@ generic_step_actions_no_escalation_xml = """<stp:actions xmlns:stp="http://genol
   </next-actions>
 </stp:actions>"""
 
+generic_sample_creation_xml = """
+<smp:samplecreation xmlns:smp="http://genologics.com/ri/sample" limsid="s1" uri="{url}/api/v2/samples/s1">
+  <location>
+    <container limsid="cont1" uri="{url}/api/v2/containers/cont1">
+    </container>
+    <value>1:1</value>
+  </location>
+  <name>
+    sample1
+  </name>
+  <project uri="{url}/api/v2/projects/p1" limsid="p1">
+  </project>
+</smp:samplecreation>
+"""
 
 class TestEntities(TestCase):
     def test_pass(self):
@@ -133,12 +148,22 @@ class TestEntities(TestCase):
 
 
 def elements_equal(e1, e2):
-    if e1.tag != e2.tag: return False
-    if e1.text and e2.text and e1.text.strip() != e2.text.strip(): return False
-    if e1.tail and e2.tail and e1.tail.strip() != e2.tail.strip(): return False
-    if e1.attrib != e2.attrib: return False
-    if len(e1) != len(e2): return False
-    return all(elements_equal(c1, c2) for c1, c2 in zip(e1, e2))
+    if e1.tag != e2.tag:
+        print('Tag: %s != %s'%(e1.tag, e2.tag))
+        return False
+    if e1.text and e2.text and e1.text.strip() != e2.text.strip():
+        print('Text: %s != %s' % (e1.text.strip(), e2.text.strip()))
+        return False
+    if e1.tail and e2.tail and e1.tail.strip() != e2.tail.strip():
+        print('Tail: %s != %s' % (e1.tail.strip(), e2.tail.strip()))
+        return False
+    if e1.attrib != e2.attrib:
+        print('Attrib: %s != %s' % (e1.attrib, e2.attrib))
+        return False
+    if len(e1) != len(e2):
+        print('length %s (%s) != length (%s) ' % (e1.tag, len(e1), e2.tag, len(e2)))
+        return False
+    return all(elements_equal(c1, c2) for c1, c2 in zip(sorted(e1, key=lambda x: x.tag), sorted(e2, key=lambda x: x.tag)))
 
 
 class TestEntities(TestCase):
@@ -289,3 +314,30 @@ class TestReagentLots(TestEntities):
             assert l.uri
             assert l.name == 'kitname'
             assert l.lot_number == '100'
+
+
+class TestSample(TestEntities):
+    sample_creation = generic_sample_creation_xml.format(url=url)
+
+    def test_create_entity(self):
+        with patch('genologics.lims.requests.post',
+                   return_value=Mock(content=self.sample_creation, status_code=201)) as patch_post:
+            l = Sample.create(
+                self.lims,
+                project=Project(self.lims, uri='project'),
+                container=Container(self.lims, uri='container'),
+                position='1:1',
+                name='s1',
+            )
+            data = '''<?xml version=\'1.0\' encoding=\'utf-8\'?>
+            <smp:samplecreation xmlns:smp="http://genologics.com/ri/sample">
+            <name>s1</name>
+            <project uri="project" />
+            <location>
+              <container uri="container" />
+              <value>1:1</value>
+            </location>
+            </smp:samplecreation>'''
+            assert elements_equal(ElementTree.fromstring(patch_post.call_args_list[0][1]['data']), ElementTree.fromstring(data))
+
+