Repository: redcap-tools/PyCap

Issue:
Version is set to 0.0.0 after installing with pip
`./pip install -e git+https://github.com/sburns/PyCap.git#egg=PyCap`


$ ./pip show pycap
Name: PyCap
Version: 0.0.0
Summary: PyCap: Python interface to REDCap
Home-page: http://sburns.github.com/PyCap
Author: None
Author-email: None
License: MIT
Location: /programs/x86_64-linux/python/3.6.5/bin/src/pycap
Requires: requests, semantic-version
Required-by: 

Discussion:
sburns: Yeah in practice its the same, it comes down to what files you expect to see changed during a PR review. Technically, this is backwards incompatible as you're removing `__version__` from the top-level package.

Someone might be doing

```
import redcap
do_something_with(redcap.__version__)
```

That will `AttributeError` if this PR lands.
pwildenhain: Good catch! I've added that variable back in for backwards compatibility. I know it's not ideal to have it documented in two places (i.e why you had the other process 😉) though I think this is worth the short term benefit of making the package version work when installing from GitHub. And in the long term, we might be able to come up with a different strategy that meets both needs.
pwildenhain: Alternatively, if you don't want to double document this, we could try refactoring the `get_version()` function to look more like this: https://github.com/jendrikseipp/vulture/blob/master/setup.py#L18 ?

Or do you think that still wouldn't solve our problem?

Patch:
diff --git a/README.rst b/README.rst
index cbd84c6..010c4b2 100644
--- a/README.rst
+++ b/README.rst
@@ -22,6 +22,7 @@ Currently, these API calls are available:
 
 -   Export Records
 -   Export Metadata
+-   Delete Records
 -   Import Records
 -   Export File
 -   Import File
@@ -82,6 +83,9 @@ Usage
     ...     # This throws if you made a bad request, e.g. tried to delete a field
     ...     # that isn't a file
 
+    # Delete record
+    >>> response = project.delete_record('1')
+
     # Export form event mappings
     >>> fem = project.export_fem()
     ...
diff --git a/docs/deep.rst b/docs/deep.rst
index ede5c1f..c8e2f79 100644
--- a/docs/deep.rst
+++ b/docs/deep.rst
@@ -141,6 +141,7 @@ Exporting large projects will fail on REDCap's backend and PyCap will throw a ``
 
 Here's an exporting function that trades speed for robustness::
 
+    # Python 3 does not support xrange(), use range() instead
     def chunked_export(project, chunk_size=100):
         def chunks(l, n):
             """Yield successive n-sized chunks from list l"""
diff --git a/redcap/__init__.py b/redcap/__init__.py
index 39c209b..f4df935 100644
--- a/redcap/__init__.py
+++ b/redcap/__init__.py
@@ -4,6 +4,7 @@
 __author__ = 'Scott Burns <scott.s.burns@gmail.com>'
 __license__ = 'MIT'
 __copyright__ = '2014, Vanderbilt University'
+__version__ = '1.0.2'
 
 """
 This module exposes the REDCap API through the Project class. Instantiate the
@@ -18,4 +19,3 @@ Other API requests are available, such as exporting users & Form-Event Mappings.
 
 from .project import Project
 from .request import RCRequest, RCAPIError, RedcapError
-from .version import VERSION as __version__
diff --git a/redcap/project.py b/redcap/project.py
index 3e3e0ad..5a93396 100755
--- a/redcap/project.py
+++ b/redcap/project.py
@@ -179,11 +179,11 @@ class Project(object):
             from pandas import read_csv
             ret_format = 'csv'
         pl = self.__basepl('formEventMapping', format=ret_format)
-        to_add = [arms]
-        str_add = ['arms']
-        for key, data in zip(str_add, to_add):
-            if data:
-                pl[key] = ','.join(data)
+
+        if arms:
+            for i, value in enumerate(arms):
+                pl["arms[{}]".format(i)] = value
+
         response, _ = self._call_api(pl, 'exp_fem')
         if format in ('json', 'csv', 'xml'):
             return response
@@ -226,7 +226,9 @@ class Project(object):
         str_add = ['fields', 'forms']
         for key, data in zip(str_add, to_add):
             if data:
-                pl[key] = ','.join(data)
+                for i, value in enumerate(data):
+                    pl["{}[{}]".format(key, i)] = value
+
         response, _ = self._call_api(pl, 'metadata')
         if format in ('json', 'csv', 'xml'):
             return response
@@ -235,6 +237,32 @@ class Project(object):
                 df_kwargs = {'index_col': 'field_name'}
             return read_csv(StringIO(response), **df_kwargs)
 
+    def delete_record(self, record, format='json'):
+        """
+        Delete a record form the Project.
+
+        Parameters
+        __________
+        record : string
+            The record ID that you wan to delete from the project
+
+        format : (``'json'``), ``'csv'``, ``'xml'``, ``'df'``
+            Return the metadata in native objects, csv or xml.
+            ``'df'`` will return a ``pandas.DataFrame``.
+        Returns
+        _______
+        None
+        """
+        pl = dict()
+        pl['action'] = 'delete'
+        pl['content'] = 'record'
+        pl['token'] = self.token
+        pl['records[0]'] = record
+        pl['format'] = format
+        response, _ = self._call_api(pl,'del_record')
+        if format in ('json', 'csv', 'xml'):
+            return response
+
     def export_records(self, records=None, fields=None, forms=None,
     events=None, raw_or_label='raw', event_name='label',
     format='json', export_survey_fields=False,
@@ -311,9 +339,9 @@ class Project(object):
         'exportCheckboxLabel')
         for key, data in zip(str_keys, keys_to_add):
             if data:
-                #  Make a url-ok string
                 if key in ('fields', 'records', 'forms', 'events'):
-                    pl[key] = ','.join(data)
+                    for i, value in enumerate(data):
+                        pl["{}[{}]".format(key, i)] = value
                 else:
                     pl[key] = data
 
@@ -665,7 +693,7 @@ class Project(object):
         pl = self.__basepl(content='generateNextRecordName')
 
         return self._call_api(pl, 'exp_next_id')[0]
-      
+
     def export_project_info(self, format='json'):
         """
         Export Project Information
diff --git a/redcap/request.py b/redcap/request.py
index 487ea6b..1aacba0 100644
--- a/redcap/request.py
+++ b/redcap/request.py
@@ -67,6 +67,8 @@ class RCRequest(object):
         valid_data = {
             'exp_record': (['type', 'format'], 'record',
                 'Exporting record but content is not record'),
+            'del_record': (['format'], 'record',
+                'Deleting record but content is not record'),
             'imp_record': (['type', 'overwriteBehavior', 'data', 'format'],
                 'record', 'Importing record but content is not record'),
             'metadata': (['format'], 'metadata',
@@ -172,3 +174,6 @@ class RCRequest(object):
         # specifically 10.5
         if 500 <= r.status_code < 600:
             raise RedcapError(r.content)
+
+        if 400 == r.status_code and self.type == 'exp_record':
+            raise RedcapError(r.content)
diff --git a/redcap/version.py b/redcap/version.py
deleted file mode 100644
index a98c726..0000000
--- a/redcap/version.py
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-__author__ = 'Scott Burns <scott.s.burns@gmail.com>'
-__license__ = 'MIT'
-__copyright__ = '2014, Vanderbilt University'
-
-VERSION = '1.0.2'
diff --git a/setup.py b/setup.py
index cece97d..dbe302b 100644
--- a/setup.py
+++ b/setup.py
@@ -5,21 +5,30 @@ __author__ = 'Scott Burns <scott.s.burns@gmail.com>'
 __license__ = 'MIT'
 __copyright__ = '2014, Vanderbilt University'
 
+import codecs
 import os
+import re
+
+# Taken from vulture setup.py: https://github.com/jendrikseipp/vulture/blob/master/setup.py
+def read(*parts):
+    here = os.path.abspath(os.path.dirname(__file__))
+    with codecs.open(os.path.join(here, *parts), "r") as f:
+        return f.read()
+
+def find_version(*file_parts):
+    version_file = read(*file_parts)
+    version_match = re.search(
+        r"^__version__ = ['\"]([^'\"]*)['\"]$", version_file, re.M
+    )
+    if version_match:
+        return version_match.group(1)
+    raise RuntimeError("Unable to find version string.")
 
 try:
     from setuptools import setup
 except ImportError:
     from distutils.core import setup
 
-
-def get_version():
-    base = os.path.dirname(__file__)
-    with open(os.path.join(base, 'redcap/version.py')) as f:
-        VERSION = None
-        exec(f.read())
-        return VERSION
-
 required = [
     'requests>=1.0.0',
     'semantic-version>=2.3.1'
@@ -37,7 +46,7 @@ if __name__ == '__main__':
         description="""PyCap: Python interface to REDCap""",
         license='MIT',
         url='http://sburns.github.com/PyCap',
-        version=get_version(),
+        version=find_version("redcap", "__init__.py"),
         download_url='http://sburns.github.com/PyCap',
         long_description=long_desc,
         packages=['redcap'],


Test patch:
diff --git a/test/test_project.py b/test/test_project.py
index f2661c4..83f7c2e 100644
--- a/test/test_project.py
+++ b/test/test_project.py
@@ -29,6 +29,11 @@ except NameError:
     def is_bytestring(s):
         return isinstance(s, bytes)
 
+try:
+    from unittest import mock
+except ImportError:
+    import mock
+
 
 class ProjectTests(unittest.TestCase):
     """docstring for ProjectTests"""
@@ -392,6 +397,23 @@ class ProjectTests(unittest.TestCase):
         csv = self.reg_proj.export_metadata(format='csv')
         self.assertTrue(self.is_good_csv(csv))
 
+    def test_metadata_export_passes_filters_as_arrays(self):
+        self.reg_proj._call_api = mock.Mock()
+        self.reg_proj._call_api.return_value = (None, None)
+        self.reg_proj.export_metadata(
+            fields=['field0', 'field1', 'field2'],
+            forms=['form0', 'form1', 'form2'],
+        )
+
+        args, _ = self.reg_proj._call_api.call_args
+
+        payload = args[0]
+
+        self.assertEqual(payload['fields[0]'], 'field0')
+        self.assertEqual(payload['fields[1]'], 'field1')
+        self.assertEqual(payload['fields[2]'], 'field2')
+        self.assertEqual(payload['forms[2]'], 'form2')
+
     def test_bad_creds(self):
         "Test that exceptions are raised with bad URL or tokens"
         with self.assertRaises(RedcapError):
@@ -408,6 +430,21 @@ class ProjectTests(unittest.TestCase):
         for arm in fem:
             self.assertIsInstance(arm, dict)
 
+    def test_fem_export_passes_filters_as_arrays(self):
+        self.reg_proj._call_api = mock.Mock()
+        self.reg_proj._call_api.return_value = (None, None)
+        self.reg_proj.export_fem(
+            arms=['arm0', 'arm1', 'arm2'],
+        )
+
+        args, _ = self.reg_proj._call_api.call_args
+
+        payload = args[0]
+
+        self.assertEqual(payload['arms[0]'], 'arm0')
+        self.assertEqual(payload['arms[1]'], 'arm1')
+        self.assertEqual(payload['arms[2]'], 'arm2')
+
     @responses.activate
     def test_file_export(self):
         """Test file export and proper content-type parsing"""
@@ -634,6 +671,27 @@ class ProjectTests(unittest.TestCase):
         for record in records:
             self.assertIn(self.reg_proj.def_field, record)
 
+    def test_export_passes_filters_as_arrays(self):
+        self.reg_proj._call_api = mock.Mock()
+        self.reg_proj._call_api.return_value = (None, None)
+        self.reg_proj.export_records(
+            records=['record0', 'record1', 'record2'],
+            fields=['field0', 'field1', 'field2'],
+            forms=['form0', 'form1', 'form2'],
+            events=['event0', 'event1', 'event2']
+        )
+
+        args, _ = self.reg_proj._call_api.call_args
+
+        payload = args[0]
+
+        self.assertEqual(payload['records[0]'], 'record0')
+        self.assertEqual(payload['records[1]'], 'record1')
+        self.assertEqual(payload['records[2]'], 'record2')
+        self.assertEqual(payload['fields[1]'], 'field1')
+        self.assertEqual(payload['forms[2]'], 'form2')
+        self.assertEqual(payload['events[0]'], 'event0')
+
     @responses.activate
     def test_generate_next_record_name(self):
         "Test exporting the next potential record ID for a project"