Repository: planetlabs/planet-client-python
Issue: The locations property in models.py breaks when order fails

On branch v2-polling-output-310, in models.py, `locations()` returns a TypeError when trying to unpack all download locations when `self.results` is a NoneType, i.e., when an order fails. Specifically, this occurs at line 380 in the following return statement:
```
return list(r[self.LOCATION_KEY] for r in self.results)
```

Below is the code used for the request, poll, and download the order, taken from docs/guide.md:
```
import asyncio
from planet import Session
from planet import OrdersClient

# Build request
request = {
  "name": "test_order",
  "products": [
    {
      "item_ids": [
        "20170614_113217_3163208_RapidEye-5"
      ],
      "item_type": "REOrthoTile",
      "product_bundle": "analytic"
    }
  ],
  "tools": [
    {
      "toar": {
        "scale_factor": 10000
      }
    },
    {
      "reproject": {
        "projection": "WSG84",
        "kernel": "cubic"
      }
    },
    {
      "tile": {
        "tile_size": 1232,
        "origin_x": -180,
        "origin_y": -90,
        "pixel_size": 2.7056277056e-05,
        "name_template": "C1232_30_30_{tilex:04d}_{tiley:04d}"
      }
    }
  ]
}    

# Polling & Downloading
from planet import reporting

async def create_poll_and_download():
    async with Session() as sess:
        cl = OrdersClient(sess)
        with reporting.StateBar(state='creating') as bar:
            # create order
            order = await cl.create_order(request)
            bar.update(state='created', order_id=order.id)

            # poll
            await cl.poll(order.id, report=bar.update)

        # download
        await cl.download_order(order.id)

asyncio.run(create_poll_and_download())
```

Below is the traceback for the aforementioned error:

```
TypeError                                 Traceback (most recent call last)
<ipython-input-12-401800b0f64e> in <module>
     13         await cl.download_order(order.id)
     14
---> 15 asyncio.run(create_poll_and_download())

/usr/local/Cellar/python@3.9/3.9.6/Frameworks/Python.framework/Versions/3.9/lib/python3.9/asyncio/runners.py in run(main, debug)
     42         if debug is not None:
     43             loop.set_debug(debug)
---> 44         return loop.run_until_complete(main)
     45     finally:
     46         try:

/usr/local/Cellar/python@3.9/3.9.6/Frameworks/Python.framework/Versions/3.9/lib/python3.9/asyncio/base_events.py in run_until_complete(self, future)
    640             raise RuntimeError('Event loop stopped before Future completed.')
    641
--> 642         return future.result()
    643
    644     def stop(self):

<ipython-input-12-401800b0f64e> in create_poll_and_download()
     11
     12         # download
---> 13         await cl.download_order(order.id)
     14
     15 asyncio.run(create_poll_and_download())

~/code/repos/planet-client-python/planet/clients/orders.py in download_order(self, order_id, directory, overwrite, progress_bar)
    332         """
    333         order = await self.get_order(order_id)
--> 334         locations = order.locations
    335         LOGGER.info(
    336             f'downloading {len(locations)} assets from order {order_id}'

~/code/repos/planet-client-python/planet/models.py in locations(self)
    378         :rtype: list of str
    379         '''
--> 380         return list(r[self.LOCATION_KEY] for r in self.results)
    381
    382     @property

TypeError: 'NoneType' object is not iterable
```

**Summary**
What's happening is that when an order fails, `self.results` will be equal to `None`, which is not iterable, which was inherited from `order.locations` in orders.py (~line 334).

**Proposal** 
1. Change the order in the guide (`docs/guide.md`) with an order that doesn’t fail.
2. Change the property `locations()` within models.py to be protected agains't failed scenarios. 
For example:
```
@property
def locations(self):
    '''Download locations for order results.

    :return: download locations in order
    :rtype: list of str
    '''
    try:
        return list(r[self.LOCATION_KEY] for r in self.results)
    except TypeError:
        return []
```

Changed files:
planet/clients/orders.py, tests/integration/test_orders_api.py

diff --git a/planet/clients/orders.py b/planet/clients/orders.py
index 82da519..5524eb6 100644
--- a/planet/clients/orders.py
+++ b/planet/clients/orders.py
@@ -293,7 +293,6 @@ class OrdersClient():
                 state.
         """
         order = await self.get_order(order_id)
-
         order_state = order['state']
         if not OrderStates.is_final(order_state):
             raise exceptions.ClientError(
@@ -301,7 +300,6 @@ class OrdersClient():
                 f'({order_state}) is not a final state. '
                 'Consider using wait functionality before '
                 'attempting to download.')
-
         locations = self._get_order_locations(order)
         LOGGER.info(
             f'downloading {len(locations)} assets from order {order_id}')
@@ -318,8 +316,14 @@ class OrdersClient():
     @staticmethod
     def _get_order_locations(order):
         links = order['_links']
-        results = links.get('results', None)
-        return list(r['location'] for r in results if r)
+        results = links.get('results', [])
+        try:
+            return list(r['location'] for r in results if r)
+        except TypeError:
+            LOGGER.warning(
+                'order does not have any locations, will not download any ' +
+                'files.')
+            return []
 
     async def wait(self,
                    order_id: str,
diff --git a/tests/integration/test_orders_api.py b/tests/integration/test_orders_api.py
index c542ece..6f52360 100644
--- a/tests/integration/test_orders_api.py
+++ b/tests/integration/test_orders_api.py
@@ -645,6 +645,37 @@ async def test_download_order_state(tmpdir, order_description, oid, session):
         await cl.download_order(oid, directory=str(tmpdir))
 
 
+@respx.mock
+@pytest.mark.asyncio
+async def test_download_order_without_order_links(tmpdir,
+                                                  order_description,
+                                                  oid,
+                                                  session):
+    dl_url1 = TEST_DOWNLOAD_URL + '/1?token=IAmAToken'
+
+    order_description['state'] = 'success'
+    order_description['_links']['results'] = None
+
+    get_url = f'{TEST_ORDERS_URL}/{oid}'
+    mock_resp = httpx.Response(HTTPStatus.OK, json=order_description)
+    respx.get(get_url).return_value = mock_resp
+
+    mock_resp1 = httpx.Response(HTTPStatus.OK,
+                                json={'key': 'value'},
+                                headers={
+                                    'Content-Type':
+                                    'application/json',
+                                    'Content-Disposition':
+                                    'attachment; filename="m1.json"'
+                                })
+    respx.get(dl_url1).return_value = mock_resp1
+
+    client = OrdersClient(session, base_url=TEST_URL)
+
+    filenames = await client.download_order(oid, directory=str(tmpdir))
+    assert len(filenames) == 0
+
+
 @respx.mock
 @pytest.mark.asyncio
 async def test_download_order_overwrite_true_preexisting_data(