Repository: planetlabs/planet-client-python
Issue: Allow clipping to MultiPolygon

**Is your feature request related to a problem? Please describe.**

I would like to clip an order to a `MultiPolygon` area of interest, but `planet.order_client.clip_tool(aoi)` raises if `aoi` is a `MultiPolygon`.

```
aoi = {
    'type': 'MultiPolygon',
        'coordinates': [
            [[[-117.62, 45.22], [-117.61, 45.22], [-117.61, 45.21], [-117.62, 45.21], [-117.62, 45.22]]],
            [[[-117.63, 45.24], [-117.62, 45.24], [-117.62, 45.23], [-117.63, 45.23], [-117.63, 45.24]]]
        ]
}
clip = planet.order_request.clip_tool(aoi)

# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
#   File "/opt/homebrew/Caskroom/miniconda/base/envs/torch/lib/python3.10/site-packages/planet/order_request.py", line 336, in clip_tool
#     parameters = {'aoi': geojson.as_polygon(aoi)}
#   File "/opt/homebrew/Caskroom/miniconda/base/envs/torch/lib/python3.10/site-packages/planet/geojson.py", line 54, in as_polygon
#     raise GeoJSONError(
# planet.exceptions.GeoJSONError: Invalid geometry type: MultiPolygon is not Polygon.
```

**Describe the solution you'd like**

`planet.order_client.clip_tool()` should accept a `MultiPolygon` aoi.

**Describe alternatives you've considered**

It works if I manually create the clip tool dict using a MultiPolygon AOI e.g.

```
clip = {'clip': {'aoi': aoi}}
# Works fine to clip an order
```

**Additional context**



Changed files:
tests/unit/test_order_request.py, tests/integration/test_orders_cli.py, tests/conftest.py, planet/order_request.py, planet/cli/orders.py

diff --git a/planet/cli/orders.py b/planet/cli/orders.py
index a5ad766..a8fede5 100644
--- a/planet/cli/orders.py
+++ b/planet/cli/orders.py
@@ -239,8 +239,8 @@ async def create(ctx, request: str, pretty):
               type=click.STRING)
 @click.option('--clip',
               type=types.JSON(),
-              help="""Clip feature GeoJSON. Can be a json string, filename,
-              or '-' for stdin.""")
+              help="""Clip feature Polygon or Multipolygon GeoJSON. Can be a
+              json string, filename, or '-' for stdin.""")
 @click.option(
     '--tools',
     type=types.JSON(),
@@ -295,12 +295,10 @@ async def request(ctx,
         raise click.BadParameter("Specify only one of '--clip' or '--tools'")
     elif clip:
         try:
-            clip = planet.geojson.as_polygon(clip)
-        except planet.exceptions.GeoJSONError as e:
+            tools = [planet.order_request.clip_tool(clip)]
+        except planet.exceptions.ClientError as e:
             raise click.BadParameter(e)
 
-        tools = [planet.order_request.clip_tool(clip)]
-
     if cloudconfig:
         delivery = planet.order_request.delivery(cloud_config=cloudconfig)
     else:
diff --git a/planet/order_request.py b/planet/order_request.py
index 586c44f..fb9b388 100644
--- a/planet/order_request.py
+++ b/planet/order_request.py
@@ -18,6 +18,7 @@ import logging
 from typing import Optional, Any, Dict, List
 
 from . import geojson, specs
+from .exceptions import ClientError
 
 LOGGER = logging.getLogger(__name__)
 
@@ -328,13 +329,19 @@ def clip_tool(aoi: dict) -> dict:
         ```
 
     Parameters:
-        aoi: clip GeoJSON.
+        aoi: clip GeoJSON, either Polygon or Multipolygon.
 
     Raises:
-        geojson.GeoJSONException: If GeoJSON is not a valid polygon.
+        geojson.GeoJSONException: If GeoJSON is not a valid polygon or
+            multipolygon.
     '''
-    parameters = {'aoi': geojson.as_polygon(aoi)}
-    return _tool('clip', parameters)
+    valid_types = ['Polygon', 'MultiPolygon']
+
+    geom = geojson.as_geom(aoi)
+    if geom['type'].lower() not in [v.lower() for v in valid_types]:
+        raise ClientError(
+            f'Invalid geometry type: {geom["type"]} is not in {valid_types}.')
+    return _tool('clip', {'aoi': geom})
 
 
 def composite_tool() -> dict:
diff --git a/tests/conftest.py b/tests/conftest.py
index 1f248d7..6fea212 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -133,3 +133,17 @@ def point_geom_geojson():
         "type": "Point",
         "coordinates": [37.791595458984375, 14.84923123791421]
     }
+
+
+@pytest.fixture
+def multipolygon_geom_geojson():
+    return {
+        "type":
+        "MultiPolygon",
+        "coordinates":
+        [[[[37.791595458984375, 14.84923123791421],
+          [37.90214538574219, 14.84923123791421],
+          [37.90214538574219, 14.945448293647944],
+          [37.791595458984375, 14.945448293647944],
+          [37.791595458984375, 14.84923123791421]]]]
+    }  # yapf: disable
diff --git a/tests/integration/test_orders_cli.py b/tests/integration/test_orders_cli.py
index 4cfda87..c6e52bd 100644
--- a/tests/integration/test_orders_cli.py
+++ b/tests/integration/test_orders_cli.py
@@ -530,7 +530,7 @@ def test_cli_orders_request_id_empty(invoke):
 @pytest.mark.parametrize("geom_fixture",
                          [('geom_geojson'), ('feature_geojson'),
                           ('featurecollection_geojson')])
-def test_cli_orders_request_clip_success(geom_fixture,
+def test_cli_orders_request_clip_polygon(geom_fixture,
                                          request,
                                          invoke,
                                          geom_geojson,
@@ -567,6 +567,40 @@ def test_cli_orders_request_clip_success(geom_fixture,
     assert order_request == json.loads(result.output)
 
 
+def test_cli_orders_request_clip_multipolygon(multipolygon_geom_geojson,
+                                              invoke,
+                                              geom_geojson,
+                                              stac_json):
+
+    result = invoke([
+        'request',
+        '--item-type=PSOrthoTile',
+        '--bundle=analytic',
+        '--name=test',
+        '4500474_2133707_2021-05-20_2419',
+        f'--clip={json.dumps(multipolygon_geom_geojson)}',
+    ])
+    assert result.exit_code == 0
+
+    order_request = {
+        "name":
+        "test",
+        "products": [{
+            "item_ids": ["4500474_2133707_2021-05-20_2419"],
+            "item_type": "PSOrthoTile",
+            "product_bundle": "analytic",
+        }],
+        "tools": [{
+            'clip': {
+                'aoi': multipolygon_geom_geojson
+            }
+        }],
+        "metadata":
+        stac_json
+    }
+    assert order_request == json.loads(result.output)
+
+
 def test_cli_orders_request_clip_invalid_geometry(invoke, point_geom_geojson):
     result = invoke([
         'request',
@@ -577,9 +611,6 @@ def test_cli_orders_request_clip_invalid_geometry(invoke, point_geom_geojson):
         f'--clip={json.dumps(point_geom_geojson)}'
     ])
     assert result.exit_code == 2
-    error_msg = ('Error: Invalid value: Invalid geometry type: ' +
-                 'Point is not Polygon.')
-    assert error_msg in result.output
 
 
 def test_cli_orders_request_both_clip_and_tools(invoke, geom_geojson):
diff --git a/tests/unit/test_order_request.py b/tests/unit/test_order_request.py
index 49e3554..218d398 100644
--- a/tests/unit/test_order_request.py
+++ b/tests/unit/test_order_request.py
@@ -222,13 +222,23 @@ def test__tool():
         _ = order_request._tool('notsupported', 'jsonstring')
 
 
-def test_clip_tool(geom_geojson, point_geom_geojson):
+def test_clip_tool_polygon(geom_geojson):
     ct = order_request.clip_tool(geom_geojson)
     expected = {'clip': {'aoi': geom_geojson}}
     assert ct == expected
 
-    with pytest.raises(exceptions.GeoJSONError):
-        _ = order_request.clip_tool(point_geom_geojson)
+
+def test_clip_tool_multipolygon(multipolygon_geom_geojson):
+    ct = order_request.clip_tool(multipolygon_geom_geojson)
+    expected = {'clip': {'aoi': multipolygon_geom_geojson}}
+    assert ct == expected
+
+
+def test_clip_tool_invalid(point_geom_geojson):
+    '''Confirm an exception is raised if an invalid geometry type is supplied.
+    '''
+    with pytest.raises(exceptions.ClientError):
+        order_request.clip_tool(point_geom_geojson)
 
 
 def test_reproject_tool():