def test_wait(self, vector_client, _): calls = [ DotDict({ 'data': { 'attributes': { 'created': '2019-01-03T20:07:51.720000+00:00', 'started': '2019-01-03T20:07:51.903000+00:00', 'state': 'RUNNING' }, 'id': 'c589d688-3230-4caf-9f9d-18854f71e91d', 'type': 'copy_query' } }), DotDict({ 'data': { 'attributes': { 'created': '2019-01-03T20:07:51.720000+00:00', 'started': '2019-01-03T20:07:51.903000+00:00', 'state': 'DONE' }, 'id': 'c589d688-3230-4caf-9f9d-18854f71e91d', 'type': 'copy_query' } }) ] mock_get = mock.MagicMock(side_effect=calls) vector_client.return_value.get_product_from_query_status = mock_get FeatureCollection.COMPLETION_POLL_INTERVAL_SECONDS = 1 FeatureCollection('foo').wait_for_copy() self.assertEqual(2, mock_get.call_count)
def test_wait(self, vector_client, _): calls = [ DotDict({ "data": { "attributes": { "created": "2019-01-03T20:07:51.720000+00:00", "started": "2019-01-03T20:07:51.903000+00:00", "state": "RUNNING", }, "id": "c589d688-3230-4caf-9f9d-18854f71e91d", "type": "copy_query", } }), DotDict({ "data": { "attributes": { "created": "2019-01-03T20:07:51.720000+00:00", "started": "2019-01-03T20:07:51.903000+00:00", "state": "DONE", }, "id": "c589d688-3230-4caf-9f9d-18854f71e91d", "type": "copy_query", } }), ] mock_get = mock.MagicMock(side_effect=calls) vector_client.return_value.get_product_from_query_status = mock_get FeatureCollection.COMPLETION_POLL_INTERVAL_SECONDS = 1 FeatureCollection("foo").wait_for_copy() assert 2 == mock_get.call_count
def test_items(self): d = DotDict({ "a": 1, "subdict": { "x": 0, "z": -1 }, "sublist": [{ "y": "foo" }] }) items = d.items() if six.PY2: assert isinstance(items, list) elif six.PY3: assert isinstance(items, DotDict_items) for k, v in items: if isinstance(v, dict): assert isinstance(v, DotDict) v.foo = "bar" if isinstance(v, list): assert isinstance(v, DotList) v.append(None) assert d.subdict.foo == "bar" assert d.sublist[1] is None
def test_items(self): d = DotDict({ "a": 1, "subdict": { "x": 0, "z": -1 }, "sublist": [{ "y": "foo" }] }) items = d.items() if six.PY2: self.assertIsInstance(items, list) elif six.PY3: self.assertIsInstance(items, DotDict_items) for k, v in items: if isinstance(v, dict): self.assertIsInstance(v, DotDict) v.foo = "bar" if isinstance(v, list): self.assertIsInstance(v, DotList) v.append(None) self.assertEqual(d.subdict.foo, "bar") self.assertEqual(d.sublist[1], None)
def test_get(self): d = DotDict({"subdict": {"x": 0}}) subdict = d.get("subdict") self.assertEqual(subdict.x, 0) subdict.foo = "bar" self.assertEqual(d.subdict.foo, "bar") default = d.get("not_here", {"foo": 1}) self.assertEqual(default.foo, 1)
def test_get(self): d = DotDict({"subdict": {"x": 0}}) subdict = d.get("subdict") assert subdict.x == 0 subdict.foo = "bar" assert d.subdict.foo == "bar" default = d.get("not_here", {"foo": 1}) assert default.foo == 1
def test_dottype_within_plain(self): # see note in DotDict.asdict # unsure if this is an important case to handle sub = {i: DotDict(val=i) for i in range(5)} obj = DotDict(sub=sub) unboxed = obj.asdict() assert self.is_unboxed(unboxed)
def test_setdefault(self): d = DotDict({"subdict": {"x": 0}}) default = d.setdefault("subdict", {}) assert default.x == 0 default.foo = "bar" assert d.subdict.foo == "bar" missing = d.setdefault("missing", {"foo": 1}) assert missing.foo == 1 assert d.missing.foo == 1
def test_setdefault(self): d = DotDict({"subdict": {"x": 0}}) default = d.setdefault("subdict", {}) self.assertEqual(default.x, 0) default.foo = "bar" self.assertEqual(d.subdict.foo, "bar") missing = d.setdefault("missing", {"foo": 1}) self.assertEqual(missing.foo, 1) self.assertEqual(d.missing.foo, 1)
def create_features(self, product_id, features, fix_geometry='accept'): """Add multiple features to an existing vector product. :param str product_id: (Required) The ID of the Vector product to which these features will belong. :param list(dict) features: (Required) Each feature must be a dict with a geometry and properties field. If you provide more than 100 features, they will be batched in groups of 100, but consider using :meth:`upload_features()` instead. :param str fix_geometry: String specifying how to handle certain problem geometries, including those which do not follow counter-clockwise winding order (which is required by the GeoJSON spec but not many popular tools). Allowed values are ``reject`` (reject invalid geometries with an error), ``fix`` (correct invalid geometries if possible and use this corrected value when creating the feature), and ``accept`` (the default) which will correct the geometry for internal use but retain the original geometry in the results. :rtype: DotDict :return: The features as a JSON API resource object. The keys are: :: data: A list of 'DotDict' instances. Each instance contains keys as described in create_feature(). :raises ClientError: A variety of http-related exceptions can thrown. If more than 100 features were passed in, some of these may have been successfully inserted, others not. If this is a problem, then stick with <= 100 features. """ if len(features) > 100: logging.warning( 'create_features: feature collection has more than 100 features,' + ' will batch by 100 but consider using upload_features') # forcibly pass a zero-length list for appropriate validation error for i in range(0, max(len(features), 1), 100): attributes = [ dict(feat, **{"fix_geometry": fix_geometry}) for feat in features[i:i + 100] ] jsonapi = self.jsonapi_collection(type="feature", attributes_list=attributes) r = self.session.post('/products/{}/features'.format(product_id), json=jsonapi) if i == 0: result = DotDict(r.json()) else: result.data.extend(DotDict(r.json()).data) return result
def test_values(self): d = DotDict({"subdictA": {"x": 0}, "subdictB": {"x": 1}}) values = d.values() if six.PY2: assert isinstance(values, list) elif six.PY3: assert isinstance(values, DotDict_values) for v in values: assert isinstance(v.x, int) v.foo = "bar" assert d.subdictA.foo == "bar" assert d.subdictB.foo == "bar"
def test_repr(self): d = DotDict(long=list(range(100))) # long lists should be truncated with "..." assert "..." in repr(d) d = DotDict({i: i for i in range(100)}) # a long top-level dict should not be truncated assert d == {i: i for i in range(100)} # short lists and dicts should not be truncated d = DotDict(short=list(range(2)), other_key=list(range(3))) assert d == ast.literal_eval(repr(d))
def test_values(self): d = DotDict({"subdictA": {"x": 0}, "subdictB": {"x": 1}}) values = d.values() if six.PY2: self.assertIsInstance(values, list) elif six.PY3: self.assertIsInstance(values, DotDict_values) for v in values: self.assertIsInstance(v.x, int) v.foo = "bar" self.assertEqual(d.subdictA.foo, "bar") self.assertEqual(d.subdictB.foo, "bar")
def test_repr(self): d = DotDict(long=list(range(100))) # long lists should be truncated with "..." with self.assertRaises((SyntaxError, ValueError)): ast.literal_eval(repr(d)) d = DotDict({i: i for i in range(100)}) # a long top-level dict should not be truncated self.assertEqual(d, {i: i for i in range(100)}) # short lists and dicts should not be truncated d = DotDict(short=list(range(2)), other_key=list(range(3))) self.assertEqual(d, ast.literal_eval(repr(d)))
def create_features(self, product_id, features, correct_winding_order=False): """ Add multiple features to an existing vector product. :param str product_id: (Required) Product to which this feature will belong. :param list(dict) features: (Required) Each feature must be a dict with a geometry and properties field. If more than 100 features, will be batched in groups of 100, but consider using upload_features() instead. :param bool correct_winding_order: Boolean specifying whether to correct Polygon and MultiPolygon features that do not follow counter-clockwise winding order. :rtype: DotDict :return: Created features, as a JSON API resource collection. The new Features' IDs are under ``.data[i].id``, and their properties are under ``.data[i].attributes``. :raises ClientError: A variety of http-related exceptions can thrown. If more than 100 features were passed in, some of these may have been successfully inserted, others not. If this is a problem, then stick with <= 100 features. """ if len(features) > 100: logging.warning( 'create_features: feature collection has more than 100 features,' + ' will batch by 100 but consider using upload_features') # forcibly pass a zero-length list for appropriate validation error for i in range(0, max(len(features), 1), 100): attributes = ([ dict(feat, **{"correct_winding_order": True}) for feat in features[i:i + 100] ] if correct_winding_order else features[i:i + 100]) jsonapi = self.jsonapi_collection(type="feature", attributes_list=attributes) r = self.session.post('/products/{}/features'.format(product_id), json=jsonapi) if i == 0: result = DotDict(r.json()) else: result.data.extend(DotDict(r.json()).data) return result
def __init__(self, geometry, properties, id=None): """ Example ------- >>> polygon = { ... 'type': 'Polygon', ... 'coordinates': [[[-95, 42], [-93, 42], [-93, 40], [-95, 41], [-95, 42]]]} >>> properties = {"temperature": 70.13, "size": "large"} >>> Feature(geometry=polygon, properties=properties) # doctest: +SKIP Feature({ 'geometry': { 'coordinates': (((-95.0, 42.0), (-93.0, 42.0), (-93.0, 40.0), (-95.0, 41.0), (-95.0, 42.0)),), 'type': 'Polygon' }, 'id': None, 'properties': { 'size': 'large', 'temperature': 70.13 } }) """ if geometry is None: raise ValueError("geometry should not be None") self.geometry = shape(geometry) self.properties = DotDict(properties) self.id = id
def shape(self, slug, output='geojson', geom='low'): """Get the geometry for a specific slug :param slug: Slug identifier. :param str output: Desired geometry format (`GeoJSON`). :param str geom: Desired resolution for the geometry (`low`, `medium`, `high`). :return: GeoJSON ``Feature`` Example:: >>> from descarteslabs.client.services import Places >>> kansas = Places().shape('north-america_united-states_kansas') >>> kansas['bbox'] [-102.051744, 36.993016, -94.588658, 40.003078] >>> kansas['geometry']['type'] 'Polygon' >>> kansas['properties'] { 'name': 'Kansas', 'parent_id': 85633793, 'path': 'continent:north-america_country:united-states_region:kansas', 'placetype': 'region', 'slug': 'north-america_united-states_kansas' } """ r = self.session.get('/shape/%s.%s' % (slug, output), params={'geom': geom}) return DotDict(r.json())
def _fetch_upload_result_page(self, product_id, continuation_token=None): r = self.session.get( '/products/{}/features/uploads'.format(product_id), params={'continuation_token': continuation_token}, headers={'Content-Type': 'application/json'}, ) return DotDict(r.json())
def create_features(product_id, attributes, correct_winding_order=False): return DotDict(data=[ dict(id=attr['properties']['id'], attributes=attr) for attr in attributes ])
def test_delattr(self): d = DotDict(delete=0) del d.delete assert "delete" not in d with pytest.raises(AttributeError): del d.delete pass
def create_feature(self, product_id, geometry, properties=None): """ Add a feature to an existing vector product. :param str product_id: (Required) Product to which this feature will belong. :param dict geometry: (Required) Shape associated with this vector feature. This accepts the following types of GeoJSON geometries: - Points - MultiPoints - Polygons - MultiPolygons - LineStrings - MultiLineStrings - GeometryCollections :param dict properties: Dictionary of arbitrary properties. :rtype: DotDict :return: Created Feature, as a JSON API resource collection. The new Feature's ID is under ``.data[0].id``, and its properties are under ``.data[0].attributes``. """ params = dict(geometry=geometry, properties=properties) jsonapi = self.jsonapi_document(type="feature", attributes=params) r = self.session.post('/products/{}/features'.format(product_id), json=jsonapi) return DotDict(r.json())
def get(self, image_id): """Get metadata of a single image. :param str image_id: Image identifier. :return: A dictionary of metadata for a single image. :rtype: DotDict :raises ~descarteslabs.client.exceptions.NotFoundError: Raised if image id cannot be found. Example:: >>> from descarteslabs.client.services import Metadata >>> meta = Metadata().get('landsat:LC08:PRE:TOAR:meta_LC80270312016188_v1') >>> keys = list(meta.keys()) >>> keys.sort() >>> keys ['acquired', 'area', 'bits_per_pixel', 'bright_fraction', 'bucket', 'cloud_fraction', 'cloud_fraction_0', 'confidence_dlsr', 'cs_code', 'descartes_version', 'file_md5s', 'file_sizes', 'files', 'fill_fraction', 'geolocation_accuracy', 'geometry', 'geotrans', 'id', 'identifier', 'key', 'owner_type', 'processed', 'product', 'proj4', 'projcs', 'published', 'raster_size', 'reflectance_scale', 'roll_angle', 'sat_id', 'solar_azimuth_angle', 'solar_elevation_angle', 'storage_state', 'sw_version', 'terrain_correction', 'tile_id'] """ r = self.session.get("/get/{}".format(image_id)) return DotDict(r.json())
def test_delattr(self): d = DotDict(delete=0) del d.delete self.assertNotIn("delete", d) with self.assertRaises(AttributeError): del d.delete pass
def prefix(self, slug, output="geojson", placetype=None, geom="low"): """Get all the places that start with a prefix :param str slug: Slug identifier. :param str output: Desired geometry format (`GeoJSON`, `TopoJSON`). :param str placetype: Restrict results to a particular place type. :param str geom: Desired resolution for the geometry (`low`, `medium`, `high`). :return: GeoJSON or TopoJSON ``FeatureCollection`` Example:: >>> from descarteslabs.client.services import Places >>> il_counties = Places().prefix('north-america_united-states_illinois', placetype='county') >>> len(il_counties['features']) 102 """ params = {} if placetype: params["placetype"] = placetype params["geom"] = geom r = self.session.get("/prefix/%s.%s" % (slug, output), params=params) return DotDict(r.json())
def setUp(self): # date format is locale-dependent, so a hardcoded date string could fail for users from different locales date = datetime.datetime(2015, 6, 1, 14, 25, 10) self.date_str = date.strftime("%c") properties = { "id": "prod:foo", "product": "prod", "crs": "EPSG:32615", "date": date, "bands": collections.OrderedDict([ # necessary to ensure deterministic order in tests ("blue", { "resolution": 5, "resolution_unit": "smoot", "dtype": "UInt16", "data_range": [0, 10000], "physical_range": [0, 1], "data_unit": "TOAR", }), ("alpha", { "resolution": 5, "resolution_unit": "smoot", "dtype": "UInt8", "data_range": [0, 1], "physical_range": [0, 1], }) ]) } properties = DotDict(properties) self.scene = MockScene({}, properties)
def get_product_from_query_status(self, product_id): """Get the status of the job creating a new product from a query. :param str product_id: (Required) The ID of the product for which to to check the status. This ID must have been created by a call to :meth:`create_product_from_query`. :rtype: DotDict :return: A dictionary with information about the status. The keys are :: data: A 'DotDict' instance with the following keys: id: The internal ID for this job. type: 'copy_job'. attributes: A DotDict instance with the following keys: created: Time that the task was created in ISO-8601 UTC. ended: Time that the task completed in ISO-8601 UTC (when available). started: Time that the start stared in ISO-8601 UTC (when available). state: 'PENDING', 'RUNNING', or 'DONE'. """ r = self.session.get( "/products/{}/search/copy/status".format(product_id)) return DotDict(r.json())
def get_product(self, product_id): """ Get a product's properties. :param str product_id: (Required) The ID of the Vector product to fetch. :rtype: DotDict :return: The vector product, as a JSON API resource object. The keys are: :: data: A single 'DotDict' instance with the following keys: id: The ID of the Vectort product. type: 'product'. meta: A single DotDict instance with the following keys: created: Time that the task was created in ISO-8601 UTC. attributes: A single DotDict instance with the following keys: title: The title given to this product. description: The description given to this product. owners: The owners of this product (at a minimum the organization and the user who created this product). readers: The users, groups, or organizations that can read this product. writers: The users, groups, or organizations that can write into this product. """ r = self.session.get('/products/{}'.format(product_id)) return DotDict(r.json())
def dltile_from_latlon(self, lat, lon, resolution, tilesize, pad): """ Return a DLTile GeoJSON Feature that covers a latitude/longitude :param float lat: Requested latitude :param float lon: Requested longitude :param float resolution: Resolution of DLTile :param int tilesize: Number of valid pixels per DLTile :param int pad: Number of ghost pixels per DLTile (overlap among tiles) :return: A DLTile GeoJSON Feature :rtype: DotDict Example:: >>> from descarteslabs.client.services import Raster >>> Raster().dltile_from_latlon(45, 60, 15.0, 1024, 16) { 'geometry': { 'coordinates': [ [ [59.88428127486419, 44.8985115884728...], [60.08463455818353, 44.90380671613201], [60.077403974563175, 45.046212550598135], [59.87655568675822, 45.040891215906676], ... ] ], 'type': 'Polygon' }, 'properties': { 'cs_code': 'EPSG:32641', 'geotrans': [ 254000.0, 15.0, 0, 4992240.0, ... ], 'key': '1024:16:15.0:41:-16:324', 'outputBounds': [254000.0, 4976400.0, 269840.0, 4992240.0], 'pad': 16, 'proj4': '+proj=utm +zone=41 +datum=WGS84 +units=m +no_defs ', 'resolution': 15.0, 'ti': -16, 'tilesize': 1024, 'tj': 324, 'wkt': 'PROJCS["WGS 84 / UTM zone 41N",GEOGCS["WGS...Northing",NORTH],AUTHORITY["EPSG","32641"]]', 'zone': 41 }, 'type': 'Feature' } """ params = {"resolution": resolution, "tilesize": tilesize, "pad": pad} r = self.session.get("/dlkeys/from_latlon/%f/%f" % (lat, lon), params=params) return DotDict(r.json())
def dltile(self, key): """ Given a DLTile key, return a DLTile GeoJSON Feature :param str key: A DLTile key that identifies a DLTile :return: A DLTile GeoJSON Feature :rtype: DotDict :raises descarteslabs.client.exceptions.BadRequestError: if the given key is not a valid DLTile key Example:: >>> from descarteslabs.client.services import Raster >>> Raster().dltile("1024:16:15.0:41:-16:324") { 'geometry': { 'coordinates': [ [ [59.88428127486419, 44.8985115884728...], [60.08463455818353, 44.90380671613201], [60.077403974563175, 45.046212550598135], [59.87655568675822, 45.040891215906676], ... ] ], 'type': 'Polygon' }, 'properties': { 'cs_code': 'EPSG:32641', 'geotrans': [ 254000.0, 15.0, 0, 4992240.0, ... ], 'key': '1024:16:15.0:41:-16:324', 'outputBounds': [254000.0, 4976400.0, 269840.0, 4992240.0], 'pad': 16, 'proj4': '+proj=utm +zone=41 +datum=WGS84 +units=m +no_defs ', 'resolution': 15.0, 'ti': -16, 'tilesize': 1024, 'tj': 324, 'wkt': 'PROJCS["WGS 84 / UTM zone 41N",GEOGCS["WGS...Northing",NORTH],AUTHORITY["EPSG","32641"]]', 'zone': 41 }, 'type': 'Feature' } """ if not key: raise ValueError("Invalid key") r = self.session.get("/dlkeys/%s" % key) return DotDict(r.json())
def get_webhook(self, group_id, webhook_id): r = self.session.get( '/groups/{group_id}/webhooks/{webhook_id}'.format( group_id=group_id, webhook_id=webhook_id, ), ) r.raise_for_status() return DotDict(r.json())