def test_items_as_collection(self): search = ItemSearch( url=SEARCH_URL, bbox=(-73.21, 43.99, -73.12, 44.05), collections='naip', limit=10, max_items=20, ) item_collection = search.items_as_collection() assert len(item_collection.features) == 20
def test_results(self): search = ItemSearch( url=SEARCH_URL, collections='naip', max_items=20, limit=10, ) results = search.items() assert all(isinstance(item, pystac.Item) for item in results)
def test_method(self): # Default method should be POST... search = ItemSearch(url=ASTRAEA_URL) assert search.method == 'POST' # "method" argument should take precedence over presence of "intersects" search = ItemSearch(url=ASTRAEA_URL, method='GET', intersects=INTERSECTS_EXAMPLE) assert search.method == 'GET'
def test_ids_results(self): ids = [ 'm_3510836_se_12_060_20180508_20190331', 'm_3510840_se_12_060_20180504_20190331' ] search = ItemSearch( url=SEARCH_URL, ids=ids, ) results = list(search.items()) assert len(results) == 2 assert all(item.id in ids for item in results)
def test_result_paging(self): search = ItemSearch( url=SEARCH_URL, bbox=(-73.21, 43.99, -73.12, 44.05), collections='naip', limit=10, max_items=20, ) # Check that the current page changes on the ItemSearch instance when a new page is requested pages = list(search.item_collections()) assert pages[1] != pages[2] assert pages[1].features != pages[2].features
def test_generator_of_collection_strings(self): # Generator of ID strings def collectioner(): yield from ['naip', 'landsat8_l1tp'] search = ItemSearch(url=ASTRAEA_URL, collections=collectioner()) assert search.request.json['collections'] == ('naip', 'landsat8_l1tp')
def test_three_datetimes(self): start = datetime(2020, 2, 1, 0, 0, 0, tzinfo=tzutc()) middle = datetime(2020, 2, 2, 0, 0, 0, tzinfo=tzutc()) end = datetime(2020, 2, 3, 0, 0, 0, tzinfo=tzutc()) with pytest.raises(Exception): ItemSearch(url=ASTRAEA_URL, datetime=[start, middle, end])
def test_many_datetimes(self): datetimes = [ "1985-04-12T23:20:50.52Z" "1996-12-19T16:39:57-08:00" "1990-12-31T23:59:60Z" "1990-12-31T15:59:60-08:00" "1937-01-01T12:00:27.87+01:00" "1985-04-12T23:20:50.52Z" "1937-01-01T12:00:27.8710+01:00" "1937-01-01T12:00:27.8+01:00" "1937-01-01T12:00:27.8Z" "1985-04-12t23:20:50.5202020z" "2020-07-23T00:00:00Z" "2020-07-23T00:00:00.0Z" "2020-07-23T00:00:00.01Z" "2020-07-23T00:00:00.012Z" "2020-07-23T00:00:00.0123Z" "2020-07-23T00:00:00.01234Z" "2020-07-23T00:00:00.012345Z" "2020-07-23T00:00:00.000Z" "2020-07-23T00:00:00.000+03:00" "2020-07-23T00:00:00+03:00" "2020-07-23T00:00:00.000+03:00" "2020-07-23T00:00:00.000z" ] for date_time in datetimes: ItemSearch(url=ASTRAEA_URL, datetime=date_time)
def test_range_string_datetime(self): # Timestamp range input search = ItemSearch( url=ASTRAEA_URL, datetime='2020-02-01T00:00:00Z/2020-02-02T00:00:00Z') assert search.request.json[ 'datetime'] == '2020-02-01T00:00:00Z/2020-02-02T00:00:00Z'
def test_generator_bbox(self): # Generator Input def bboxer(): yield from [-104.5, 44.0, -104.0, 45.0] search = ItemSearch(url=ASTRAEA_URL, bbox=bboxer()) assert search.request.json['bbox'] == (-104.5, 44.0, -104.0, 45.0)
def test_list_of_strings_datetime(self): # Timestamp list input search = ItemSearch( url=ASTRAEA_URL, datetime=['2020-02-01T00:00:00Z', '2020-02-02T00:00:00Z']) assert search.request.json[ 'datetime'] == '2020-02-01T00:00:00Z/2020-02-02T00:00:00Z'
def test_list_of_datetimes(self): start = datetime(2020, 2, 1, 0, 0, 0, tzinfo=tzutc()) end = datetime(2020, 2, 2, 0, 0, 0, tzinfo=tzutc()) # Datetime range input search = ItemSearch(url=ASTRAEA_URL, datetime=[start, end]) assert search.request.json[ 'datetime'] == '2020-02-01T00:00:00Z/2020-02-02T00:00:00Z'
def test_multiple_id_string(self): # Comma-separated ID string search = ItemSearch( url=ASTRAEA_URL, ids= 'm_3510836_se_12_060_20180508_20190331,m_3510840_se_12_060_20180504_20190331' ) assert search.request.json['ids'] == ( 'm_3510836_se_12_060_20180508_20190331', 'm_3510840_se_12_060_20180504_20190331')
def test_list_of_id_strings(self): # List of IDs search = ItemSearch(url=ASTRAEA_URL, ids=[ 'm_3510836_se_12_060_20180508_20190331', 'm_3510840_se_12_060_20180504_20190331' ]) assert search.request.json['ids'] == ( 'm_3510836_se_12_060_20180508_20190331', 'm_3510840_se_12_060_20180504_20190331')
def test_localized_datetime_converted_to_utc(self): # Localized datetime input (should be converted to UTC) start_localized = datetime(2020, 2, 1, 0, 0, 0, tzinfo=gettz('US/Eastern')) search = ItemSearch(url=ASTRAEA_URL, datetime=start_localized) assert search.request.json['datetime'] == '2020-02-01T05:00:00Z'
def test_generator_of_id_string(self): # Generator of IDs def ids(): yield from [ 'm_3510836_se_12_060_20180508_20190331', 'm_3510840_se_12_060_20180504_20190331' ] search = ItemSearch(url=ASTRAEA_URL, ids=ids()) assert search.request.json['ids'] == ( 'm_3510836_se_12_060_20180508_20190331', 'm_3510840_se_12_060_20180504_20190331')
def test_datetime_results(self): # Datetime range string datetime_ = '2019-01-01T00:00:01Z/2019-01-01T00:00:10Z' search = ItemSearch(url=SEARCH_URL, datetime=datetime_) results = list(search.items()) assert len(results) == 12 min_datetime = datetime(2019, 1, 1, 0, 0, 1, tzinfo=tzutc()) max_datetime = datetime(2019, 1, 1, 0, 0, 10, tzinfo=tzutc()) search = ItemSearch(url=SEARCH_URL, datetime=(min_datetime, max_datetime)) results = search.items() assert all(min_datetime <= item.datetime <= (max_datetime + timedelta(seconds=1)) for item in results)
def test_intersects_results(self): # GeoJSON-like dict intersects_dict = { 'type': 'Polygon', 'coordinates': [[[-73.21, 43.99], [-73.21, 44.05], [-73.12, 44.05], [-73.12, 43.99], [-73.21, 43.99]]] } search = ItemSearch(url=SEARCH_URL, intersects=intersects_dict, collections='naip') results = list(search.items()) assert len(results) == 30 # Geo-interface object class MockGeoObject: __geo_interface__ = intersects_dict intersects_obj = MockGeoObject() search = ItemSearch(url=SEARCH_URL, intersects=intersects_obj, collections='naip') results = search.items() assert all(isinstance(item, pystac.Item) for item in results)
def test_list_bbox(self): # List input search = ItemSearch(url=ASTRAEA_URL, bbox=[-104.5, 44.0, -104.0, 45.0]) assert search.request.json['bbox'] == (-104.5, 44.0, -104.0, 45.0)
def test_single_datetime_object(self): start = datetime(2020, 2, 1, 0, 0, 0, tzinfo=tzutc()) # Single datetime input search = ItemSearch(url=ASTRAEA_URL, datetime=start) assert search.request.json['datetime'] == '2020-02-01T00:00:00Z'
def test_intersects_dict(self): # Dict input search = ItemSearch(url=SEARCH_URL, intersects=INTERSECTS_EXAMPLE) assert search.request.json['intersects'] == INTERSECTS_EXAMPLE
def test_intersects_json_string(self): # JSON string input search = ItemSearch(url=SEARCH_URL, intersects=json.dumps(INTERSECTS_EXAMPLE)) assert search.request.json['intersects'] == INTERSECTS_EXAMPLE
def test_single_string_datetime(self): # Single timestamp input search = ItemSearch(url=ASTRAEA_URL, datetime='2020-02-01T00:00:00Z') assert search.request.json['datetime'] == '2020-02-01T00:00:00Z'
def test_single_id_string(self): # Single ID search = ItemSearch(url=ASTRAEA_URL, ids='m_3510836_se_12_060_20180508_20190331') assert search.request.json['ids'] == ( 'm_3510836_se_12_060_20180508_20190331', )
def test_string_bbox(self): # String Input search = ItemSearch(url=ASTRAEA_URL, bbox='-104.5,44.0,-104.0,45.0') assert search.request.json['bbox'] == (-104.5, 44.0, -104.0, 45.0)
def test_collection_object(self, astraea_api): collection = astraea_api.get_child('landsat8_l1tp') # Single pystac.Collection search = ItemSearch(url=ASTRAEA_URL, collections=collection) assert search.request.json['collections'] == ('landsat8_l1tp', )
def test_open_list_of_datetimes(self): start = datetime(2020, 2, 1, 0, 0, 0, tzinfo=tzutc()) # Open datetime range input search = ItemSearch(url=ASTRAEA_URL, datetime=(start, None)) assert search.request.json['datetime'] == '2020-02-01T00:00:00Z/..'
def test_tuple_bbox(self): # Tuple input search = ItemSearch(url=ASTRAEA_URL, bbox=(-104.5, 44.0, -104.0, 45.0)) assert search.request.json['bbox'] == (-104.5, 44.0, -104.0, 45.0)
def search(self, *, limit: Optional[int] = None, bbox: Optional[BBoxLike] = None, datetime: Optional[DatetimeLike] = None, intersects: Optional[IntersectsLike] = None, ids: Optional[IDsLike] = None, collections: Optional[CollectionsLike] = None, query: Optional[QueryLike] = None, max_items: Optional[int] = None, method: Optional[str] = 'POST', next_resolver: Optional[Callable] = None) -> ItemSearch: """Query the ``/search`` endpoint using the given parameters. This method returns an :class:`~pystac_client.ItemSearch` instance, see that class's documentation for details on how to get the number of matches and iterate over results. All keyword arguments are passed directly to the :class:`~pystac_client.ItemSearch` instance. .. warning:: This method is only implemented if the API conforms to the `STAC API - Item Search <https://github.com/radiantearth/stac-api-spec/tree/master/item-search>`__ spec *and* contains a link with a ``"rel"`` type of ``"search"`` in its root catalog. If the API does not meet either of these criteria, this method will raise a :exc:`NotImplementedError`. Parameters ---------- limit : int, optional The maximum number of items to return *per page*. Defaults to ``None``, which falls back to the limit set by the service. bbox: list or tuple or Iterator or str, optional May be a list, tuple, or iterator representing a bounding box of 2D or 3D coordinates. Results will be filtered to only those intersecting the bounding box. datetime: str or datetime.datetime or list or tuple or Iterator, optional Either a single datetime or datetime range used to filter results. You may express a single datetime using a :class:`datetime.datetime` instance, a `RFC 3339-compliant <https://tools.ietf.org/html/rfc3339>`__ timestamp, or a simple date string (see below). Instances of :class:`datetime.datetime` may be either timezone aware or unaware. Timezone aware instances will be converted to a UTC timestamp before being passed to the endpoint. Timezone unaware instances are assumed to represent UTC timestamps. You may represent a datetime range using a ``"/"`` separated string as described in the spec, or a list, tuple, or iterator of 2 timestamps or datetime instances. For open-ended ranges, use either ``".."`` (``'2020-01-01:00:00:00Z/..'``, ``['2020-01-01:00:00:00Z', '..']``) or a value of ``None`` (``['2020-01-01:00:00:00Z', None]``). If using a simple date string, the datetime can be specified in ``YYYY-mm-dd`` format, optionally truncating to ``YYYY-mm`` or just ``YYYY``. Simple date strings will be expanded to include the entire time period, for example: - ``2017`` expands to ``2017-01-01T00:00:00Z/2017-12-31T23:59:59Z`` - ``2017-06`` expands to ``2017-06-01T00:00:00Z/2017-06-30T23:59:59Z`` - ``2017-06-10`` expands to ``2017-06-10T00:00:00Z/2017-06-10T23:59:59Z`` If used in a range, the end of the range expands to the end of that day/month/year, for example: - ``2017/2018`` expands to ``2017-01-01T00:00:00Z/2018-12-31T23:59:59Z`` - ``2017-06/2017-07`` expands to ``2017-06-01T00:00:00Z/2017-07-31T23:59:59Z`` - ``2017-06-10/2017-06-11`` expands to ``2017-06-10T00:00:00Z/2017-06-11T23:59:59Z`` intersects: str or dict, optional A GeoJSON-like dictionary or JSON string. Results will be filtered to only those intersecting the geometry ids: list, optional List of Item ids to return. All other filter parameters that further restrict the number of search results (except ``limit``) are ignored. collections: list, optional List of one or more Collection IDs or :class:`pystac.Collection` instances. Only Items in one of the provided Collections will be searched max_items : int or None, optional The maximum number of items to return from the search. *Note that this is not a STAC API - Item Search parameter and is instead used by the client to limit the total number of returned items*. method : str or None, optional The HTTP method to use when making a request to the service. This must be either ``"GET"``, ``"POST"``, or ``None``. If ``None``, this will default to ``"POST"`` if the ``intersects`` argument is present and ``"GET"`` if not. If a ``"POST"`` request receives a ``405`` status for the response, it will automatically retry with a ``"GET"`` request for all subsequent requests. next_resolver: Callable, optional A callable that will be used to construct the next request based on a "next" link and the previous request. Defaults to using the :func:`~pystac_client.paging.simple_stac_resolver`. Returns ------- results : ItemSearch Raises ------ NotImplementedError If the API does not conform to the `Item Search spec <https://github.com/radiantearth/stac-api-spec/tree/master/item-search>`__ or does not have a link with a ``"rel"`` type of ``"search"``. """ if self.conformance is not None and not self.conforms_to( ConformanceClasses.STAC_API_ITEM_SEARCH): spec_name = ConformanceClasses.STAC_API_ITEM_SEARCH.name spec_uris = '\n\t'.join( ConformanceClasses.STAC_API_ITEM_SEARCH.all_uris) msg = f'This service does not conform to the {spec_name} spec and therefore the search method is not ' \ f'implemented. Services must publish one of the following conformance URIs in order to conform to ' \ f'this spec (preferably the first one):\n\t{spec_uris}' raise NotImplementedError(msg) search_link = self.get_single_link('search') if search_link is None: raise NotImplementedError( 'No link with a "rel" type of "search" could be found in this services\'s ' 'root catalog.') return ItemSearch(search_link.target, limit=limit, bbox=bbox, datetime=datetime, intersects=intersects, ids=ids, collections=collections, query=query, max_items=max_items, method=method, headers=self.headers, conformance=self.conformance, next_resolver=next_resolver)
def test_mixed_collection_object_and_string(self, astraea_api): collection = astraea_api.get_child('landsat8_l1tp') # Mixed list search = ItemSearch(url=ASTRAEA_URL, collections=[collection, 'naip']) assert search.request.json['collections'] == ('landsat8_l1tp', 'naip')