Repository: lifeomic/phc-sdk-py

Issue:
Multiple addresses in column breaks frame expansion
Pulling out address breaks when multiple values are present. For example:
```
[{'state': 'NC', 'postalCode': '27540', 'period': {'start': '2001'}}, {'use': 'old', 'state': 'SC', 'period': {'start': '1999', 'end': '2001'}}]
```

Error:
```
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-6-0355439bcf07> in <module>
----> 1 phc.Patient.get_data_frame()

/opt/conda/lib/python3.7/site-packages/phc/easy/patients/__init__.py in get_data_frame(limit, all_results, raw, query_overrides, auth_args, ignore_cache, expand_args)
    101             query_overrides,
    102             auth_args,
--> 103             ignore_cache,
    104         )

/opt/conda/lib/python3.7/site-packages/phc/easy/query/__init__.py in execute_fhir_dsl_with_options(query, transform, all_results, raw, query_overrides, auth_args, ignore_cache)
    168             return df
    169 
--> 170         return transform(df)

/opt/conda/lib/python3.7/site-packages/phc/easy/patients/__init__.py in transform(df)
     92 
     93         def transform(df: pd.DataFrame):
---> 94             return Patient.transform_results(df, **expand_args)
     95 
     96         return Query.execute_fhir_dsl_with_options(

/opt/conda/lib/python3.7/site-packages/phc/easy/patients/__init__.py in transform_results(data_frame, **expand_args)
     32         }
     33 
---> 34         return Frame.expand(data_frame, **args)
     35 
     36     @staticmethod

/opt/conda/lib/python3.7/site-packages/phc/easy/frame.py in expand(frame, code_columns, date_columns, custom_columns)
     94             *[
     95                 column_to_frame(frame, key, func)
---> 96                 for key, func in custom_columns
     97             ],
     98             frame.drop([*codeable_col_names, *custom_names], axis=1),

/opt/conda/lib/python3.7/site-packages/phc/easy/frame.py in <listcomp>(.0)
     94             *[
     95                 column_to_frame(frame, key, func)
---> 96                 for key, func in custom_columns
     97             ],
     98             frame.drop([*codeable_col_names, *custom_names], axis=1),

/opt/conda/lib/python3.7/site-packages/phc/easy/frame.py in column_to_frame(frame, column_name, expand_func)
     29     "Converts a column (if exists) to a data frame with multiple columns"
     30     if column_name in frame.columns:
---> 31         return expand_func(frame[column_name])
     32 
     33     return pd.DataFrame([])

/opt/conda/lib/python3.7/site-packages/phc/easy/patients/address.py in expand_address_column(address_col)
     32 
     33 def expand_address_column(address_col):
---> 34     return pd.DataFrame(map(expand_address_value, address_col.values))

/opt/conda/lib/python3.7/site-packages/pandas/core/frame.py in __init__(self, data, index, columns, dtype, copy)
    467         elif isinstance(data, abc.Iterable) and not isinstance(data, (str, bytes)):
    468             if not isinstance(data, (abc.Sequence, ExtensionArray)):
--> 469                 data = list(data)
    470             if len(data) > 0:
    471                 if is_list_like(data[0]) and getattr(data[0], "ndim", 1) == 1:

/opt/conda/lib/python3.7/site-packages/phc/easy/patients/address.py in expand_address_value(value)
     22 
     23     # Value is always list of one item
---> 24     assert len(value) == 1
     25     value = value[0]
     26 

AssertionError:
```

Comments:

Patch:
diff --git a/phc/easy/patients/__init__.py b/phc/easy/patients/__init__.py
index c6ecfb0..fe3c26d 100644
--- a/phc/easy/patients/__init__.py
+++ b/phc/easy/patients/__init__.py
@@ -1,24 +1,23 @@
 import pandas as pd
 
-from phc.easy.auth import Auth
 from phc.easy.frame import Frame
+from phc.easy.item import Item
 from phc.easy.patients.address import expand_address_column
 from phc.easy.patients.name import expand_name_column
-from phc.easy.query import Query
 
 
-class Patient:
+class Patient(Item):
     @staticmethod
-    def get_count(query_overrides: dict = {}, auth_args=Auth.shared()):
-        return Query.find_count_of_dsl_query(
-            {
-                "type": "select",
-                "columns": "*",
-                "from": [{"table": "patient"}],
-                **query_overrides,
-            },
-            auth_args=auth_args,
-        )
+    def table_name():
+        return "patient"
+
+    @staticmethod
+    def code_keys():
+        return [
+            "extension.valueCodeableConcept.coding",
+            "identifier.type.coding",
+            "meta.tag",
+        ]
 
     @staticmethod
     def transform_results(data_frame: pd.DataFrame, **expand_args):
@@ -32,65 +31,3 @@ class Patient:
         }
 
         return Frame.expand(data_frame, **args)
-
-    @staticmethod
-    def get_data_frame(
-        all_results: bool = False,
-        raw: bool = False,
-        query_overrides: dict = {},
-        auth_args: Auth = Auth.shared(),
-        ignore_cache: bool = False,
-        expand_args: dict = {},
-    ):
-        """Retrieve patients as a data frame with unwrapped FHIR columns
-
-        Attributes
-        ----------
-        all_results : bool = False
-            Override limit to retrieve all patients
-
-        raw : bool = False
-            If raw, then values will not be expanded (useful for manual
-            inspection if something goes wrong). Note that this option will
-            override all_results if True.
-
-        query_overrides : dict = {}
-            Override any part of the elasticsearch FHIR query
-
-        auth_args : Any
-            The authenication to use for the account and project (defaults to shared)
-
-        ignore_cache : bool = False
-            Bypass the caching system that auto-saves results to a CSV file.
-            Caching only occurs when all results are being retrieved.
-
-        expand_args : Any
-            Additional arguments passed to phc.Frame.expand
-
-        Examples
-        --------
-        >>> import phc.easy as phc
-        >>> phc.Auth.set({'account': '<your-account-name>'})
-        >>> phc.Project.set_current('My Project Name')
-        >>> phc.Patient.get_data_frame()
-
-        """
-        query = {
-            "type": "select",
-            "columns": "*",
-            "from": [{"table": "patient"}],
-            **query_overrides,
-        }
-
-        def transform(df: pd.DataFrame):
-            return Patient.transform_results(df, **expand_args)
-
-        return Query.execute_fhir_dsl_with_options(
-            query,
-            transform,
-            all_results,
-            raw,
-            query_overrides,
-            auth_args,
-            ignore_cache,
-        )
diff --git a/phc/easy/patients/address.py b/phc/easy/patients/address.py
index edbd1c8..c7af9a3 100644
--- a/phc/easy/patients/address.py
+++ b/phc/easy/patients/address.py
@@ -1,4 +1,6 @@
 import pandas as pd
+
+from funcy import first
 from phc.easy.codeable import generic_codeable_to_dict
 from phc.easy.util import concat_dicts
 
@@ -22,18 +24,29 @@ def expand_address_value(value):
     if type(value) is not list:
         return {}
 
-    # Value is always list of one item
-    assert len(value) == 1
-    value = value[0]
+    primary_address = first(
+        filter(lambda v: v.get("use") != "old", value)
+    ) or first(value)
+
+    other_addresses = list(
+        filter(lambda address: address != primary_address, value)
+    )
 
-    return concat_dicts(
-        [
-            expand_address_attr(f"address_{key}", item_value)
-            for key, item_value in value.items()
-            if key != "text"
-        ]
+    other_attrs = (
+        {"other_addresses": other_addresses} if len(other_addresses) > 0 else {}
     )
 
+    return {
+        **concat_dicts(
+            [
+                expand_address_attr(f"address_{key}", item_value)
+                for key, item_value in primary_address.items()
+                if key != "text"
+            ]
+        ),
+        **other_attrs,
+    }
+
 
 def expand_address_column(address_col):
     return pd.DataFrame(map(expand_address_value, address_col.values))


Test patch:
diff --git a/tests/test_easy_patient_address.py b/tests/test_easy_patient_address.py
index d51ff86..42bb1cd 100644
--- a/tests/test_easy_patient_address.py
+++ b/tests/test_easy_patient_address.py
@@ -1,18 +1,64 @@
 import pandas as pd
+import math
 from phc.easy.patients.address import expand_address_column
 
+
+def non_na_dict(dictionary: dict):
+    return {
+        k: v
+        for k, v in dictionary.items()
+        if not isinstance(v, float) or not math.isnan(v)
+    }
+
+
 def test_expand_address_column():
-    sample = pd.DataFrame([{
-        'address': [
-            {'line': ['123 ABC Court'], 'city': 'Zionsville', 'state': 'IN', 'use': 'home'}
+    sample = pd.DataFrame(
+        [
+            {
+                "address": [
+                    {
+                        "line": ["123 ABC Court"],
+                        "city": "Zionsville",
+                        "state": "IN",
+                        "use": "home",
+                    }
+                ]
+            },
+            {
+                "address": [
+                    {
+                        "use": "old",
+                        "state": "SC",
+                        "period": {"start": "1999", "end": "2001"},
+                    },
+                    {
+                        "state": "NC",
+                        "city": "Raleigh",
+                        "period": {"start": "2001"},
+                    },
+                ]
+            },
         ]
-    }])
+    )
 
     df = expand_address_column(sample.address)
 
-    assert df.iloc[0].to_dict() == {
-        'address_line_0': '123 ABC Court',
-        'address_city': 'Zionsville',
-        'address_state': 'IN',
-        'address_use': 'home'
+    assert non_na_dict(df.iloc[0].to_dict()) == {
+        "address_line_0": "123 ABC Court",
+        "address_city": "Zionsville",
+        "address_state": "IN",
+        "address_use": "home",
+    }
+
+    assert non_na_dict(df.iloc[1].to_dict()) == {
+        "address_city": "Raleigh",
+        "address_state": "NC",
+        "address_period_start": "2001",
+        "other_addresses": [
+            {
+                "use": "old",
+                "state": "SC",
+                "period": {"start": "1999", "end": "2001"},
+            }
+        ],
     }