Repository: planetlabs/planet-client-python

Issue:
Don't require a filter for a quick search.
To solve the issues below, do the following:
1. In quick search, make filter a named option (`--filter` for CLI, `search_filter` for api), if not supplied then no filtering will take place
2. Change `planet data filter` to output an empty filter (no filtering) by default so that `planet data filter | planet search PSScene --filter -` has the same behavior as `planet search PSScene` - this resolves #704 

---

If I try to do a quick search with no filter I get:

```
% planet data search-quick PSScene                                                              

Usage: planet data search-quick [OPTIONS] ITEM_TYPES FILTER

Try 'planet data search-quick --help' for help.

Error: Missing argument 'FILTER'.
```

In v1 doing a search without a filter would just show you the latest data. 

Right now you can do `planet data filter | planet data search-quick SkysatCollect -`, since filter outputs `{"type": "PermissionFilter", "config": ["assets:download"]}` by default.

But then if you didn't want to only search downloadable assets you could do:

`planet data filter --permission false | planet data search-quick SkysatCollect -` and then you get a failure, since the latter doesn't actually output a filter, it emits nothing.



Discussion:

Patch:
diff --git a/docs/cli/cli-data.md b/docs/cli/cli-data.md
index 316927e..a000342 100644
--- a/docs/cli/cli-data.md
+++ b/docs/cli/cli-data.md
@@ -17,10 +17,10 @@ At this point you should have completed [Step 5](../get-started/quick-start-guid
 of the quick start guide, and run your first full data search command:
 
 ```
-planet data search PSScene filter.json > recent-psscene.json
+planet data search PSScene --filter filter.json > recent-psscene.json
 ```
 
-This saves the latest 100 scenes in a file, that you can open and look at.
+This saves the descriptions of the latest 100 standard-quality scenes you have permissions to download in a file, that you can open and look at.
 
 ### Pretty printing
 
@@ -28,7 +28,7 @@ You will likely notice that this file is quite wide, with one very long line for
 item returned. You can make for a more readable file by using the `--pretty` flag:
 
 ```
-planet data search --pretty PSScene filter.json > recent-psscene.json
+planet data search --pretty PSScene --filter filter.json > recent-psscene.json
 ```
 
 The `--pretty` flag is built into most of the CLI calls. But you can also achieve the
@@ -38,7 +38,7 @@ piping any JSON output through it prints it in a more readable form. So the foll
 command will do the same thing as the previous one:
 
 ```
-planet data search PSScene filter.json | jq > recent-psscene.json
+planet data search PSScene --filter filter.json | jq > recent-psscene.json
 ```
 
 You can read a bit [more about jq]((cli-intro.md#jq) in the CLI intro.
@@ -49,14 +49,14 @@ You also don't have to save the output to a file. If you don't redirect it into
 it will just print out on the console.
 
 ```
-planet data search PSScene filter.json 
+planet data search PSScene --filter filter.json
 ```
 
 If you enter this command you'll see the output stream by. Here you can use jq again, and
 it'll often give you nice syntax highlighting in addition to formatting.
 
 ```
-planet data search PSScene filter.json | jq
+planet data search PSScene --filter filter.json | jq
 ```
 
 ### Create filter and search in one call
@@ -66,7 +66,7 @@ passing the output of the `data filter` command directly to be the input of the
 command:
 
 ```
-planet data filter | planet data search --pretty PSScene -
+planet data filter --permission --std-quality | planet data search --pretty PSScene --filter -
 ```
 
 Note the dash (`-`), which explicitly tells the CLI to use the output from the call that is piped into it.
@@ -74,6 +74,17 @@ Note the dash (`-`), which explicitly tells the CLI to use the output from the c
 You can learn more about the pipe command, as well as the `>` command above in the 
 [Piping & redirection section](cli-intro.md#piping-redirection) of the CLI Introduction.
 
+### Search without filtering
+
+If no filtering is required, the search command can be called directly:
+
+```
+planet data search PSScene
+```
+
+This outputs the last 100 scenes.
+
+
 ### Search on Item Type
 
 These first searches were done on the [PSScene](https://developers.planet.com/docs/data/psscene/) 'item type', but you
@@ -82,7 +93,7 @@ its catalog. The item type is the first argument of the `search` command, follow
 you can specify any number of item types here:
 
 ```
-planet data filter | planet data search PSScene,Sentinel2L1C,Landsat8L1G,SkySatCollect -
+planet data search PSScene,Sentinel2L1C,Landsat8L1G,SkySatCollect
 ```
 
 This will search for all the most recent images captured by PlanetScope, SkySat, Sentinel 2 and Landsat 8 satellites. 
@@ -96,7 +107,7 @@ By default the `search` command returns only the 100 first scenes. But with the
 under the hood will automatically page through all the results from the API. 
 
 ```
-planet data filter | planet data search --limit 3000 PSScene
+planet data search --limit 3000 PSScene
 ```
 
 Note you can also do a call with no limits if you set the limit to `0`. Though don't use this haphazardly, or you'll be
@@ -111,13 +122,13 @@ the `planet collect` method to transform the output from the Data API to valid G
 output to it:
 
 ```console
-planet data filter | planet data search PSScene - | planet collect -
+planet data search PSScene | planet collect -
 ```
 
 If you want to visualize this you can save it as a file:
 
 ```console
-planet data filter | planet data search PSScene - | planet collect - > planet-search.geojson
+planet data search PSScene | planet collect - > planet-search.geojson
 ```
 
 This you can then open with your favorite GIS program, or see this 
@@ -138,13 +149,13 @@ descending order. The options are are:
 The lets you do things like get the id of the most recent skysat image taken (that you have download access to):
 
 ```console
-planet data filter | planet data search SkySatCollect --sort 'acquired desc' --limit 1 - 
+planet data search SkySatCollect --sort 'acquired desc' --limit 1
 ```
 
 And you can also just get the ID, using `jq`
 
 ```console
-planet data filter | planet data search SkySatCollect --sort 'acquired desc' --limit 1 - | jq -r .id
+planet data search SkySatCollect --sort 'acquired desc' --limit 1 - | jq -r .id
 ```
 
 
@@ -202,14 +213,14 @@ of Iowa. You can copy it and save as a file called `geometry.geojson`
 And then run it with this command:
 
 ```console
-planet data filter --geom geometry.geojson | planet data search PSScene -
+planet data filter --geom geometry.geojson | planet data search PSScene --filter -
 ```
 
 Note that by default all searches with the command-line return 100 results, but you can easily increase that with
 the `--limit` flag:
 
 ```console
-planet data filter --geom geometry.geojson | planet data search --limit 500 PSScene -
+planet data filter --geom geometry.geojson | planet data search --limit 500 PSScene --filter -
 ```
 
 Creating geometries for search can be annoying in a command-line workflow, but there are some ideas in the
@@ -220,7 +231,7 @@ Creating geometries for search can be annoying in a command-line workflow, but t
 Some of the most common filtering is by date. You could get all imagery acquired before August 2021:
 
 ```console
-planet data filter --date-range acquired lt 2021-08-01 | planet data search PSScene -
+planet data filter --date-range acquired lt 2021-08-01 | planet data search PSScene --filter -
 ```
 
 The 'operator' in this case is 'less than' (`lt`). The options are:
@@ -236,7 +247,7 @@ do a search for all images in July of 2021:
 
 ```console
 planet data filter --date-range acquired gte 2021-07-01 --date-range acquired lt 2021-08-01 | \
-planet data search PSScene -
+planet data search PSScene --filter -
 ```
 
 The date input understands [RFC 3339](https://datatracker.ietf.org/doc/html/rfc3339) and 
@@ -246,7 +257,7 @@ on July 1st 2021:
 
 ```console
 planet data filter --date-range acquired gte 2021-07-01:06:20:10 --date-range acquired lt 2021-07-01:06:20:15 | \
-planet data search PSScene - 
+planet data search PSScene --filter -
 ```
 
 ### Range Filter
@@ -255,7 +266,7 @@ The range filter uses the same operators as the date filter, but works against a
 of these tend to be ones about cloudy pixels. For example you can search for data with clear pixels greater than 90%:
 
 ```console
-planet data filter --range clear_percent gt 90
+planet data filter --range clear_percent gt 90 | planet data search PSScene --filter -
 ```
 
 ### String-In Filter
@@ -264,19 +275,20 @@ For properties that are strings you can use the `string-in` filter. For example
 with PS2 instrument:
 
 ```console
-planet data filter --string-in instrument PS2 | planet data search PSScene -
+planet data filter --string-in instrument PS2 | planet data search PSScene --filter -
 ```
 
 You can specify multiple strings to match, with a comma:
 
 ```console
-planet data filter --string-in instrument PS2,PSB.SD | planet data search PSScene -
+planet data filter --string-in instrument PS2,PSB.SD | planet data search PSScene --filter -
+
 ```
 
 Another example is to select all data in a single strip:
 
 ```console
-planet data filter --string-in strip_id 5743640 | planet data search PSScene -
+planet data filter --string-in strip_id 5743640 | planet data search PSScene --filter -
 ```
 
 Note that in all these commands we are piping the results into the search. If you don't include the pipe then you'll
@@ -287,13 +299,13 @@ get the filter output, which can be interested to inspect to see exactly what is
 You can limit your search to only data with a particular asset, for example search just for 8-band analytic assets:
 
 ```console
-planet data filter --asset ortho_analytic_8b_sr | planet data search PSScene -
+planet data filter --asset ortho_analytic_8b_sr | planet data search PSScene --filter -
 ```
 
 Or 8-band assets that also have a UDM.
 
 ```console
-planet data filter --asset ortho_analytic_8b_sr --asset udm2 | planet data search PSScene -
+planet data filter --asset ortho_analytic_8b_sr --asset udm2 | planet data search PSScene --filter -
 ```
 
 You can find the list of available assets in each Item Type Page, like 
@@ -306,14 +318,14 @@ sure you got the asset right, and it's valid for the item-types you're searching
 
 ### Permission Filter
 
-The 'permission filter' is set to true by default, since most people want to search only for data they have access to
-and are able to download. But if you'd like to just get search Planet's catalog and get a sense of what is out there
-you can set the permission filter to false:
+By default, no search filters are applied. However, many people want to search only for data they have access to download
+that are of standard (aka not test) quality. Therefore, these filters can be easily added with the `--permission` and
+`--std-quality` flags. To use the permission and standard quality filters:
 
 ```console
-planet data filter --permission false --asset ortho_analytic_8b_sr | planet data search PSScene -
+planet data filter --permission --std-quality --asset ortho_analytic_8b_sr | planet data search PSScene --filter -
 ```
 
 ## Stats
 
-TODO
\ No newline at end of file
+TODO
diff --git a/docs/cli/cli-intro.md b/docs/cli/cli-intro.md
index 7301208..6100876 100644
--- a/docs/cli/cli-intro.md
+++ b/docs/cli/cli-intro.md
@@ -84,14 +84,13 @@ one command to be the input for the next one. So instead of having to save to a
 then referring to it you can just do it all in one call:
 
 ```
-planet data filter --range cloud_percent lt 10 | planet data search-quick PSScene -
+planet data filter --range cloud_percent lt 10 | planet data search PSScene --filter -
 ```
 
 The pipe says to take the output of the first command and pass it to the input of 
 the second. You'll notice that the planet command has a dash (`-`), this is a convention
 that is often used by different CLI programs to explicitly say 'read from
-standard out'. Most Planet CLI commands require it, but one or two will implicitly
-read from standard out if it's not explicitly included. Using the dash to mean
+standard out'. Using the dash to mean
 'read from standard out' is a general convention used by many programs, but it's 
 not universal, so check the docs of the program you're using as to how it reads 
 from piped input. For example GDAL/OGR uses a specific `/vsistdin/` convention to 
@@ -174,7 +173,7 @@ represent GeoJSON features, the JSON blob is a GeoJSON FeatureCollection.
 Otherwise, the JSON blob is a list of the individual results.
 
 ```console
-$ planet data filter | planet data search PSScene - | planet collect -
+$ planet data search PSScene | planet collect -
 ```
 
 This gives you a fully compliant GeoJSON FeatureCollection, which is 
diff --git a/docs/cli/cli-tips-tricks.md b/docs/cli/cli-tips-tricks.md
index 050e6b8..4da63c3 100644
--- a/docs/cli/cli-tips-tricks.md
+++ b/docs/cli/cli-tips-tricks.md
@@ -53,7 +53,7 @@ JSON from the text box:
 On a mac you can use `pbpaste` to grab whatever is currently in your clipboard:
 
 ```console
-pbpaste | planet data filter --geom -  | planet data search SkySatCollect -
+pbpaste | planet data filter --geom -  | planet data search SkySatCollect --filter -
 ```
 
 (TODO: Find alternatives for windows and linux)
@@ -94,7 +94,7 @@ Or also on Placemark, which tends to perform a bit better (especially when you g
 For both it's recommended to pass the output through `planet collect` to get properly formatted GeoJSON:
 
 ```console
-planet data filter --string-in strip_id 5743669 | planet data search PSScene - | planet collect - | pbcopy
+planet data filter --string-in strip_id 5743669 | planet data search PSScene --filter - | planet collect - | pbcopy
 ```
 
 (TODO: Get pbcopy equivalents for windows and linux)
@@ -108,7 +108,7 @@ The following command will get the latest SkySat image captured, upload to githu
 your browser to see it:
 
 ```console
-planet data filter | planet data search SkySatCollect - --sort 'acquired desc' --limit 1 \
+planet data search SkySatCollect --sort 'acquired desc' --limit 1 \
 | planet collect - | jq | gh gist create -f latest-skysat.geojson -w
 ```
 
@@ -116,7 +116,7 @@ Or you can show all ps-scenes in a strip on github gist.
 (You may need to reload the page, for some reason it doesn't always showing up immediately after open)
 
 ```console
-planet data filter --string-in strip_id 5743640 | planet data search PSScene - \
+planet data filter --string-in strip_id 5743640 | planet data search PSScene --filter - \
 | planet collect - | gh gist create -f ps-search.geojson -w
 ```
 
@@ -125,9 +125,9 @@ TODO: get a command that gets the latest strip id and uses that as input in one
 This current command doesn't quite work.
 
 ```console
-strip-id=`planet data filter | planet data search PSScene - --limit 1 \
+strip-id=`planet data search PSScene --limit 1 \
 | jq -r '.properties.strip_id' | sed 's/\\[tn]//g'`
-planet data filter --string-in strip_id $stripid | planet data search PSScene -
+planet data filter --string-in strip_id $stripid | planet data search PSScene --filter -
 ```
 
 
@@ -146,23 +146,20 @@ Once it's set up you can just pipe any search command directly to `kepler` (it u
 
 ```console
 curl -s https://storage.googleapis.com/open-geodata/ch/vermont.json \
-| planet data filter --permission false --geom -  \
-| planet data search PSScene - \
+| planet data filter --geom -  \
+| planet data search PSScene --filter - \
 | kepler
 ```
 
-Note `--permission false` is added to these examples so that anyone with Planet access can try them, even if you don't
-have download permission for the area shown.
-
 (TODO: Add animated gif, showing some options)
 
 Kepler really excels at larger amounts of data, so try it out with larger limits:
 
 ```console
 curl -s https://storage.googleapis.com/open-geodata/ch/vermont.json \
-| planet data filter --permission false --geom - \
+| planet data filter --geom - \
 | planet data search PSScene,Sentinel2L1C,Landsat8L1G,SkySatCollect,Sentinel1 \
---sort 'acquired desc' --limit 1500 - \
+--sort 'acquired desc' --limit 1500 --filter - \
 | kepler
 ```
 
@@ -176,8 +173,8 @@ And you can bring it all together using Placemark for input and Kepler for outpu
 
 ```console
 curl -s https://api.placemark.io/api/v1/map/a0BWUEErqU9A1EDHZWHez/feature/91a07390-0652-11ed-8fdd-15633e4f8f01 \
-| planet data filter --permission false --geom - \
-| planet data search PSScene,Landsat8L1G,SkySatCollect,Sentinel1 - | kepler
+| planet data filter --geom - \
+| planet data search PSScene,Landsat8L1G,SkySatCollect,Sentinel1 --filter - | kepler
 ```
 
 #### Large Dataset Visualization
@@ -188,7 +185,7 @@ to disk. Getting 20,000 skysat collects will take at least a couple of minutes,
 100 megabytes of GeoJSON on disk.
 
 ```console
-planet data filter | planet data search SkySatCollect --limit 20000 > skysat-large.geojson
+planet data search SkySatCollect --limit 20000 > skysat-large.geojson
 ```
 
 Kepler can fairly easily handle 20,000 skysat footprints, try:
@@ -274,7 +271,7 @@ mapshaper -i footprints.geojson -simplify 15% -o simplified.geojson
 Once you find a simplification amount you're happy with you can use it as a piped output. 
 
 ```console
-planet data filter | planet data search --limit 20 SkySatCollect - | planet collect - | mapshaper -i - -simplify 15% -o skysat-ms2.geojson
+planet data search --limit 20 SkySatCollect - | planet collect - | mapshaper -i - -simplify 15% -o skysat-ms2.geojson
 ```
 
 Mapshaper also has more simplification algorithms to try out, so we recommend diving into the 
diff --git a/docs/get-started/quick-start-guide.md b/docs/get-started/quick-start-guide.md
index a1f10eb..5de63f3 100644
--- a/docs/get-started/quick-start-guide.md
+++ b/docs/get-started/quick-start-guide.md
@@ -100,10 +100,10 @@ In this step, you search for the most recent PSScene images available to downloa
 One of the commands you’ll use most frequently is `planet data filter`. This “convenience method” creates the JSON you need to run other commands. Run it with no arguments to see how it works by default:
 
 ```console
-planet data filter
+planet data filter --permission --std-quality
 ```
 
-Look at the console output to see some default filters. `PermissionFilter` filters the output to only contain imagery that you have permission to download. You’ll also see `quality_category`, which means the output lists only images in the [`standard quality` category](https://developers.planet.com/docs/data/planetscope/#image-quality-standard-vs-test-imagery). 
+Look at the console output to see some default filters. `PermissionFilter` filters the output to only contain imagery that you have permission to download. You’ll also see `quality_category`, which means the output lists only images in the [`standard quality` category](https://developers.planet.com/docs/data/planetscope/#image-quality-standard-vs-test-imagery). Without these options, an empty filter is generated which would be used to disable filtering and simply return all results.
 
 !!!note "The --help switch is your friend"
     You can do a lot with this `filter` command. We recommend running `planet data filter --help` often to get a reference of how the commands work.
@@ -113,13 +113,13 @@ Look at the console output to see some default filters. `PermissionFilter` filte
 Run the filter command and save it to a file named `filter.json`:
 
 ```console
-planet data filter > filter.json
+planet data filter --permission --std-quality > filter.json
 ```
 
 Then use that file with the search command and save the results to another file named `recent-psscene.json`.
 
 ```console
-planet data search PSScene filter.json > recent-psscene.json
+planet data search PSScene --filter filter.json > recent-psscene.json
 ```
 
 Open `recent-psscene.json` to see the 100 most recent PSScene images you have permissions to actually download.
diff --git a/planet/cli/data.py b/planet/cli/data.py
index b730609..fa716b2 100644
--- a/planet/cli/data.py
+++ b/planet/cli/data.py
@@ -155,8 +155,8 @@ def string_in_to_filter(ctx, param, values) -> Optional[List[dict]]:
     VALUE is a comma-separated list of entries.
     When multiple entries are specified, an implicit 'or' logic is applied.""")
 @click.option('--permission',
-              type=bool,
-              default=True,
+              is_flag=True,
+              default=False,
               show_default=True,
               help='Filter to assets with download permissions.')
 @click.option('--range',
@@ -168,8 +168,8 @@ def string_in_to_filter(ctx, param, values) -> Optional[List[dict]]:
     FIELD is the name of the field to filter on.
     COMP can be lt, lte, gt, or gte.""")
 @click.option('--std-quality',
-              type=bool,
-              default=True,
+              is_flag=True,
+              default=False,
               show_default=True,
               help='Filter to standard quality.')
 @click.option('--string-in',
@@ -209,6 +209,9 @@ def filter(ctx,
     inputs. This is only a subset of the complex filtering supported by the
     API. For advanced filter creation, either create the filter by hand or use
     the Python API.
+
+    If no options are specified, an empty filter is returned which, when used
+    in a search, bypasses all search filtering.
     """
     permission = data_filter.permission_filter() if permission else None
     std_quality = data_filter.std_quality_filter() if std_quality else None
@@ -234,7 +237,13 @@ def filter(ctx,
             else:
                 filters.append(f)
 
-    filt = data_filter.and_filter(filters)
+    if filters:
+        filt = data_filter.and_filter(filters)
+    else:
+        # make it explicit that we return an empty filter
+        # when no filters are specified
+        filt = data_filter.empty_filter()
+
     echo_json(filt, pretty)
 
 
@@ -245,7 +254,9 @@ def filter(ctx,
 @click.argument("item_types",
                 type=types.CommaSeparatedString(),
                 callback=check_item_types)
-@click.argument("filter", type=types.JSON())
+@click.option('--filter',
+              type=types.JSON(),
+              help='Apply specified filter to search.')
 @limit
 @click.option('--name', type=str, help='Name of the saved search.')
 @click.option('--sort',
@@ -262,8 +273,9 @@ async def search(ctx, item_types, filter, limit, name, sort, pretty):
 
     ITEM_TYPES is a comma-separated list of item-types to search.
 
-    FILTER must be JSON and can be specified a json string, filename, or '-'
-    for stdin.
+    If --filter is specified, the filter must be JSON and can be a json string,
+    filename, or '-' for stdin. If not specified, search results are not
+    filtered.
 
     Quick searches are stored for approximately 30 days and the --name
     parameter will be applied to the stored quick search.
@@ -271,7 +283,7 @@ async def search(ctx, item_types, filter, limit, name, sort, pretty):
     async with data_client(ctx) as cl:
 
         async for item in cl.search(item_types,
-                                    filter,
+                                    search_filter=filter,
                                     name=name,
                                     sort=sort,
                                     limit=limit):
diff --git a/planet/clients/data.py b/planet/clients/data.py
index 2acd649..4e0f52c 100644
--- a/planet/clients/data.py
+++ b/planet/clients/data.py
@@ -19,6 +19,7 @@ from pathlib import Path
 import time
 from typing import Any, AsyncIterator, Callable, Dict, List, Optional
 
+from ..data_filter import empty_filter
 from .. import exceptions
 from ..constants import PLANET_BASE_URL
 from ..http import Session
@@ -95,7 +96,7 @@ class DataClient:
 
     async def search(self,
                      item_types: List[str],
-                     search_filter: dict,
+                     search_filter: Optional[dict] = None,
                      name: Optional[str] = None,
                      sort: Optional[str] = None,
                      limit: int = 100) -> AsyncIterator[dict]:
@@ -112,7 +113,8 @@ class DataClient:
 
         Parameters:
             item_types: The item types to include in the search.
-            search_filter: Structured search criteria.
+            search_filter: Structured search criteria to apply. If None,
+                no search criteria is applied.
             sort: Field and direction to order results by. Valid options are
             given in SEARCH_SORT.
             name: The name of the saved search.
@@ -127,6 +129,8 @@ class DataClient:
         """
         url = f'{self._base_url}/quick-search'
 
+        search_filter = search_filter or empty_filter()
+
         # TODO: validate item_types
         request_json = {'filter': search_filter, 'item_types': item_types}
         if name:
diff --git a/planet/data_filter.py b/planet/data_filter.py
index e92fb65..86db69a 100644
--- a/planet/data_filter.py
+++ b/planet/data_filter.py
@@ -21,6 +21,11 @@ from planet import exceptions, geojson
 LOGGER = logging.getLogger(__name__)
 
 
+def empty_filter() -> dict:
+    """Create an Empty filter for bypassing search filtering."""
+    return {'type': 'AndFilter', 'config': []}
+
+
 def and_filter(nested_filters: List[dict]) -> dict:
     """Create an AndFilter
 


Test patch:
diff --git a/tests/integration/test_data_api.py b/tests/integration/test_data_api.py
index 8283d8b..3856e4d 100644
--- a/tests/integration/test_data_api.py
+++ b/tests/integration/test_data_api.py
@@ -24,7 +24,7 @@ import httpx
 import pytest
 import respx
 
-from planet import exceptions, DataClient
+from planet import exceptions, DataClient, data_filter
 
 TEST_URL = 'http://www.MockNotRealURL.com/api/path'
 TEST_SEARCHES_URL = f'{TEST_URL}/searches'
@@ -67,10 +67,7 @@ def search_response(item_descriptions):
 
 @respx.mock
 @pytest.mark.asyncio
-async def test_search_basic(item_descriptions,
-                            search_filter,
-                            search_response,
-                            session):
+async def test_search_basic(item_descriptions, search_response, session):
 
     quick_search_url = f'{TEST_URL}/quick-search'
     next_page_url = f'{TEST_URL}/blob/?page_marker=IAmATest'
@@ -89,15 +86,46 @@ async def test_search_basic(item_descriptions,
     respx.get(next_page_url).return_value = mock_resp2
 
     cl = DataClient(session, base_url=TEST_URL)
-    items_list = [
-        i async for i in cl.search(
-            ['PSScene'], search_filter, name='quick_search')
-    ]
+    items_list = [i async for i in cl.search(['PSScene'])]
+
+    # check that request is correct
+    expected_request = {
+        "item_types": ["PSScene"], "filter": data_filter.empty_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.asyncio
+async def test_search_name(item_descriptions, search_response, session):
+
+    quick_search_url = f'{TEST_URL}/quick-search'
+    next_page_url = f'{TEST_URL}/blob/?page_marker=IAmATest'
+
+    item1, item2, item3 = item_descriptions
+    page1_response = {
+        "_links": {
+            "_next": next_page_url
+        }, "features": [item1, item2]
+    }
+    mock_resp1 = httpx.Response(HTTPStatus.OK, json=page1_response)
+    respx.post(quick_search_url).return_value = mock_resp1
+
+    page2_response = {"_links": {"_self": next_page_url}, "features": [item3]}
+    mock_resp2 = httpx.Response(HTTPStatus.OK, json=page2_response)
+    respx.get(next_page_url).return_value = mock_resp2
+
+    cl = DataClient(session, base_url=TEST_URL)
+    items_list = [i async for i in cl.search(['PSScene'], name='quick_search')]
 
     # check that request is correct
     expected_request = {
         "item_types": ["PSScene"],
-        "filter": search_filter,
+        "filter": data_filter.empty_filter(),
         "name": "quick_search"
     }
     actual_body = json.loads(respx.calls[0].request.content)
@@ -107,6 +135,43 @@ async def test_search_basic(item_descriptions,
     assert items_list == item_descriptions
 
 
+@respx.mock
+@pytest.mark.asyncio
+async def test_search_filter(item_descriptions,
+                             search_filter,
+                             search_response,
+                             session):
+
+    quick_search_url = f'{TEST_URL}/quick-search'
+    next_page_url = f'{TEST_URL}/blob/?page_marker=IAmATest'
+
+    item1, item2, item3 = item_descriptions
+    page1_response = {
+        "_links": {
+            "_next": next_page_url
+        }, "features": [item1, item2]
+    }
+    mock_resp1 = httpx.Response(HTTPStatus.OK, json=page1_response)
+    respx.post(quick_search_url).return_value = mock_resp1
+
+    page2_response = {"_links": {"_self": next_page_url}, "features": [item3]}
+    mock_resp2 = httpx.Response(HTTPStatus.OK, json=page2_response)
+    respx.get(next_page_url).return_value = mock_resp2
+
+    cl = DataClient(session, base_url=TEST_URL)
+    items_list = [
+        i async for i in cl.search(['PSScene'], search_filter=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.asyncio
 async def test_search_sort(item_descriptions,
diff --git a/tests/integration/test_data_cli.py b/tests/integration/test_data_cli.py
index 109497c..ebddb8a 100644
--- a/tests/integration/test_data_cli.py
+++ b/tests/integration/test_data_cli.py
@@ -73,19 +73,6 @@ def test_data_search_command_registered(invoke):
     # Add other sub-commands here.
 
 
-PERMISSION_FILTER = {"type": "PermissionFilter", "config": ["assets:download"]}
-STD_QUALITY_FILTER = {
-    "type": "StringInFilter",
-    "field_name": "quality_category",
-    "config": ["standard"]
-}
-
-
-@pytest.fixture()
-def default_filters():
-    return [PERMISSION_FILTER, STD_QUALITY_FILTER]
-
-
 @pytest.fixture
 def assert_and_filters_equal():
     """Check for equality when the order of the config list doesn't matter"""
@@ -103,27 +90,45 @@ def assert_and_filters_equal():
 
 @respx.mock
 @pytest.mark.asyncio
-@pytest.mark.parametrize("permission, p_remove",
-                         [(None, None),
-                          ('--permission=False', PERMISSION_FILTER)])
-@pytest.mark.parametrize("std_quality, s_remove",
-                         [(None, None),
-                          ('--std-quality=False', STD_QUALITY_FILTER)])
-def test_data_filter_defaults(permission,
-                              p_remove,
-                              std_quality,
-                              s_remove,
-                              invoke,
-                              default_filters,
-                              assert_and_filters_equal):
-    runner = CliRunner()
+def test_data_filter_defaults(invoke, assert_and_filters_equal):
+
+    result = invoke(["filter"])
+    assert result.exit_code == 0
+
+    empty_filter = {"type": "AndFilter", "config": []}
+    assert_and_filters_equal(json.loads(result.output), empty_filter)
+
+
+@respx.mock
+@pytest.mark.asyncio
+def test_data_filter_permission(invoke, assert_and_filters_equal):
+    result = invoke(["filter", "--permission"])
+    assert result.exit_code == 0
+
+    expected_filt = {
+        "type": "AndFilter",
+        "config": [{
+            "type": "PermissionFilter", "config": ["assets:download"]
+        }]
+    }
+    assert_and_filters_equal(json.loads(result.output), expected_filt)
 
-    args = [arg for arg in [permission, std_quality] if arg]
-    result = invoke(["filter", *args], runner=runner)
+
+@respx.mock
+@pytest.mark.asyncio
+def test_data_filter_std_quality(invoke, assert_and_filters_equal):
+    result = invoke(["filter", '--std-quality'])
     assert result.exit_code == 0
 
-    [default_filters.remove(rem) for rem in [p_remove, s_remove] if rem]
-    expected_filt = {"type": "AndFilter", "config": default_filters}
+    expected_filt = {
+        "type":
+        "AndFilter",
+        "config": [{
+            "type": "StringInFilter",
+            "field_name": "quality_category",
+            "config": ["standard"]
+        }]
+    }
     assert_and_filters_equal(json.loads(result.output), expected_filt)
 
 
@@ -135,35 +140,24 @@ def test_data_filter_defaults(permission,
                            ['ortho_analytic_8b_sr', 'ortho_analytic_4b_sr']),
                           ('ortho_analytic_8b_sr , ortho_analytic_4b_sr',
                            ['ortho_analytic_8b_sr', 'ortho_analytic_4b_sr'])])
-def test_data_filter_asset(asset,
-                           expected,
-                           invoke,
-                           default_filters,
-                           assert_and_filters_equal):
-    runner = CliRunner()
+def test_data_filter_asset(asset, expected, invoke, assert_and_filters_equal):
 
-    result = invoke(["filter", f'--asset={asset}'], runner=runner)
+    result = invoke(["filter", f'--asset={asset}'])
     assert result.exit_code == 0
 
     asset_filter = {"type": "AssetFilter", "config": expected}
-    expected_filt = {
-        "type": "AndFilter", "config": default_filters + [asset_filter]
-    }
+    expected_filt = {"type": "AndFilter", "config": [asset_filter]}
     assert_and_filters_equal(json.loads(result.output), expected_filt)
 
 
 @respx.mock
 @pytest.mark.asyncio
-def test_data_filter_date_range_success(invoke,
-                                        assert_and_filters_equal,
-                                        default_filters):
+def test_data_filter_date_range_success(invoke, assert_and_filters_equal):
     """Check filter is created correctly and that multiple options results in
     multiple filters"""
-    runner = CliRunner()
 
     result = invoke(["filter"] + '--date-range field gt 2021-01-01'.split() +
-                    '--date-range field2 lt 2022-01-01'.split(),
-                    runner=runner)
+                    '--date-range field2 lt 2022-01-01'.split())
     assert result.exit_code == 0
 
     date_range_filter1 = {
@@ -183,7 +177,7 @@ def test_data_filter_date_range_success(invoke,
 
     expected_filt = {
         "type": "AndFilter",
-        "config": default_filters + [date_range_filter1, date_range_filter2]
+        "config": [date_range_filter1, date_range_filter2]
     }
 
     assert_and_filters_equal(json.loads(result.output), expected_filt)
@@ -192,10 +186,7 @@ def test_data_filter_date_range_success(invoke,
 @respx.mock
 @pytest.mark.asyncio
 def test_data_filter_date_range_invalid(invoke):
-    runner = CliRunner()
-
-    result = invoke(["filter"] + '--date-range field gt 2021'.split(),
-                    runner=runner)
+    result = invoke(["filter"] + '--date-range field gt 2021'.split())
     assert result.exit_code == 2
 
 
@@ -208,15 +199,13 @@ def test_data_filter_geom(geom_fixture,
                           request,
                           invoke,
                           geom_geojson,
-                          assert_and_filters_equal,
-                          default_filters):
+                          assert_and_filters_equal):
     """Ensure that all GeoJSON forms of describing a geometry are handled
     and all result in the same, valid GeometryFilter being created"""
-    runner = CliRunner()
 
     geom = request.getfixturevalue(geom_fixture)
     geom_str = json.dumps(geom)
-    result = invoke(["filter", f'--geom={geom_str}'], runner=runner)
+    result = invoke(["filter", f'--geom={geom_str}'])
     assert result.exit_code == 0
 
     geom_filter = {
@@ -225,23 +214,17 @@ def test_data_filter_geom(geom_fixture,
         "config": geom_geojson
     }
 
-    expected_filt = {
-        "type": "AndFilter", "config": default_filters + [geom_filter]
-    }
+    expected_filt = {"type": "AndFilter", "config": [geom_filter]}
 
     assert_and_filters_equal(json.loads(result.output), expected_filt)
 
 
 @respx.mock
 @pytest.mark.asyncio
-def test_data_filter_number_in_success(invoke,
-                                       assert_and_filters_equal,
-                                       default_filters):
-    runner = CliRunner()
+def test_data_filter_number_in_success(invoke, assert_and_filters_equal):
 
     result = invoke(["filter"] + '--number-in field 1'.split() +
-                    '--number-in field2 2,3.5'.split(),
-                    runner=runner)
+                    '--number-in field2 2,3.5'.split())
     assert result.exit_code == 0
 
     number_in_filter1 = {
@@ -252,8 +235,7 @@ def test_data_filter_number_in_success(invoke,
     }
 
     expected_filt = {
-        "type": "AndFilter",
-        "config": default_filters + [number_in_filter1, number_in_filter2]
+        "type": "AndFilter", "config": [number_in_filter1, number_in_filter2]
     }
 
     assert_and_filters_equal(json.loads(result.output), expected_filt)
@@ -261,26 +243,19 @@ def test_data_filter_number_in_success(invoke,
 
 @respx.mock
 @pytest.mark.asyncio
-def test_data_filter_number_in_badparam(invoke,
-                                        assert_and_filters_equal,
-                                        default_filters):
-    runner = CliRunner()
+def test_data_filter_number_in_badparam(invoke, assert_and_filters_equal):
 
-    result = invoke(["filter"] + '--number-in field 1,str'.split(),
-                    runner=runner)
+    result = invoke(["filter"] + '--number-in field 1,str'.split())
     assert result.exit_code == 2
 
 
 @respx.mock
 @pytest.mark.asyncio
-def test_data_filter_range(invoke, assert_and_filters_equal, default_filters):
+def test_data_filter_range(invoke, assert_and_filters_equal):
     """Check filter is created correctly, that multiple options results in
     multiple filters, and that floats are processed correctly."""
-    runner = CliRunner()
-
     result = invoke(["filter"] + '--range field gt 70'.split() +
-                    '--range cloud_cover lt 0.5'.split(),
-                    runner=runner)
+                    '--range cloud_cover lt 0.5'.split())
     assert result.exit_code == 0
 
     range_filter1 = {
@@ -297,8 +272,7 @@ def test_data_filter_range(invoke, assert_and_filters_equal, default_filters):
     }
 
     expected_filt = {
-        "type": "AndFilter",
-        "config": default_filters + [range_filter1, range_filter2]
+        "type": "AndFilter", "config": [range_filter1, range_filter2]
     }
 
     assert_and_filters_equal(json.loads(result.output), expected_filt)
@@ -306,14 +280,10 @@ def test_data_filter_range(invoke, assert_and_filters_equal, default_filters):
 
 @respx.mock
 @pytest.mark.asyncio
-def test_data_filter_string_in(invoke,
-                               assert_and_filters_equal,
-                               default_filters):
-    runner = CliRunner()
+def test_data_filter_string_in(invoke, assert_and_filters_equal):
 
     result = invoke(["filter"] + '--string-in field foo'.split() +
-                    '--string-in field2 foo,bar'.split(),
-                    runner=runner)
+                    '--string-in field2 foo,bar'.split())
     assert result.exit_code == 0
 
     string_in_filter1 = {
@@ -326,8 +296,7 @@ def test_data_filter_string_in(invoke,
     }
 
     expected_filt = {
-        "type": "AndFilter",
-        "config": default_filters + [string_in_filter1, string_in_filter2]
+        "type": "AndFilter", "config": [string_in_filter1, string_in_filter2]
     }
 
     assert_and_filters_equal(json.loads(result.output), expected_filt)
@@ -335,14 +304,12 @@ def test_data_filter_string_in(invoke,
 
 @respx.mock
 @pytest.mark.asyncio
-def test_data_filter_update(invoke, assert_and_filters_equal, default_filters):
+def test_data_filter_update(invoke, assert_and_filters_equal):
     """Check filter is created correctly and that multiple options results in
     multiple filters"""
-    runner = CliRunner()
 
     result = invoke(["filter"] + '--update field gt 2021-01-01'.split() +
-                    '--update field2 gte 2022-01-01'.split(),
-                    runner=runner)
+                    '--update field2 gte 2022-01-01'.split())
     assert result.exit_code == 0
 
     update_filter1 = {
@@ -361,20 +328,38 @@ def test_data_filter_update(invoke, assert_and_filters_equal, default_filters):
     }
 
     expected_filt = {
-        "type": "AndFilter",
-        "config": default_filters + [update_filter1, update_filter2]
+        "type": "AndFilter", "config": [update_filter1, update_filter2]
     }
 
     assert_and_filters_equal(json.loads(result.output), expected_filt)
 
 
+@respx.mock
+@pytest.mark.parametrize("item_types, expect_success",
+                         [('PSScene', True), ('SkySatScene', True),
+                          ('PSScene, SkySatScene', True), ('INVALID', False)])
+def test_data_search_cmd_item_types(item_types, expect_success, invoke):
+    """Test for planet data search_quick item types, valid and invalid."""
+    mock_resp = httpx.Response(HTTPStatus.OK,
+                               json={'features': [{
+                                   "key": "value"
+                               }]})
+    respx.post(TEST_QUICKSEARCH_URL).return_value = mock_resp
+
+    result = invoke(['search', item_types])
+
+    if expect_success:
+        assert result.exit_code == 0
+        assert len(result.output.strip().split('\n')) == 1  # we have 1 feature
+    else:
+        assert result.exit_code == 2
+
+
 @respx.mock
 @pytest.mark.asyncio
 @pytest.mark.parametrize("filter", ['{1:1}', '{"foo"}'])
-@pytest.mark.parametrize("item_types",
-                         ['PSScene', 'SkySatScene', 'PSScene, SkySatScene'])
-def test_data_search_cmd_filter_invalid_json(invoke, item_types, filter):
-    """Test for planet data search_quick. Test with multiple item_types.
+def test_data_search_cmd_filter_invalid_json(invoke, filter):
+    """Test for planet data search_quick.
     Test should fail as filter does not contain valid JSON."""
     mock_resp = httpx.Response(HTTPStatus.OK,
                                json={'features': [{
@@ -382,15 +367,12 @@ def test_data_search_cmd_filter_invalid_json(invoke, item_types, filter):
                                }]})
     respx.post(TEST_QUICKSEARCH_URL).return_value = mock_resp
 
-    runner = CliRunner()
-    result = invoke(["search", item_types, filter], runner=runner)
+    result = invoke(['search', 'PSScene', f'--filter={filter}'])
     assert result.exit_code == 2
 
 
 @respx.mock
-@pytest.mark.parametrize("item_types",
-                         ['PSScene', 'SkySatScene', 'PSScene, SkySatScene'])
-def test_data_search_cmd_filter_success(invoke, item_types):
+def test_data_search_cmd_filter_success(invoke):
     """Test for planet data search_quick. Test with multiple item_types.
     Test should succeed as filter contains valid JSON."""
     filter = {
@@ -407,12 +389,17 @@ def test_data_search_cmd_filter_success(invoke, item_types):
                                }]})
     respx.post(TEST_QUICKSEARCH_URL).return_value = mock_resp
 
-    runner = CliRunner()
-    result = invoke(["search", item_types, json.dumps(filter)], runner=runner)
+    result = invoke(['search', 'PSScene', f'--filter={json.dumps(filter)}'])
 
     assert result.exit_code == 0
     assert len(result.output.strip().split('\n')) == 1  # we have 1 feature
 
+    # check that filter was sent
+    expected_request = {"item_types": ['PSScene'], "filter": filter}
+    actual_body = json.loads(respx.calls[0].request.content)
+
+    assert actual_body == expected_request
+
 
 @respx.mock
 def test_data_search_cmd_sort_success(invoke):
@@ -421,41 +408,18 @@ def test_data_search_cmd_sort_success(invoke):
     sort = 'published asc'
     search_url = f'{TEST_QUICKSEARCH_URL}?_sort={sort}'
 
-    filter = {
-        "type": "DateRangeFilter",
-        "field_name": "acquired",
-        "config": {
-            "gt": "2019-12-31T00:00:00Z"
-        }
-    }
-
     feature = {"key": "value"}
     mock_resp = httpx.Response(HTTPStatus.OK, json={'features': [feature]})
     respx.post(search_url).return_value = mock_resp
 
-    runner = CliRunner()
-    result = invoke(
-        ['search', 'PSScene', json.dumps(filter), f'--sort={sort}'],
-        runner=runner)
+    result = invoke(['search', 'PSScene', f'--sort={sort}'])
     assert result.exit_code == 0
     assert json.loads(result.output) == feature
 
 
 @respx.mock
 def test_data_search_cmd_sort_invalid(invoke):
-    filter = {
-        "type": "DateRangeFilter",
-        "field_name": "acquired",
-        "config": {
-            "gt": "2019-12-31T00:00:00Z"
-        }
-    }
-
-    runner = CliRunner()
-    result = invoke(
-        ['search', 'PSScene', json.dumps(filter), '--sort=invalid'],
-        runner=runner)
-
+    result = invoke(['search', 'PSScene', '--sort=invalid'])
     assert result.exit_code == 2
 
 
@@ -471,13 +435,6 @@ def test_data_search_cmd_limit(invoke,
     If no value is specified, make sure the result contains at most 100
     entries.
     """
-    filter = {
-        "type": "DateRangeFilter",
-        "field_name": "acquired",
-        "config": {
-            "gt": "2019-12-31T00:00:00Z", "lte": "2020-01-31T00:00:00Z"
-        }
-    }
     item_types = 'SkySatCollect'
 
     # Creating 102 (3x34) search results
@@ -501,9 +458,7 @@ def test_data_search_cmd_limit(invoke,
     respx.post(TEST_QUICKSEARCH_URL).return_value = mock_resp
 
     runner = CliRunner()
-    result = invoke(
-        ["search", item_types, json.dumps(filter), "--limit", limit],
-        runner=runner)
+    result = invoke(["search", item_types, "--limit", limit], runner=runner)
     assert result.exit_code == 0
     assert result.output.count('"id"') == limited_list_length
 
@@ -514,7 +469,7 @@ def test_data_search_cmd_limit(invoke,
 @pytest.mark.parametrize("item_types",
                          ['PSScene', 'SkySatScene', 'PSScene, SkySatScene'])
 def test_data_search_create_filter_invalid_json(invoke, item_types, filter):
-    """Test for planet data search_create. Test with multiple item_types.
+    """Test for planet data search_create.
     Test should fail as filter does not contain valid JSON."""
     mock_resp = httpx.Response(HTTPStatus.OK,
                                json={'features': [{