예제 #1
0
class MetadataTest(unittest.TestCase):
    def setUp(self):
        self.url = "http://example.com/metadata"
        self.instance = Metadata(url=self.url,
                                 auth=Auth(jwt_token=public_token,
                                           token_info_path=None))
        self.instance._raster = Raster(url=self.url, auth=self.instance.auth)
        self.match_url = re.compile(self.url)

    def mock_response(self, method, json, status=200, **kwargs):
        responses.add(method,
                      self.match_url,
                      json=json,
                      status=status,
                      **kwargs)

    def test_expr_serialization(self):
        p = self.instance.properties
        q = ((0.1 < p.cloud_fraction <= 0.2) &
             (p.sat_id == "f00b")) | (p.sat_id == "usa-245")
        expected_q = {
            "or": [
                {
                    "and": [
                        {
                            "range": {
                                "cloud_fraction": {
                                    "gt": 0.1,
                                    "lte": 0.2
                                }
                            }
                        },
                        {
                            "eq": {
                                "sat_id": "f00b"
                            }
                        },
                    ]
                },
                {
                    "eq": {
                        "sat_id": "usa-245"
                    }
                },
            ]
        }

        assert q.serialize() == expected_q

    def test_expr_contains(self):
        p = self.instance.properties
        q = p.sat_id.in_(("usa-245", "terra"))
        expected_q = {
            "or": [{
                "eq": {
                    "sat_id": "usa-245"
                }
            }, {
                "eq": {
                    "sat_id": "terra"
                }
            }]
        }

        assert q.serialize() == expected_q

    @responses.activate
    def test_paged_search(self):
        features = [{"id": "foo"}]
        token = "token"
        self.mock_response(responses.POST,
                           json=features,
                           headers={"x-continuation-token": token})
        collection = self.instance.paged_search(limit=100)
        assert features == collection.features
        assert token == collection.properties.continuation_token

    @responses.activate
    def test_paged_search_deprecated_args(self):
        features = [{"id": "foo"}]
        self.mock_response(responses.POST, json=features)
        with warnings.catch_warnings(record=True) as w:
            collection = self.instance.paged_search(limit=100,
                                                    start_time="2017-07-08")
            assert 1 == len(w)
            assert w[0].category == DeprecationWarning
        assert features == collection.features

    @responses.activate
    def test_paged_search_dltile(self):
        features = [{"id": "foo"}]
        tile_geom = {
            "coordinates": [[
                [-94.01008346640455, 40.992358024242606],
                [-93.90737611136569, 40.99321227969176],
                [-93.908445279927, 41.0710332380541],
                [-94.01127360818097, 41.070176651899104],
                [-94.01008346640455, 40.992358024242606],
            ]],
            "type":
            "Polygon",
        }
        self.mock_response(responses.GET, json={"geometry": tile_geom})
        self.mock_response(responses.POST, json=features)
        collection = self.instance.paged_search(
            limit=100, dltile="256:16:30.0:15:-11:591")
        assert features == collection.features

    @responses.activate
    def test_paged_search_shapely(self):
        features = [{"id": "foo"}]
        geom = {
            "coordinates": [[
                [-94.01008346640455, 40.992358024242606],
                [-93.90737611136569, 40.99321227969176],
                [-93.908445279927, 41.0710332380541],
                [-94.01127360818097, 41.070176651899104],
                [-94.01008346640455, 40.992358024242606],
            ]],
            "type":
            "Polygon",
        }
        shape_geom = shape(geom)
        self.mock_response(responses.GET, json={"geometry": geom})
        self.mock_response(responses.POST, json=features)
        collection = self.instance.paged_search(limit=100, geom=shape_geom)
        assert features == collection.features

    @responses.activate
    def test_search(self):
        features = [{"id": "foo"}, {"id": "bar"}, {"id": "baz"}]
        self.mock_response(responses.POST, json=features)
        collection = self.instance.search(limit=2)
        req = responses.calls[0].request
        assert "storage_state" not in json.loads(req.body.decode("utf-8"))
        assert features[:2] == collection.features

    @responses.activate
    def test_search_storage_state(self):
        features = [{"id": "foo"}, {"id": "bar"}, {"id": "baz"}]
        self.mock_response(responses.POST, json=features)
        collection = self.instance.search(limit=2, storage_state="available")
        assert features[:2] == collection.features
        req = responses.calls[0].request
        assert "storage_state" in json.loads(req.body.decode("utf-8"))

    @responses.activate
    def test_features(self):
        features = [{"id": "foo"}, {"id": "bar"}, {"id": "baz"}]
        self.mock_response(responses.POST,
                           json=features[:2],
                           headers={"x-continuation-token": "token"})
        self.mock_response(
            responses.POST,
            json=features[2:],
            headers={"x-continuation-token": "token2"},
        )
        # Note: Unfortunately the client has historically been written in such a way that it always
        # expects a token header, even if the end of the search was reached, so an extra request
        # with 0 results happens in practice.
        self.mock_response(responses.POST,
                           json=[],
                           headers={"x-continuation-token": "token3"})
        assert features == list(self.instance.features())
        req = responses.calls[0].request
        assert "storage_state" not in json.loads(req.body.decode("utf-8"))

    @responses.activate
    def test_summary_default(self):
        summary = {"count": 42}
        self.mock_response(responses.POST, json=summary)
        assert summary == self.instance.summary()
        req = responses.calls[0].request
        assert "storage_state" not in json.loads(req.body.decode("utf-8"))

    @responses.activate
    def test_summary_storage_state(self):
        summary = {"count": 42}
        self.mock_response(responses.POST, json=summary)
        assert summary == self.instance.summary(storage_state="available")
        expected_req = {"date": "acquired", "storage_state": "available"}
        req = responses.calls[0].request
        assert json.loads(req.body.decode("utf-8")) == expected_req

    @responses.activate
    def test_summary_dltile(self):
        summary = {"count": 42}
        tile_geom = {
            "coordinates": [[
                [-94.01008346640455, 40.992358024242606],
                [-93.90737611136569, 40.99321227969176],
                [-93.908445279927, 41.0710332380541],
                [-94.01127360818097, 41.070176651899104],
                [-94.01008346640455, 40.992358024242606],
            ]],
            "type":
            "Polygon",
        }
        self.mock_response(responses.GET, json={"geometry": tile_geom})
        self.mock_response(responses.POST, json=summary)
        assert summary == self.instance.summary(
            dltile="256:16:30.0:15:-11:591")

    @responses.activate
    def test_summary_shapely(self):
        summary = {"count": 42}
        geom = {
            "coordinates": [[
                [-94.01008346640455, 40.992358024242606],
                [-93.90737611136569, 40.99321227969176],
                [-93.908445279927, 41.0710332380541],
                [-94.01127360818097, 41.070176651899104],
                [-94.01008346640455, 40.992358024242606],
            ]],
            "type":
            "Polygon",
        }
        shape_geom = shape(geom)
        self.mock_response(responses.GET, json={"geometry": geom})
        self.mock_response(responses.POST, json=summary)
        assert summary == self.instance.summary(geom=shape_geom)
예제 #2
0
class MetadataTest(unittest.TestCase):

    def setUp(self):
        self.url = "http://example.com/metadata"
        self.instance = Metadata(url=self.url, auth=Auth(jwt_token=public_token, token_info_path=None))
        self.instance._raster = Raster(url=self.url, auth=self.instance.auth)
        self.match_url = re.compile(self.url)

    def mock_response(self, method, json, status=200, **kwargs):
        responses.add(method, self.match_url, json=json, status=status, **kwargs)

    def test_expr_serialization(self):
        p = self.instance.properties
        q = ((0.1 < p.cloud_fraction <= 0.2) & (p.sat_id == "f00b")) | (
            p.sat_id == "usa-245"
        )
        expected_q = {
            "or": [
                {
                    "and": [
                        {"range": {"cloud_fraction": {"gt": 0.1, "lte": 0.2}}},
                        {"eq": {"sat_id": "f00b"}},
                    ]
                },
                {"eq": {"sat_id": "usa-245"}},
            ]
        }

        self.assertEqual(q.serialize(), expected_q)

    @responses.activate
    def test_paged_search(self):
        features = [{"id": "foo"}]
        token = "token"
        self.mock_response(responses.POST, json=features, headers={"x-continuation-token": token})
        collection = self.instance.paged_search(limit=100)
        self.assertEqual(features, collection.features)
        self.assertEqual(token, collection.properties.continuation_token)

    @responses.activate
    def test_paged_search_deprecated_args(self):
        features = [{"id": "foo"}]
        self.mock_response(responses.POST, json=features)
        with warnings.catch_warnings(record=True) as w:
            collection = self.instance.paged_search(limit=100, start_time="2017-07-08")
            self.assertEqual(1, len(w))
            self.assertEqual(w[0].category, DeprecationWarning)
        self.assertEqual(features, collection.features)

    @responses.activate
    def test_paged_search_dltile(self):
        features = [{"id": "foo"}]
        tile_geom = {
            "coordinates": [[[-94.01008346640455, 40.992358024242606], [-93.90737611136569, 40.99321227969176],
                             [-93.908445279927, 41.0710332380541], [-94.01127360818097, 41.070176651899104],
                             [-94.01008346640455, 40.992358024242606]]],
            "type": "Polygon"
        }
        self.mock_response(responses.GET, json={"geometry": tile_geom})
        self.mock_response(responses.POST, json=features)
        collection = self.instance.paged_search(limit=100, dltile="256:16:30.0:15:-11:591")
        self.assertEqual(features, collection.features)

    @responses.activate
    def test_search(self):
        features = [{"id": "foo"}, {"id": "bar"}, {"id": "baz"}]
        self.mock_response(responses.POST, json=features)
        collection = self.instance.search(limit=2)
        self.assertEqual(features[:2], collection.features)

    @responses.activate
    def test_features(self):
        features = [{"id": "foo"}, {"id": "bar"}, {"id": "baz"}]
        self.mock_response(responses.POST, json=features[:2], headers={"x-continuation-token": "token"})
        self.mock_response(responses.POST, json=features[2:], headers={"x-continuation-token": "token2"})
        # Note: Unfortunately the client has historically been written in such a way that it always
        # expects a token header, even if the end of the search was reached, so an extra request
        # with 0 results happens in practice.
        self.mock_response(responses.POST, json=[], headers={"x-continuation-token": "token3"})
        self.assertEqual(features, list(self.instance.features()))

    @responses.activate
    def test_summary(self):
        summary = {"count": 42}
        self.mock_response(responses.POST, json=summary)
        self.assertEqual(summary, self.instance.summary())

    @responses.activate
    def test_summary_dltile(self):
        summary = {"count": 42}
        tile_geom = {
            "coordinates": [[[-94.01008346640455, 40.992358024242606], [-93.90737611136569, 40.99321227969176],
                             [-93.908445279927, 41.0710332380541], [-94.01127360818097, 41.070176651899104],
                             [-94.01008346640455, 40.992358024242606]]],
            "type": "Polygon"
        }
        self.mock_response(responses.GET, json={"geometry": tile_geom})
        self.mock_response(responses.POST, json=summary)
        self.assertEqual(summary, self.instance.summary(dltile="256:16:30.0:15:-11:591"))
예제 #3
0
def search(aoi,
           products=None,
           start_datetime=None,
           end_datetime=None,
           cloud_fraction=None,
           limit=100,
           sort_field=None,
           sort_order='asc',
           date_field='acquired',
           query=None,
           randomize=False,
           raster_client=None,
           metadata_client=None):
    """
    Search for Scenes in the Descartes Labs catalog.

    Returns a SceneCollection of Scenes that overlap with an area of interest,
    and meet the given search criteria.

    Parameters
    ----------
    aoi : GeoJSON-like dict, GeoContext, or object with __geo_interface__
        Search for scenes that intersect this area by any amount.
        If a GeoContext, a copy is returned as ``ctx``, with missing values filled in.
        Otherwise, the returned ``ctx`` will be an `AOI`, with this as its geometry.
    products : str or List[str], optional
        Descartes Labs product identifiers
    start_datetime : str, datetime-like, optional
        Restrict to scenes acquired after this datetime
    end_datetime : str, datetime-like, optional
        Restrict to scenes acquired before this datetime
    cloud_fraction : float, optional
        Restrict to scenes that are covered in clouds by less than this fraction
        (between 0 and 1)
    limit : int, optional
        Maximum number of Scenes to return, up to 10000.
    sort_field : str, optional
        Field name in ``Scene.properties`` by which to order the results
    sort_order : str, optional, default 'asc'
        ``"asc"`` or ``"desc"``
    date_field : str, optional, default 'acquired'
        The field used when filtering by date
        (``"acquired"``, ``"processed"``, ``"published"``)
    query : descarteslabs.common.property_filtering.Expression, optional
        Expression used to filter Scenes by their properties, built from ``dl.properties``.

        >>> query = 150 < dl.properties.azimuth_angle < 160 & dl.properties.cloud_fraction < 0.5
        >>> query = dl.properties.sat_id == "Terra"
    randomize : bool, default False, optional
        Randomize the order of the results.
        You may also use an int or str as an explicit seed.
    raster_client : Raster, optional
        Unneeded in general use; lets you use a specific client instance
        with non-default auth and parameters.
    metadata_client : Metadata, optional
        Unneeded in general use; lets you use a specific client instance
        with non-default auth and parameters.

    Returns
    -------
    scenes : SceneCollection
        Scenes matching your criteria.
    ctx: GeoContext
        The given ``aoi`` as a GeoContext (if it isn't one already),
        with reasonable default parameters for loading all matching Scenes.

        If ``aoi`` was a `GeoContext`, ``ctx`` will be a copy of ``aoi``,
        with any properties that were ``None`` assigned the defaults below.

        If ``aoi`` was not a `GeoContext`, an `AOI` instance will be created
        with ``aoi`` as its geometry, and defaults assigned as described below:

        **Default Spatial Parameters:**

        * resolution: the finest resolution of any band of all matching scenes
        * crs: the most common CRS used of all matching scenes
    """

    if isinstance(aoi, geocontext.GeoContext):
        ctx = aoi
        if ctx.bounds is None and ctx.geometry is None:
            raise ValueError(
                "Unspecified where to search, "
                "since the GeoContext given for ``aoi`` has neither geometry nor bounds set"
            )
    else:
        ctx = geocontext.AOI(geometry=aoi)

    if raster_client is None:
        raster_client = Raster()
    if metadata_client is None:
        metadata_client = Metadata()

    if isinstance(products, six.string_types):
        products = [products]

    if isinstance(start_datetime, datetime.datetime):
        start_datetime = start_datetime.isoformat()

    if isinstance(end_datetime, datetime.datetime):
        end_datetime = end_datetime.isoformat()

    if limit > MAX_RESULT_WINDOW:
        raise ValueError("Limit must be <= {}".format(MAX_RESULT_WINDOW))

    metadata_params = dict(products=products,
                           geom=ctx.__geo_interface__,
                           start_datetime=start_datetime,
                           end_datetime=end_datetime,
                           cloud_fraction=cloud_fraction,
                           limit=limit,
                           sort_field=sort_field,
                           sort_order=sort_order,
                           date=date_field,
                           q=query,
                           randomize=randomize)

    metadata = metadata_client.search(**metadata_params)
    if products is None:
        products = {
            meta["properties"]["product"]
            for meta in metadata["features"]
        }

    product_bands = {
        product:
        Scene._scenes_bands_dict(metadata_client.get_bands_by_product(product))
        for product in products
    }

    scenes = SceneCollection(
        (Scene(meta, product_bands[meta["properties"]["product"]])
         for meta in metadata["features"]),
        raster_client=raster_client)

    if len(scenes) > 0:
        assign_ctx = {}
        if ctx.resolution is None and ctx.shape is None:
            resolutions = filter(None,
                                 (b.get("resolution")
                                  for band in six.itervalues(product_bands)
                                  for b in six.itervalues(band)))
            try:
                assign_ctx["resolution"] = min(resolutions)
            except ValueError:
                assign_ctx[
                    "resolution"] = None  # from min of an empty sequence; no band defines resolution

        if ctx.crs is None:
            assign_ctx["crs"] = collections.Counter(
                scene.properties["crs"]
                for scene in scenes).most_common(1)[0][0]

        if len(assign_ctx) > 0:
            ctx = ctx.assign(**assign_ctx)

    return scenes, ctx
예제 #4
0
def search(
    aoi,
    products=None,
    start_datetime=None,
    end_datetime=None,
    cloud_fraction=None,
    storage_state=None,
    limit=100,
    sort_field=None,
    sort_order="asc",
    date_field="acquired",
    query=None,
    randomize=False,
    raster_client=None,
    metadata_client=None,
):
    """
    Search for Scenes in the Descartes Labs catalog.

    Returns a SceneCollection of Scenes that overlap with an area of interest,
    and meet the given search criteria.

    Parameters
    ----------
    aoi : GeoJSON-like dict, :class:`~descarteslabs.scenes.geocontext.GeoContext`, or object with __geo_interface__
        Search for scenes that intersect this area by any amount.
        If a :class:`~descarteslabs.scenes.geocontext.GeoContext`, a copy is returned as ``ctx``, with missing values
        filled in. Otherwise, the returned ``ctx`` will be an `AOI`, with this as its geometry.
    products : str or List[str], optional
        Descartes Labs product identifiers
    start_datetime : str, datetime-like, optional
        Restrict to scenes acquired after this datetime
    end_datetime : str, datetime-like, optional
        Restrict to scenes acquired before this datetime
    cloud_fraction : float, optional
        Restrict to scenes that are covered in clouds by less than this fraction
        (between 0 and 1)
    storage_state : str, optional
        Filter results based on ``storage_state`` value
        (``"available"``, ``"remote"``, or ``None``)
    limit : int or None, optional, default 100
        Maximum number of Scenes to return, or None for all results.
    sort_field : str, optional
        Field name in :py:attr:`Scene.properties` by which to order the results
    sort_order : str, optional, default 'asc'
        ``"asc"`` or ``"desc"``
    date_field : str, optional, default 'acquired'
        The field used when filtering by date
        (``"acquired"``, ``"processed"``, ``"published"``)
    query : ~descarteslabs.common.property_filtering.filtering.Expression, optional
        Expression used to filter Scenes by their properties, built from
        :class:`dl.properties <descarteslabs.common.property_filtering.filtering.GenericProperties>`.
        You can construct filter expression using the ``==``, ``!=``, ``<``, ``>``,
        ``<=`` and ``>=`` operators as well as the
        :meth:`~descarteslabs.common.property_filtering.filtering.Property.like`
        and :meth:`~descarteslabs.common.property_filtering.filtering.Property.in_`
        methods. You cannot use the boolean keywords ``and`` and ``or`` because of
        Python language limitations; instead you can combine filter expressions
        with ``&`` (boolean "and") and ``|`` (boolean "or").
        Example:
        ``150 < dl.properties.azimuth_angle < 160 & dl.properties.cloud_fraction < 0.5``
    randomize : bool, default False, optional
        Randomize the order of the results.
        You may also use an int or str as an explicit seed.
    raster_client : Raster, optional
        Unneeded in general use; lets you use a specific client instance
        with non-default auth and parameters.
    metadata_client : Metadata, optional
        Unneeded in general use; lets you use a specific client instance
        with non-default auth and parameters.

    Returns
    -------
    scenes : `SceneCollection`
        Scenes matching your criteria.
    ctx: :class:`~descarteslabs.scenes.geocontext.GeoContext`
        The given ``aoi`` as a :class:`~descarteslabs.scenes.geocontext.GeoContext` (if it isn't one already),
        with reasonable default parameters for loading all matching Scenes.

        If ``aoi`` was a :class:`~descarteslabs.scenes.geocontext.GeoContext`, ``ctx`` will be a copy of ``aoi``,
        with any properties that were ``None`` assigned the defaults below.

        If ``aoi`` was not a :class:`~descarteslabs.scenes.geocontext.GeoContext`, an `AOI` instance will be created
        with ``aoi`` as its geometry, and defaults assigned as described below:

        **Default Spatial Parameters:**

        * resolution: the finest resolution of any band of all matching scenes
        * crs: the most common CRS used of all matching scenes
    """

    if isinstance(aoi, geocontext.GeoContext):
        ctx = aoi
        if ctx.bounds is None and ctx.geometry is None:
            raise ValueError(
                "Unspecified where to search, "
                "since the GeoContext given for ``aoi`` has neither geometry nor bounds set"
            )
    else:
        ctx = geocontext.AOI(geometry=aoi)

    if raster_client is None:
        raster_client = Raster()
    if metadata_client is None:
        metadata_client = Metadata()

    if isinstance(products, six.string_types):
        products = [products]

    if isinstance(start_datetime, datetime.datetime):
        start_datetime = start_datetime.isoformat()

    if isinstance(end_datetime, datetime.datetime):
        end_datetime = end_datetime.isoformat()

    metadata_params = dict(
        products=products,
        geom=ctx.__geo_interface__,
        start_datetime=start_datetime,
        end_datetime=end_datetime,
        cloud_fraction=cloud_fraction,
        storage_state=storage_state,
        limit=limit,
        sort_field=sort_field,
        sort_order=sort_order,
        date=date_field,
        q=query,
        randomize=randomize,
    )

    metadata = metadata_client.search(**metadata_params)
    if products is None:
        products = {
            meta["properties"]["product"]
            for meta in metadata["features"]
        }

    product_bands = {
        product:
        Scene._scenes_bands_dict(metadata_client.get_bands_by_product(product))
        for product in products
    }

    scenes = SceneCollection(
        (Scene(meta, product_bands[meta["properties"]["product"]])
         for meta in metadata["features"]),
        raster_client=raster_client,
    )

    if len(scenes) > 0 and isinstance(ctx, geocontext.AOI):
        assign_ctx = {}
        if ctx.resolution is None and ctx.shape is None:
            resolutions = filter(
                None,
                (b.get("resolution") for band in six.itervalues(product_bands)
                 for b in six.itervalues(band)),
            )
            try:
                assign_ctx["resolution"] = min(resolutions)
            except ValueError:
                assign_ctx[
                    "resolution"] = None  # from min of an empty sequence; no band defines resolution

        if ctx.crs is None:
            assign_ctx["crs"] = collections.Counter(
                scene.properties["crs"]
                for scene in scenes).most_common(1)[0][0]

        if len(assign_ctx) > 0:
            ctx = ctx.assign(**assign_ctx)

    return scenes, ctx