Repository: planetlabs/planet-client-python
Pull request #1043 (2024-06-04 15:20:26)

Add hosting parameter support for Orders API
**Related Issue(s):**

Closes #
https://github.com/planetlabs/planet-client-python/issues/1042

**Proposed Changes:**

For inclusion in changelog (if applicable):

1. Hosting parameter can be specified in the SDK / CLI when making Orders API requests. 

Not intended for changelog:

1. 

**Diff of User Interface**

Old behavior:
No support for hosting parameter.

New behavior:
Hosting parameter can be specified in the SDK / CLI when making Orders API requests. 


**PR Checklist:**

- [x] This PR is as small and focused as possible
- [x] If this PR includes proposed changes for inclusion in the changelog, the title of this PR summarizes those changes and is ready for inclusion in the Changelog.
- [x] I have updated docstrings for function changes and docs in the 'docs' folder for user interface / behavior changes
- [x] This PR does not break any examples or I have updated them

**(Optional) @mentions for Notifications:**
@asonnenschein 


Patch:
diff --git a/.gitignore b/.gitignore
index 17f499e..7d69a81 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,4 +26,3 @@ coverage.xml
 .vscode/
 # Docs build
 site
-.venv
diff --git a/CHANGES.txt b/CHANGES.txt
index 2ef22e4..5a65e64 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,8 +1,3 @@
-2.7.1 (2024-05-29)
-
-Fixed:
-- Rearranged arguments in Data API search commands to not break existing scripts  (#1038).
-
 2.7.0 (2024-05-21)
 
 Added:
diff --git a/docs/cli/cli-orders.md b/docs/cli/cli-orders.md
index c9a1e24..ba11f07 100644
--- a/docs/cli/cli-orders.md
+++ b/docs/cli/cli-orders.md
@@ -176,6 +176,26 @@ planet orders request \
 
 *New in version 2.1*
 
+#### Sentinel Hub Hosting
+
+You can deliver your orders directly to Sentinel Hub using the hosting options.
+
+```
+planet orders request \
+    --item-type PSScene \
+    --bundle analytic_sr_udm2 \
+    --name 'My First Order' \
+    20220605_124027_64_242b \
+    --hosting sentinel_hub \
+    --collection_id ba8f7274-aacc-425e-8a38-e21517bfbeff
+```
+
+- The --hosting option is optional and currently supports sentinel_hub as its only value.
+- The --collection_id is also optional. If you decide to use this, ensure that the order request and the collection have matching bands. If you're unsure, allow the system to create a new collection for you by omitting the --collection_id option. This will ensure the newly set-up collection is configured correctly, and you can subsequently add items to this collection as needed.
+
+For more information on Sentinel Hub hosting, see the [Orders API documentation](https://developers.planet.com/apis/orders/delivery/#delivery-to-sentinel-hub-collection) and the [Linking Planet User to Sentinel Hub User
+](https://support.planet.com/hc/en-us/articles/16550358397469-Linking-Planet-User-to-Sentinel-Hub-User) support post.
+
 ### Save an Order Request
 
 The above command just prints out the necessary JSON to create an order. To actually use it you can
@@ -236,6 +256,16 @@ Note the default output will be a bit more 'flat' - if you'd like the above form
 command-line just use `jq` as above: `planet orders create request-1.json | jq` (but remember
 if you run that command again it will create a second order).
 
+#### Sentinel Hub Hosting
+
+For convenience, `planet orders create` accepts the same `--hosting` and `--collection_id` options that [`planet orders request`](#sentinel-hub-hosting) does.
+
+```sh
+planet orders create request-1.json \
+    --hosting sentinel_hub \
+    --collection_id ba8f7274-aacc-425e-8a38-e21517bfbeff
+```
+
 ### Create Request and Order in One Call
 
 Using a unix command called a 'pipe', which looks like `|`, you can skip the step of saving to disk,
diff --git a/docs/cli/cli-subscriptions.md b/docs/cli/cli-subscriptions.md
index af5101f..c13cdb6 100644
--- a/docs/cli/cli-subscriptions.md
+++ b/docs/cli/cli-subscriptions.md
@@ -120,17 +120,6 @@ planet subscriptions create my-subscription.json
 !!!note "Note"
     The above command assumes that you’ve saved the subscriptions JSON as `my-subscription.json` and that you’ve replaced the delivery information with your own bucket and credentials.
 
-#### Create a Subscription with Hosting and Collection ID
-
-In addition to the basic subscription creation process, you can now specify hosting options and a collection ID directly in the create command.
-
-```sh
-planet subscriptions create my-subscription.json --hosting sentinel_hub --collection_id ba8f7274-aacc-425e-8a38-e21517bfbeff
-```
-
-- The --hosting option is optional and currently supports sentinel_hub as its only value.
-- The --collection_id is also optional. If you decide to use this, ensure that the subscription request and the collection have matching bands. If you're unsure, allow the system to create a new collection for you by omitting the --collection_id option. This will ensure the newly set-up collection is configured correctly, and you can subsequently add items to this collection as needed.
-
 ### List Subscriptions
 
 Now that you’ve got a subscription working you can make use of the other commands.
@@ -487,42 +476,36 @@ The main documentation page also has the parameters for Google Cloud, AWS and Or
 
 ### Subscriptions Request
 
-When creating a new subscription, you can include hosting options directly using the --hosting and --collection-id flags.
-
-- The --hosting option is optional and currently supports sentinel_hub as its only value.
-- The --collection_id is also optional. If you decide to use this, ensure that the subscription request and the collection have matching bands. If you're unsure, allow the system to create a new collection for you by omitting the --collection_id option. This will ensure the newly set-up collection is configured correctly, and you can subsequently add items to this collection as needed.
-- You may also input --hosting as a JSON file. The file should be formatted:
-
-```json
-"hosting": {
-  "parameters": {
-    "collection_id": "4bdef85c-3f50-4006-a713-2350da665f80"
-  },
-  "type": "sentinel_hub"
-},
-```
-
 Once you’ve got all your sub-blocks of JSON saved you’re ready to make a complete subscriptions request with the `subscriptions request` command:
 
+The above will print it nicely out so you can see the full request. You can write it out
+as a file, or pipe it directly into `subscriptions create` or `subscriptions update`:
+
 ```sh
 planet subscriptions request \
     --name 'First Subscription' \
     --source request-catalog.json \
     --tools tools.json \
     --delivery cloud-delivery.json \
-    --hosting sentinel_hub \
-    -- collection_id 4bdef85c-3f50-4006-a713-2350da665f80 \
-    --pretty
+    | planet subscriptions create -
 ```
 
-The above will print it nicely out so you can see the full request. You can write it out
-as a file, or pipe it directly into `subscriptions create` or `subscriptions update`:
+#### Sentinel Hub Hosting
+
+When creating a new subscription, you can include hosting options directly using the --hosting and --collection-id flags.
+
+- The --hosting option is optional and currently supports sentinel_hub as its only value.
+- The --collection_id is also optional. If you decide to use this, ensure that the subscription request and the collection have matching bands. If you're unsure, allow the system to create a new collection for you by omitting the --collection_id option. This will ensure the newly set-up collection is configured correctly, and you can subsequently add items to this collection as needed.
+- You may also input --hosting as a JSON file. The file should be formatted:
 
 ```sh
 planet subscriptions request \
     --name 'First Subscription' \
     --source request-catalog.json \
     --tools tools.json \
-    --delivery cloud-delivery.json \
+    --hosting sentinel_hub \
     | planet subscriptions create -
 ```
+
+For more information on Sentinel Hub hosting, see the [Subscriptions API documentation](https://developers.planet.com/docs/subscriptions/delivery/#delivery-to-sentinel-hub-collection) and the [Linking Planet User to Sentinel Hub User
+](https://support.planet.com/hc/en-us/articles/16550358397469-Linking-Planet-User-to-Sentinel-Hub-User) support post.
\ No newline at end of file
diff --git a/planet/cli/orders.py b/planet/cli/orders.py
index 6a6b511..db7734e 100644
--- a/planet/cli/orders.py
+++ b/planet/cli/orders.py
@@ -22,6 +22,7 @@ import planet
 from planet import OrdersClient  # allow mocking
 from . import types
 from .cmds import coro, translate_exceptions
+from ..order_request import sentinel_hub
 from .io import echo_json
 from .options import limit, pretty
 from .session import CliSession
@@ -205,8 +206,20 @@ async def download(ctx, order_id, overwrite, directory, checksum):
 @translate_exceptions
 @coro
 @click.argument("request", type=types.JSON())
+@click.option(
+    "--hosting",
+    type=click.Choice([
+        "sentinel_hub",
+    ]),
+    default=None,
+    help='Hosting type. Currently, only "sentinel_hub" is supported.',
+)
+@click.option("--collection-id",
+              default=None,
+              help='Collection ID for Sentinel Hub hosting. '
+              'If omitted, a new collection will be created.')
 @pretty
-async def create(ctx, request: str, pretty):
+async def create(ctx, request, pretty, **kwargs):
     """Create an order.
 
     This command outputs the created order description, optionally
@@ -214,7 +227,19 @@ async def create(ctx, request: str, pretty):
 
     REQUEST is the full description of the order to be created. It must be JSON
     and can be specified a json string, filename, or '-' for stdin.
+
+    Other flag options are hosting and collection_id. The hosting flag
+    specifies the hosting type, and the collection_id flag specifies the
+    collection ID for Sentinel Hub. If the collection_id is omitted, a new
+    collection will be created.
     """
+
+    hosting = kwargs.get('hosting')
+    collection_id = kwargs.get('collection_id')
+
+    if hosting == "sentinel_hub":
+        request["hosting"] = sentinel_hub(collection_id)
+
     async with orders_client(ctx) as cl:
         order = await cl.create_order(request)
 
@@ -281,6 +306,13 @@ async def create(ctx, request: str, pretty):
     help="""Include or exclude metadata in SpatioTemporal Asset Catalog (STAC)
     format. Not specifying either defaults to including it (--stac), except
     for orders with google_earth_engine delivery""")
+@click.option('--hosting',
+              type=click.Choice(['sentinel_hub']),
+              help='Hosting for data delivery. '
+              'Currently, only "sentinel_hub" is supported.')
+@click.option('--collection_id',
+              help='Collection ID for Sentinel Hub hosting. '
+              'If omitted, a new collection will be created.')
 @pretty
 async def request(ctx,
                   item_type,
@@ -295,6 +327,8 @@ async def request(ctx,
                   single_archive,
                   delivery,
                   stac,
+                  hosting,
+                  collection_id,
                   pretty):
     """Generate an order request.
 
@@ -337,6 +371,8 @@ async def request(ctx,
                                                  delivery=delivery,
                                                  notifications=notifications,
                                                  tools=tools,
-                                                 stac=stac_json)
+                                                 stac=stac_json,
+                                                 hosting=hosting,
+                                                 collection_id=collection_id)
 
     echo_json(request, pretty)
diff --git a/planet/cli/subscriptions.py b/planet/cli/subscriptions.py
index 01eec56..f22f11f 100644
--- a/planet/cli/subscriptions.py
+++ b/planet/cli/subscriptions.py
@@ -101,7 +101,7 @@ async def list_subscriptions_cmd(ctx, status, limit, pretty):
 )
 @click.option("--collection-id",
               default=None,
-              help='Collection ID for Sentinel Hub.'
+              help='Collection ID for Sentinel Hub hosting. '
               'If omitted, a new collection will be created.')
 @pretty
 @click.pass_context
@@ -302,7 +302,7 @@ async def list_subscription_results_cmd(ctx,
     help="Clip to the source geometry without specifying a clip tool.")
 @click.option("--collection-id",
               default=None,
-              help='Collection ID for Sentinel Hub.'
+              help='Collection ID for Sentinel Hub hosting. '
               'If omitted, a new collection will be created.')
 @pretty
 def request(name,
diff --git a/planet/clients/data.py b/planet/clients/data.py
index 364be58..6389887 100644
--- a/planet/clients/data.py
+++ b/planet/clients/data.py
@@ -113,11 +113,11 @@ class DataClient:
 
     async def search(self,
                      item_types: List[str],
+                     geometry: Optional[dict] = None,
                      search_filter: Optional[dict] = None,
                      name: Optional[str] = None,
                      sort: Optional[str] = None,
-                     limit: int = 100,
-                     geometry: Optional[dict] = None) -> AsyncIterator[dict]:
+                     limit: int = 100) -> AsyncIterator[dict]:
         """Iterate over results from a quick search.
 
         Quick searches are saved for a short period of time (~month). The
@@ -136,10 +136,10 @@ class DataClient:
             sort: Field and direction to order results by. Valid options are
             given in SEARCH_SORT.
             name: The name of the saved search.
-            limit: Maximum number of results to return. When set to 0, no
-                maximum is applied.
             geometry: GeoJSON, a feature reference or a list of feature
                 references
+            limit: Maximum number of results to return. When set to 0, no
+                maximum is applied.
 
         Yields:
             Description of an item.
@@ -173,14 +173,12 @@ class DataClient:
         async for i in Items(response, self._session.request, limit=limit):
             yield i
 
-    async def create_search(
-        self,
-        item_types: List[str],
-        search_filter: dict,
-        name: str,
-        enable_email: bool = False,
-        geometry: Optional[dict] = None,
-    ) -> dict:
+    async def create_search(self,
+                            item_types: List[str],
+                            search_filter: dict,
+                            name: str,
+                            geometry: Optional[dict] = None,
+                            enable_email: bool = False) -> dict:
         """Create a new saved structured item search.
 
         To filter to items you have access to download which are of standard
@@ -201,10 +199,10 @@ class DataClient:
 
         Parameters:
             item_types: The item types to include in the search.
+            geometry: A feature reference or a GeoJSON
             search_filter: Structured search criteria.
             name: The name of the saved search.
             enable_email: Send a daily email when new results are added.
-            geometry: A feature reference or a GeoJSON
 
         Returns:
             Description of the saved search.
@@ -235,17 +233,17 @@ class DataClient:
                             item_types: List[str],
                             search_filter: dict,
                             name: str,
-                            enable_email: bool = False,
-                            geometry: Optional[dict] = None) -> dict:
+                            geometry: Optional[dict] = None,
+                            enable_email: bool = False) -> dict:
         """Update an existing saved search.
 
         Parameters:
             search_id: Saved search identifier.
             item_types: The item types to include in the search.
+            geometry: A feature reference or a GeoJSON
             search_filter: Structured search criteria.
             name: The name of the saved search.
             enable_email: Send a daily email when new results are added.
-            geometry: A feature reference or a GeoJSON
 
         Returns:
             Description of the saved search.
diff --git a/planet/order_request.py b/planet/order_request.py
index 074d7bd..ad2166f 100644
--- a/planet/order_request.py
+++ b/planet/order_request.py
@@ -30,7 +30,9 @@ def build_request(name: str,
                   notifications: Optional[dict] = None,
                   order_type: Optional[str] = None,
                   tools: Optional[List[dict]] = None,
-                  stac: Optional[dict] = None) -> dict:
+                  stac: Optional[dict] = None,
+                  hosting: Optional[str] = None,
+                  collection_id: Optional[str] = None) -> dict:
     """Prepare an order request.
 
     ```python
@@ -65,6 +67,9 @@ def build_request(name: str,
         order_type: Accept a partial order, indicated by 'partial'.
         tools: Tools to apply to the products. Order defines
             the toolchain order of operatations.
+        stac: Include STAC metadata.
+        hosting: A hosting destination (e.g. Sentinel Hub).
+        collection_id: A Sentinel Hub collection ID.
 
     Raises:
         planet.specs.SpecificationException: If order_type is not a valid
@@ -91,6 +96,9 @@ def build_request(name: str,
     if stac:
         details['metadata'] = stac
 
+    if hosting == 'sentinel_hub':
+        details['hosting'] = sentinel_hub(collection_id)
+
     return details
 
 
@@ -534,3 +542,11 @@ def band_math_tool(b1: str,
     # e.g. {"b1": "b1", "b2":"arctan(b1)"} if b1 and b2 are specified
     parameters = dict((k, v) for k, v in locals().items() if v)
     return _tool('bandmath', parameters)
+
+
+def sentinel_hub(collection_id: Optional[str] = None) -> dict:
+    """Specify a Sentinel Hub hosting destination."""
+    params = {}
+    if collection_id:
+        params['collection_id'] = collection_id
+    return {'sentinel_hub': params}
diff --git a/planet/subscription_request.py b/planet/subscription_request.py
index 5d773ca..c7a7381 100644
--- a/planet/subscription_request.py
+++ b/planet/subscription_request.py
@@ -86,9 +86,8 @@ def build_request(name: str,
         ClientError: when a valid Subscriptions API request can't be
             constructed.
 
-    Examples:
+    Example::
 
-    ```python
         from datetime import datetime
         from planet.subscription_request import build_request, catalog_source, amazon_s3
 
@@ -114,7 +113,6 @@ def build_request(name: str,
         subscription_request = build_request(
             "test_subscription", source=source, delivery=delivery, hosting=hosting
         )
-        ```
     """
     # Because source is a Mapping we must make copies for
     # the function's return value. dict() shallow copies a Mapping


Test patch:
diff --git a/tests/integration/test_data_api.py b/tests/integration/test_data_api.py
index ddf7c23..a98e0dc 100644
--- a/tests/integration/test_data_api.py
+++ b/tests/integration/test_data_api.py
@@ -211,8 +211,7 @@ async def test_search_filter(item_descriptions,
 
     cl = DataClient(session, base_url=TEST_URL)
     items_list = [
-        i async for i in cl.search(item_types=['PSScene'],
-                                   search_filter=search_filter)
+        i async for i in cl.search(['PSScene'], search_filter=search_filter)
     ]
 
     # check that request is correct
@@ -224,35 +223,6 @@ async def test_search_filter(item_descriptions,
     assert items_list == item_descriptions
 
 
-@respx.mock
-@pytest.mark.anyio
-async def test_search_filter_positional_args(item_descriptions,
-                                             search_filter,
-                                             search_response,
-                                             session):
-    """test the search method using positional args"""
-
-    quick_search_url = f'{TEST_URL}/quick-search'
-
-    item1, item2, item3 = item_descriptions
-    response = {"features": [item1, item2, item3]}
-    mock_resp = httpx.Response(HTTPStatus.OK, json=response)
-    respx.post(quick_search_url).return_value = mock_resp
-
-    cl = DataClient(session, base_url=TEST_URL)
-
-    # search using a positional arg for the filter.
-    items_list = [i async for i in cl.search(['PSScene'], search_filter)]
-
-    # check that request is correct
-    expected_request = {"item_types": ["PSScene"], "filter": search_filter}
-    actual_body = json.loads(respx.calls[0].request.content)
-    assert actual_body == expected_request
-
-    # check that all of the items were returned unchanged
-    assert items_list == item_descriptions
-
-
 @respx.mock
 @pytest.mark.anyio
 async def test_search_sort(item_descriptions,
@@ -325,7 +295,7 @@ async def test_create_search_basic(search_filter, session):
     respx.post(TEST_SEARCHES_URL).return_value = mock_resp
 
     cl = DataClient(session, base_url=TEST_URL)
-    search = await cl.create_search(item_types=['PSScene'],
+    search = await cl.create_search(['PSScene'],
                                     search_filter=search_filter,
                                     name='test')
 
@@ -343,43 +313,6 @@ async def test_create_search_basic(search_filter, session):
     assert search == page_response
 
 
-@respx.mock
-@pytest.mark.anyio
-async def test_create_search_basic_positional_args(search_filter, session):
-    """Test that positional arguments are accepted for create_search"""
-
-    page_response = {
-        "__daily_email_enabled": False,
-        "_links": {
-            "_self": "string", "thumbnail": "string"
-        },
-        "created": "2019-08-24T14:15:22Z",
-        "filter": search_filter,
-        "id": "string",
-        "last_executed": "2019-08-24T14:15:22Z",
-        "name": "test",
-        "updated": "2019-08-24T14:15:22Z"
-    }
-    mock_resp = httpx.Response(HTTPStatus.OK, json=page_response)
-    respx.post(TEST_SEARCHES_URL).return_value = mock_resp
-
-    cl = DataClient(session, base_url=TEST_URL)
-    search = await cl.create_search(['PSScene'], search_filter, name='test')
-
-    # check that request is correct
-    expected_request = {
-        "item_types": ["PSScene"],
-        "filter": search_filter,
-        "name": "test",
-        "__daily_email_enabled": False
-    }
-    actual_body = json.loads(respx.calls[0].request.content)
-    assert actual_body == expected_request
-
-    # check the response is returned unaltered
-    assert search == page_response
-
-
 @respx.mock
 @pytest.mark.anyio
 async def test_create_search_email(search_filter, session):
@@ -487,46 +420,6 @@ async def test_update_search_basic(search_filter, session):
     assert search == page_response
 
 
-@respx.mock
-@pytest.mark.anyio
-async def test_update_search_basic_positional_args(search_filter, session):
-    """Test that positional arguments are accepted for update_search"""
-
-    page_response = {
-        "__daily_email_enabled": False,
-        "_links": {
-            "_self": "string", "thumbnail": "string"
-        },
-        "created": "2019-08-24T14:15:22Z",
-        "filter": search_filter,
-        "id": VALID_SEARCH_ID,
-        "last_executed": "2019-08-24T14:15:22Z",
-        "name": "test",
-        "updated": "2019-08-24T14:15:22Z"
-    }
-    mock_resp = httpx.Response(HTTPStatus.OK, json=page_response)
-    respx.put(
-        f'{TEST_SEARCHES_URL}/{VALID_SEARCH_ID}').return_value = mock_resp
-
-    cl = DataClient(session, base_url=TEST_URL)
-    search = await cl.update_search(VALID_SEARCH_ID, ['PSScene'],
-                                    search_filter,
-                                    name='test')
-
-    # check that request is correct
-    expected_request = {
-        "item_types": ["PSScene"],
-        "filter": search_filter,
-        "name": "test",
-        "__daily_email_enabled": False
-    }
-    actual_body = json.loads(respx.calls[0].request.content)
-    assert actual_body == expected_request
-
-    # check the response is returned unaltered
-    assert search == page_response
-
-
 @respx.mock
 @pytest.mark.anyio
 @pytest.mark.parametrize("limit, expected_list_length", [(None, 4), (3, 3)])
diff --git a/tests/integration/test_orders_cli.py b/tests/integration/test_orders_cli.py
index 7bc057b..c873877 100644
--- a/tests/integration/test_orders_cli.py
+++ b/tests/integration/test_orders_cli.py
@@ -768,3 +768,65 @@ def test_cli_orders_request_no_stac(invoke):
         }]
     }
     assert order_request == json.loads(result.output)
+
+
+@respx.mock
+def test_cli_orders_request_hosting_sentinel_hub(invoke, stac_json):
+
+    result = invoke([
+        'request',
+        '--item-type=PSScene',
+        '--bundle=visual',
+        '--name=test',
+        '20220325_131639_20_2402',
+        '--hosting=sentinel_hub',
+    ])
+
+    order_request = {
+        "name":
+        "test",
+        "products": [{
+            "item_ids": ["20220325_131639_20_2402"],
+            "item_type": "PSScene",
+            "product_bundle": "visual",
+        }],
+        "metadata":
+        stac_json,
+        "hosting": {
+            "sentinel_hub": {}
+        }
+    }
+    assert order_request == json.loads(result.output)
+
+
+@respx.mock
+def test_cli_orders_request_hosting_sentinel_hub_collection_id(
+        invoke, stac_json):
+
+    result = invoke([
+        'request',
+        '--item-type=PSScene',
+        '--bundle=visual',
+        '--name=test',
+        '20220325_131639_20_2402',
+        '--hosting=sentinel_hub',
+        '--collection_id=1234'
+    ])
+
+    order_request = {
+        "name":
+        "test",
+        "products": [{
+            "item_ids": ["20220325_131639_20_2402"],
+            "item_type": "PSScene",
+            "product_bundle": "visual",
+        }],
+        "metadata":
+        stac_json,
+        "hosting": {
+            "sentinel_hub": {
+                "collection_id": "1234"
+            }
+        }
+    }
+    assert order_request == json.loads(result.output)
diff --git a/tests/unit/test_order_request.py b/tests/unit/test_order_request.py
index 46a01c4..b3f0f8a 100644
--- a/tests/unit/test_order_request.py
+++ b/tests/unit/test_order_request.py
@@ -329,3 +329,15 @@ def test_no_archive_items_without_type():
     assert "archive_type" not in delivery_config
     assert "archive_filename" not in delivery_config
     assert "single_archive" not in delivery_config
+
+
+def test_sentinel_hub():
+    sh_config = order_request.sentinel_hub()
+    expected = {'sentinel_hub': {}}
+    assert sh_config == expected
+
+
+def test_sentinel_hub_collection_id():
+    sh_config = order_request.sentinel_hub("1234")
+    expected = {'sentinel_hub': {'collection_id': "1234"}}
+    assert sh_config == expected