Repository: alexgolec/tda-api

Issue:
Functions expecting arrays should tolerate single scalar values
Provided they are of the right type. 

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/alexgolec/tda-api/pull/38?src=pr&el=h1) Report
> Merging [#38](https://codecov.io/gh/alexgolec/tda-api/pull/38?src=pr&el=desc) into [master](https://codecov.io/gh/alexgolec/tda-api/commit/eb0aecdb65d326c56727dd26be3848557cb069ee&el=desc) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/alexgolec/tda-api/pull/38/graphs/tree.svg?width=650&height=150&src=pr&token=M8H98HA8CK)](https://codecov.io/gh/alexgolec/tda-api/pull/38?src=pr&el=tree)

```diff
@@           Coverage Diff           @@
##           master      #38   +/-   ##
=======================================
  Coverage   99.31%   99.31%           
=======================================
  Files          11       11           
  Lines        1601     1609    +8     
=======================================
+ Hits         1590     1598    +8     
  Misses         11       11           
```


| [Impacted Files](https://codecov.io/gh/alexgolec/tda-api/pull/38?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [tda/client.py](https://codecov.io/gh/alexgolec/tda-api/pull/38/diff?src=pr&el=tree#diff-dGRhL2NsaWVudC5weQ==) | `99.77% <100.00%> (+<0.01%)` | :arrow_up: |
| [tda/utils.py](https://codecov.io/gh/alexgolec/tda-api/pull/38/diff?src=pr&el=tree#diff-dGRhL3V0aWxzLnB5) | `96.15% <100.00%> (+0.15%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/alexgolec/tda-api/pull/38?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/alexgolec/tda-api/pull/38?src=pr&el=footer). Last update [eb0aecd...416a563](https://codecov.io/gh/alexgolec/tda-api/pull/38?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/tda/client.py b/tda/client.py
index b1dbc79..31e82ec 100644
--- a/tda/client.py
+++ b/tda/client.py
@@ -437,6 +437,9 @@ class Client(EnumEnforcer):
         projection = self.convert_enum(
             projection, self.Instrument.Projection)
 
+        if isinstance(symbols, str):
+            symbols = [symbols]
+
         params = {
             'apikey': self.api_key,
             'symbol': ','.join(symbols),
@@ -856,6 +859,9 @@ class Client(EnumEnforcer):
         `Official documentation
         <https://developer.tdameritrade.com/quotes/apis/get/marketdata/
         quotes>`__.'''
+        if isinstance(symbols, str):
+            symbols = [symbols]
+
         params = {
             'apikey': self.api_key,
             'symbol': ','.join(symbols)
@@ -956,6 +962,9 @@ class Client(EnumEnforcer):
         `Official documentation
         <https://developer.tdameritrade.com/user-principal/apis/get/
         userprincipals/streamersubscriptionkeys-0>`__.'''
+        if isinstance(account_ids, int) or isinstance(account_ids, str):
+            account_ids = [account_ids]
+
         params = {
             'apikey': self.api_key,
             'accountIds': ','.join(str(i) for i in account_ids)
diff --git a/tda/utils.py b/tda/utils.py
index 152ae10..55f7473 100644
--- a/tda/utils.py
+++ b/tda/utils.py
@@ -32,6 +32,9 @@ class EnumEnforcer:
         if iterable is None:
             return None
 
+        if isinstance(iterable, required_enum_type):
+            return [iterable.value]
+
         values = []
         for value in iterable:
             if isinstance(value, required_enum_type):


Test patch:
diff --git a/tests/client_test.py b/tests/client_test.py
index 73f0fe6..842ad1f 100644
--- a/tests/client_test.py
+++ b/tests/client_test.py
@@ -210,6 +210,18 @@ class TestClient(unittest.TestCase):
                 'status': 'FILLED,EXPIRED'
             })
 
+    @no_duplicates
+    @patch('tda.client.datetime.datetime', mockdatetime)
+    def test_get_orders_by_path_statuses_scalar(self):
+        self.client.get_orders_by_path(
+            ACCOUNT_ID, statuses=Client.Order.Status.FILLED)
+        self.mock_session.get.assert_called_once_with(
+            self.make_url('/v1/accounts/{accountId}/orders'), params={
+                'fromEnteredTime': MIN_ISO,
+                'toEnteredTime': NOW_DATETIME_ISO,
+                'status': 'FILLED'
+            })
+
     @no_duplicates
     @patch('tda.client.datetime.datetime', mockdatetime)
     def test_get_orders_by_path_statuses_unchecked(self):
@@ -313,6 +325,17 @@ class TestClient(unittest.TestCase):
                 'status': 'FILLED,EXPIRED'
             })
 
+    @no_duplicates
+    @patch('tda.client.datetime.datetime', mockdatetime)
+    def test_get_orders_by_query_statuses_scalar(self):
+        self.client.get_orders_by_query(statuses=Client.Order.Status.FILLED)
+        self.mock_session.get.assert_called_once_with(
+            self.make_url('/v1/orders'), params={
+                'fromEnteredTime': MIN_ISO,
+                'toEnteredTime': NOW_DATETIME_ISO,
+                'status': 'FILLED'
+            })
+
     @no_duplicates
     @patch('tda.client.datetime.datetime', mockdatetime)
     def test_get_orders_by_query_statuses_unchecked(self):
@@ -467,6 +490,14 @@ class TestClient(unittest.TestCase):
             self.make_url('/v1/accounts/{accountId}'),
             params={'fields': 'positions,orders'})
 
+    @no_duplicates
+    def test_get_account_fields_scalar(self):
+        self.client.get_account(
+                ACCOUNT_ID, fields=Client.Account.Fields.POSITIONS)
+        self.mock_session.get.assert_called_once_with(
+            self.make_url('/v1/accounts/{accountId}'),
+            params={'fields': 'positions'})
+
     @no_duplicates
     def test_get_account_fields_unchecked(self):
         self.client.set_enforce_enums(False)
@@ -492,6 +523,13 @@ class TestClient(unittest.TestCase):
             self.make_url('/v1/accounts'),
             params={'fields': 'positions,orders'})
 
+    @no_duplicates
+    def test_get_accounts_fields_scalar(self):
+        self.client.get_accounts(fields=Client.Account.Fields.POSITIONS)
+        self.mock_session.get.assert_called_once_with(
+            self.make_url('/v1/accounts'),
+            params={'fields': 'positions'})
+
     @no_duplicates
     def test_get_accounts_fields_unchecked(self):
         self.client.set_enforce_enums(False)
@@ -512,6 +550,16 @@ class TestClient(unittest.TestCase):
                 'symbol': 'AAPL,MSFT',
                 'projection': 'fundamental'})
 
+    @no_duplicates
+    def test_search_instruments_one_instrument(self):
+        self.client.search_instruments(
+            'AAPL', Client.Instrument.Projection.FUNDAMENTAL)
+        self.mock_session.get.assert_called_once_with(
+            self.make_url('/v1/instruments'), params={
+                'apikey': API_KEY,
+                'symbol': 'AAPL',
+                'projection': 'fundamental'})
+
     @no_duplicates
     def test_search_instruments_unchecked(self):
         self.client.set_enforce_enums(False)
@@ -550,6 +598,16 @@ class TestClient(unittest.TestCase):
                 'markets': 'EQUITY,BOND',
                 'date': NOW_DATE_ISO})
 
+    @no_duplicates
+    def test_get_hours_for_multiple_markets_single_market(self):
+        self.client.get_hours_for_multiple_markets(
+                Client.Markets.EQUITY, NOW_DATETIME)
+        self.mock_session.get.assert_called_once_with(
+            self.make_url('/v1/marketdata/hours'), params={
+                'apikey': API_KEY,
+                'markets': 'EQUITY',
+                'date': NOW_DATE_ISO})
+
     @no_duplicates
     def test_get_hours_for_multiple_markets_date(self):
         self.client.get_hours_for_multiple_markets([
@@ -568,8 +626,8 @@ class TestClient(unittest.TestCase):
                 Client.Markets.EQUITY,
                 Client.Markets.BOND], '2020-01-01')
         self.assertEqual(str(cm.exception),
-                         "expected type in (datetime.date, datetime.datetime) for " +
-                         "date, got 'builtins.str'")
+                         "expected type in (datetime.date, datetime.datetime) "
+                         "for date, got 'builtins.str'")
 
     @no_duplicates
     def test_get_hours_for_multiple_markets_unchecked(self):
@@ -1020,6 +1078,14 @@ class TestClient(unittest.TestCase):
                 'apikey': API_KEY,
                 'symbol': 'AAPL,MSFT'})
 
+    @no_duplicates
+    def test_get_quotes_single_symbol(self):
+        self.client.get_quotes('AAPL')
+        self.mock_session.get.assert_called_once_with(
+            self.make_url('/v1/marketdata/quotes'), params={
+                'apikey': API_KEY,
+                'symbol': 'AAPL'})
+
     # get_transaction
 
     @no_duplicates
@@ -1156,6 +1222,15 @@ class TestClient(unittest.TestCase):
                 'apikey': API_KEY,
                 'accountIds': '1000,2000,3000'})
 
+    @no_duplicates
+    def test_get_streamer_subscription_keys_one_account_id(self):
+        self.client.get_streamer_subscription_keys(1000)
+        self.mock_session.get.assert_called_once_with(
+            self.make_url('/v1/userprincipals/streamersubscriptionkeys'),
+            params={
+                'apikey': API_KEY,
+                'accountIds': '1000'})
+
     @no_duplicates
     def test_get_streamer_subscription_keys_str(self):
         self.client.get_streamer_subscription_keys(['1000', '2000', '3000'])
@@ -1185,6 +1260,15 @@ class TestClient(unittest.TestCase):
                 'apikey': API_KEY,
                 'fields': 'streamerSubscriptionKeys,preferences'})
 
+    @no_duplicates
+    def test_get_user_principals_one_field(self):
+        self.client.get_user_principals(
+            fields=Client.UserPrincipals.Fields.PREFERENCES)
+        self.mock_session.get.assert_called_once_with(
+            self.make_url('/v1/userprincipals'), params={
+                'apikey': API_KEY,
+                'fields': 'preferences'})
+
     @no_duplicates
     def test_get_user_principals_fields_unchecked(self):
         self.client.set_enforce_enums(False)