Source code for descarteslabs.vectors.featurecollection

import copy
from shapely.geometry import shape

from descarteslabs.common.dotdict import DotDict
from descarteslabs.client.exceptions import NotFoundError
from descarteslabs.client.deprecation import deprecate
from descarteslabs.common.tasks import UploadTask, ExportTask, TransientResultError
from descarteslabs.client.services.vector import Vector
from descarteslabs.vectors.feature import Feature

# import these exceptions for backwards compatibility
from descarteslabs.vectors.exceptions import (  # noqa
    InvalidQueryException,  # noqa
    VectorException,  # noqa
    FailedCopyError,  # noqa
    WaitTimeoutError,  # noqa
)
from descarteslabs.vectors.async_job import DeleteJob, CopyJob
import six


class _FeaturesIterator(object):
    """Private iterator for features() that also returns length"""

    def __init__(self, response):
        self._response = response

    def __len__(self):
        return len(self._response)

    def __iter__(self):
        return self

    def __next__(self):
        return Feature._create_from_jsonapi(next(self._response))

    def next(self):
        """Backwards compatibility for Python 2"""
        return self.__next__()


[docs]class FeatureCollection(object): """ A proxy object for accesssing millions of features within a collection having similar access controls, geometries and properties. Such a grouping is named a ``product`` and identified by ``id``. If creating a new ``FeatureCollection`` use :meth:`create` instead. Features will not be retrieved from the ``FeatureCollection`` until :meth:`features` is called. Attributes ---------- id : str The unique identifier for this ``FeatureCollection``. name : str (Deprecated) Will be removed in future versions. title : str A more verbose and expressive name for display purposes. description : str Information about the ``FeatureCollection``, why it exists, and what it provides. owners : list(str) User, group, or organization IDs that own this ``FeatureCollection``. Defaults to [``user:current_user``, ``org:current_org``]. The owner can edit, delete, and change access to this ``FeatureCollection``. readers : list(str) User, group, or organization IDs that can read this ``FeatureCollection``. writers : list(str) User, group, or organization IDs that can edit this ``FeatureCollection`` (includes read permission). Note ---- All ``owner``, ``reader``, and ``writer`` IDs must be prefixed with ``email:``, ``user:``, ``group:`` or ``org:``. Using ``org:`` as an ``owner`` will assign those privileges only to administrators for that organization; using ``org:`` as a ``reader`` or ``writer`` assigns those privileges to everyone in that organization. """ ATTRIBUTES = ["owners", "writers", "readers", "id", "name", "title", "description"] COMPLETE_STATUSES = ["DONE", "SUCCESS", "FAILURE"] COMPLETION_POLL_INTERVAL_SECONDS = 5 def __init__(self, id=None, vector_client=None, refresh=True): self.id = id self._vector_client = vector_client self._query_geometry = None self._query_limit = None self._query_property_expression = None if refresh: self.refresh() @classmethod def _from_jsonapi(cls, response, vector_client=None): self = cls(response.id, vector_client=vector_client, refresh=False) self.__dict__.update(response.attributes) return self
[docs] @classmethod @deprecate(renames={"name": "product_id"}) def create( cls, product_id, title, description, owners=None, readers=None, writers=None, vector_client=None, ): """ Create a vector product in your catalog. Parameters ---------- product_id : str A unique name for this product. In the created product a namespace consisting of your user id (e.g. "ae60fc891312ggadc94ade8062213b0063335a3c:") or your organization id (e.g., "yourcompany:") will be prefixed to this, if it doesn't already have one, in order to make the id globally unique. title : str A more verbose and expressive name for display purposes. description : str Information about the ``FeatureCollection``, why it exists, and what it provides. owners : list(str), optional User, group, or organization IDs that own the newly created FeatureCollection. Defaults to [``current user``, ``current org``]. The owner can edit and delete this ``FeatureCollection``. readers : list(str), optional User, group, or organization IDs that can read the newly created ``FeatureCollection``. writers : list(str), optional User, group, or organization IDs that can edit the newly created ``FeatureCollection`` (includes read permission). Returns ------- :class:`FeatureCollection` A new ``FeatureCollection``. Raises ------ ~descarteslabs.client.exceptions.BadRequestError Raised when the request is malformed, e.g. the supplied product id is already in use. ~descarteslabs.client.exceptions.RateLimitError Raised when too many requests have been made within a given time period. ~descarteslabs.client.exceptions.ServerError Raised when a unknown error occurred on the server. Example ------- >>> from descarteslabs.vectors import FeatureCollection >>> FeatureCollection.create(product_id='my-vector-product-id', ... title='My Vector Collection', ... description='Just a test') # doctest: +SKIP """ params = dict( product_id=product_id, title=title, description=description, owners=owners, readers=readers, writers=writers, ) if vector_client is None: vector_client = Vector() return cls._from_jsonapi( vector_client.create_product(**params).data, vector_client )
[docs] @classmethod def list(cls, vector_client=None): """ List all ``FeatureCollection`` products that you have access to. Returns ------- list(:class:`FeatureCollection`) A list of all products that you have access to. Raises ------ ~descarteslabs.client.exceptions.NotFoundError Raised if subsequent pages cannot be found. ~descarteslabs.client.exceptions.RateLimitError Raised when too many requests have been made within a given time period. ~descarteslabs.client.exceptions.ServerError Raised when a unknown error occurred on the server. Example ------- >>> from descarteslabs.vectors import FeatureCollection >>> FeatureCollection.list() # doctest: +SKIP """ if vector_client is None: vector_client = Vector() list = [] page = 1 # The first page will always succeed... response = vector_client.list_products(page=page) while len(response) > 0: partial_list = [ cls._from_jsonapi(fc, vector_client) for fc in response.data ] list.extend(partial_list) page += 1 # Subsequent pages may throw NotFoundError try: response = vector_client.list_products(page=page) except NotFoundError: response = [] return list
@property def vector_client(self): if self._vector_client is None: self._vector_client = Vector() return self._vector_client
[docs] def count(self): """ Return the number of features in the product, regardless of what filters have been applied to the ``FeatureCollection``. Returns ------- int Total number of features in the product. Raises ------ ~descarteslabs.client.exceptions.NotFoundError Raised if the product cannot be found. ~descarteslabs.client.exceptions.RateLimitError Raised when too many requests have been made within a given time period. ~descarteslabs.client.exceptions.ServerError Raised when a unknown error occurred on the server. Example ------- >>> from descarteslabs.vectors import FeatureCollection, properties as p >>> all_us_cities = FeatureCollection('d1349cc2d8854d998aa6da92dc2bd24') # doctest: +SKIP >>> count = all_us_cities.count() # doctest: +SKIP """ return self.vector_client.count_features(self.id)
[docs] def filter(self, geometry=None, properties=None): """ Include only the features matching the given ``geometry`` and ``properties``. Filters are not evaluated until iterating over the ``FeatureCollection``, and can be chained by calling filter multiple times. Parameters ---------- geometry: GeoJSON-like dict, object with ``__geo_interface__``; optional Include features intersecting this geometry. If this ``FeatureCollection`` is already filtered by a geometry, the new geometry will override it -- they cannot be chained. properties : ~descarteslabs.common.property_filtering.filtering.Expression; optional Expression used to filter features 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 (assuming ``from descarteslabs import properties as p``): ``query_expr=(p.temperature >= 50) & (p.hour_of_day > 18)``. More complex example: ``query_expr=(100 > p.temperature >= 50) | ((p.month != 10) & (p.day_of_month > 14))``. If you supply a property which doesn't exist as part of the expression that comparison will evaluate to ``False``. Returns ------- :class:`FeatureCollection` A new ``FeatureCollection`` with the given filter. Raises ------ ~descarteslabs.vectors.exceptions.InvalidQueryException Raised when there is a previously applied geometry filter and a new geometry was provided. ~descarteslabs.client.exceptions.NotFoundError Raised if the product cannot be found. ~descarteslabs.client.exceptions.RateLimitError Raised when too many requests have been made within a given time period. ~descarteslabs.client.exceptions.ServerError Raised when a unknown error occurred on the server. Example ------- >>> from descarteslabs.vectors import FeatureCollection, properties as p >>> aoi_geometry = { ... 'type': 'Polygon', ... 'coordinates': [[[-109, 31], [-102, 31], [-102, 37], [-109, 37], [-109, 31]]]} >>> all_us_cities = FeatureCollection('d1349cc2d8854d998aa6da92dc2bd24') # doctest: +SKIP >>> filtered_cities = all_us_cities.filter(properties=(p.name.like("S%"))) # doctest: +SKIP >>> filtered_cities = filtered_cities.filter(geometry=aoi_geometry) # doctest: +SKIP >>> filtered_cities = filtered_cities.filter(properties=(p.area_land_meters > 1000)) # doctest: +SKIP """ if geometry is not None and self._query_geometry is not None: # Oopsies; we're about to overwrite an existing geometry filter raise InvalidQueryException( "You cannot overwrite an existing geometry filter" ) copied_fc = copy.deepcopy(self) if geometry is not None: copied_fc._query_geometry = getattr(geometry, "__geo_interface__", geometry) if properties is not None: if copied_fc._query_property_expression is None: copied_fc._query_property_expression = properties else: copied_fc._query_property_expression = ( copied_fc._query_property_expression & properties ) return copied_fc
[docs] def limit(self, limit): """ Limit the number of ``Feature`` yielded in :meth:`features`. Parameters ---------- limit : int The number of rows to limit the result to. Returns ------- :class:`FeatureCollection` A new ``FeatureCollection`` with the given limit. Example ------- >>> from descarteslabs.vectors import FeatureCollection >>> fc = FeatureCollection('my-vector-product-id') # doctest: +SKIP >>> fc = fc.limit(10) # doctest: +SKIP """ copied_fc = copy.deepcopy(self) copied_fc._query_limit = limit return copied_fc
[docs] def features(self): """ Iterate through each ``Feature`` in the ``FeatureCollection``, taking into account calls to :meth:`filter` and :meth:`limit`. A query or limit of some sort must be set, otherwise a ``BadRequestError`` will be raised. The length of the returned iterator indicates the full query size. Returns ------- `Iterator` which returns :class:`Feature <descarteslabs.vectors.feature.Feature>` and has a length. Raises ------ ~descarteslabs.client.exceptions.BadRequestError Raised when the request is malformed, e.g. the limit is not a number. ~descarteslabs.client.exceptions.NotFoundError Raised if the product cannot be found. ~descarteslabs.client.exceptions.RateLimitError Raised when too many requests have been made within a given time period. ~descarteslabs.client.exceptions.ServerError Raised when a unknown error occurred on the server. Example ------- >>> from descarteslabs.vectors import FeatureCollection >>> fc = FeatureCollection("a35126a241bd022c026e96ab9fe5e0ea23967d08:USBuildingFootprints") # doctest: +SKIP >>> features = fc.limit(10).features() #doctest: +SKIP >>> print(len(features)) #doctest: +SKIP >>> for feature in features: # doctest: +SKIP ... print(feature) # doctest: +SKIP """ params = dict( product_id=self.id, geometry=self._query_geometry, query_expr=self._query_property_expression, query_limit=self._query_limit, ) return _FeaturesIterator(self.vector_client.search_features(**params))
# TODO: remove name from params
[docs] @deprecate(renames={"name": None}) def update( self, name=None, title=None, description=None, owners=None, readers=None, writers=None, ): """ Updates the attributes of the ``FeatureCollection``. Parameters ---------- name : str, optional (Deprecated) Will be removed in future versions. title : str, optional A more verbose and expressive name for display purposes. description : str, optional Information about the ``FeatureCollection``, why it exists, and what it provides. owners : list(str), optional User, group, or organization IDs that own the FeatureCollection. Defaults to [``current user``, ``current org``]. The owner can edit and delete this ``FeatureCollection``. readers : list(str), optional User, group, or organization IDs that can read the ``FeatureCollection``. writers : list(str), optional User, group, or organization IDs that can edit the ``FeatureCollection`` (includes read permission). Raises ------ ~descarteslabs.client.exceptions.BadRequestError Raised when the request is malformed, e.g. the owners list is missing prefixes. ~descarteslabs.client.exceptions.NotFoundError Raised if the product cannot be found. ~descarteslabs.client.exceptions.RateLimitError Raised when too many requests have been made within a given time period. ~descarteslabs.client.exceptions.ServerError Raised when a unknown error occurred on the server. Example ------- >>> attributes = dict(owners=['email:me@org.com'], ... readers=['group:trusted']) >>> FeatureCollection('my-vector-product-id').update(**attributes) # doctest: +SKIP """ params = dict( title=title, description=description, owners=owners, readers=readers, writers=writers, ) params = {k: v for k, v in six.iteritems(params) if v is not None} response = self.vector_client.update_product(self.id, **params) self.__dict__.update(response["data"]["attributes"])
# TODO: remove name from params
[docs] @deprecate(["title", "description"], renames={"name": None}) def replace( self, name=None, title=None, description=None, owners=None, readers=None, writers=None, ): """ Replaces the attributes of the ``FeatureCollection``. To change a single attribute, see :meth:`update`. Parameters ---------- name : str, optional (Deprecated) Will be removed in future version. title : str (Required) A more verbose name for display purposes. description : str (Required) Information about the ``FeatureCollection``, why it exists, and what it provides. owners : list(str), optional User, group, or organization IDs that own the FeatureCollection. Defaults to [``current user``, ``current org``]. The owner can edit and delete this ``FeatureCollection``. readers : list(str), optional User, group, or organization IDs that can read the ``FeatureCollection``. writers : list(str), optional User, group, or organization IDs that can edit the ``FeatureCollection`` (includes read permission). Raises ------ ~descarteslabs.client.exceptions.BadRequestError Raised when the request is malformed, e.g. the owners list is missing prefixes. ~descarteslabs.client.exceptions.NotFoundError Raised if the product cannot be found. ~descarteslabs.client.exceptions.RateLimitError Raised when too many requests have been made within a given time period. ~descarteslabs.client.exceptions.ServerError Raised when a unknown error occurred on the server. Example ------- >>> attributes = dict(title='title', ... description='description', ... owners=['email:you@org.com'], ... readers=['group:readers'], ... writers=[]) >>> FeatureCollection('my-vector-product-id').replace(**attributes) # doctest: +SKIP """ params = dict( title=title, description=description, owners=owners, readers=readers, writers=writers, ) response = self.vector_client.replace_product(self.id, **params) self.__dict__.update(response["data"]["attributes"])
[docs] def refresh(self): """ Loads the attributes for the ``FeatureCollection``. Example ------- >>> from descarteslabs.vectors import FeatureCollection >>> FeatureCollection('my-vector-product-id').refresh() # doctest: +SKIP """ response = self.vector_client.get_product(self.id) self.__dict__.update(response.data.attributes)
[docs] def delete(self): """ Delete the ``FeatureCollection`` from the catalog. Example ------- >>> from descarteslabs.vectors import FeatureCollection >>> FeatureCollection('my-vector-product-id').delete() # doctest: +SKIP """ self.vector_client.delete_product(self.id)
[docs] def add(self, features, fix_geometry="accept"): """ Add multiple features to an existing ``FeatureCollection``. Parameters ---------- features : ``Feature`` or list(``Feature``) A single feature or list of features to add. Collections of more than 100 features will be batched in groups of 100, but consider using upload() instead. fix_geometry : str 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. Returns ------- list(:class:`Feature <descarteslabs.vectors.feature.Feature>`) A copy of the given list of features that includes the ``id``. Raises ------ ~descarteslabs.client.exceptions.NotFoundError Raised if the product cannot be found. ~descarteslabs.client.exceptions.BadRequestError Raised when the request is malformed. May also indicate that too many features were included. If more than 100 features were provided, some of these features may have been successfuly inserted while others may not have been inserted. ~descarteslabs.client.exceptions.RateLimitError Raised when too many requests have been made within a given time period. ~descarteslabs.client.exceptions.ServerError Raised when a unknown error occurred on the server. Example ------- >>> from descarteslabs.vectors import FeatureCollection, Feature >>> polygon = { ... 'type': 'Polygon', ... 'coordinates': [[[-95, 42],[-93, 42],[-93, 40],[-95, 41],[-95, 42]]]} >>> features = [Feature(geometry=polygon, properties={}) for _ in range(100)] >>> FeatureCollection('my-vector-product-id').add(features) # doctest: +SKIP """ if isinstance(features, Feature): features = [features] attributes = [ {k: v for k, v in f.geojson.items() if k in ["properties", "geometry"]} for f in features ] documents = self.vector_client.create_features( self.id, attributes, fix_geometry=fix_geometry ) copied_features = copy.deepcopy(features) for feature, doc in zip(copied_features, documents.data): feature.id = doc.id if fix_geometry == "fix": feature.geometry = shape(doc.attributes.geometry) return copied_features
[docs] def upload(self, file_ref, max_errors=0, fix_geometry="accept"): """ Asynchronously add features from a file of `Newline Delimited JSON <https://github.com/ndjson/ndjson-spec>`_ features. The file itself will be uploaded synchronously, but loading the features is done asynchronously. Parameters ---------- file_ref : io.IOBase or str An open file object, or a path to the file to upload. max_errors : int The maximum number of errors permitted before declaring failure. fix_geometry : str 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. Returns ------- :class:`UploadTask <descarteslabs.common.tasks.uploadtask.UploadTask>` The upload task. The details may take time to become available so asking for them before they're available will block until the details are available. Raises ------ ~descarteslabs.client.exceptions.NotFoundError Raised if the product cannot be found. ~descarteslabs.client.exceptions.RateLimitError Raised when too many requests have been made within a given time period. ~descarteslabs.client.exceptions.ServerError Raised when a unknown error occurred on the server. Example ------- >>> from descarteslabs.vectors import FeatureCollection, Feature >>> fc = FeatureCollection('my-vector-product-id') # doctest: +SKIP >>> task = fc.upload("/path/to/features.ndjson") # doctest: +SKIP """ upload_id = self.vector_client.upload_features( file_ref, self.id, max_errors=max_errors, fix_geometry=fix_geometry ) return UploadTask(self.id, upload_id=upload_id, client=self.vector_client)
[docs] def list_uploads(self, pending=True): """ Get all the upload tasks for this product. Parameters ---------- pending : bool If ``True`` then include pending/currently running upload tasks in the result, otherwise only include complete upload tasks. Defaults to ``True``. Returns ------- list(:class:`UploadTask <descarteslabs.common.tasks.uploadtask.UploadTask>`) The list of tasks for the product. Raises ------ ~descarteslabs.client.exceptions.NotFoundError Raised if the product cannot be found. ~descarteslabs.client.exceptions.RateLimitError Raised when too many requests have been made within a given time period. ~descarteslabs.client.exceptions.ServerError Raised when a unknown error occurred on the server. Example ------- >>> from descarteslabs.vectors import FeatureCollection, Feature >>> fc = FeatureCollection('my-vector-product-id') # doctest: +SKIP >>> task = fc.upload("/path/to/features.ndjson") # doctest: +SKIP >>> uploads = fc.list_uploads() # doctest: +SKIP """ results = [] for result in self.vector_client.get_upload_results(self.id, pending=pending): # PENDING tasks aren't really tasks yet, and the id is the upload_id, not the task_id if result.attributes and result.attributes.get("status") == "PENDING": results.append( UploadTask( self.id, upload_id=result.id, result_attrs=result.attributes, client=self.vector_client, ) ) else: upload = UploadTask( self.id, tuid=result.id, result_attrs=result.attributes, client=self.vector_client, ) # get_upload_results does not (and cannot) include the task result, # so force an update try: upload.get_result(wait=False) except TransientResultError: pass results.append(upload) return results
[docs] @deprecate(renames={"name": "product_id"}) def copy( self, product_id, title, description, owners=None, readers=None, writers=None ): """ Apply a filter to an existing product and create a new vector product in your catalog from the result, taking into account calls to ``filter`` and ``limit``. A query of some sort must be set, otherwise a ``BadRequestError`` will be raised. Copies occur asynchronously and can take a long time to complete. Features will not be accessible in the new ``FeatureCollection`` until the copy completes. Use :meth:`wait_for_copy` to block until the copy completes. Parameters ---------- product_id : str A unique name for this product. In the created product a namespace consisting of your user id (e.g. "ae60fc891312ggadc94ade8062213b0063335a3c:") or your organization id (e.g., "yourcompany:") will be prefixed to this, if it doesn't already have one, in order to make the id globally unique. title : str A more verbose and expressive name for display purposes. description : str Information about the ``FeatureCollection``, why it exists, and what it provides. owners : list(str), optional User, group, or organization IDs that own the newly created FeatureCollection. Defaults to [``current user``, ``current org``]. The owner can edit and delete this ``FeatureCollection``. readers : list(str), optional User, group, or organization IDs that can read the newly created ``FeatureCollection``. writers : list(str), optional User, group, or organization IDs that can edit the newly created ``FeatureCollection`` (includes read permission). Returns ------- :class:`FeatureCollection` A new ``FeatureCollection``. Raises ------ ~descarteslabs.client.exceptions.BadRequestError Raised when the request is malformed, e.g. no query was specified. ~descarteslabs.client.exceptions.NotFoundError Raised if the product cannot be found. ~descarteslabs.client.exceptions.RateLimitError Raised when too many requests have been made within a given time period. ~descarteslabs.client.exceptions.ServerError Raised when a unknown error occurred on the server. Example ------- >>> from descarteslabs.vectors import FeatureCollection, properties as p >>> aoi_geometry = { ... 'type': 'Polygon', ... 'coordinates': [[[-109, 31], [-102, 31], [-102, 37], [-109, 37], [-109, 31]]]} >>> all_us_cities = FeatureCollection('d1349cc2d8854d998aa6da92dc2bd24') # doctest: +SKIP >>> filtered_cities = all_us_cities.filter(properties=(p.name.like("S%"))) # doctest: +SKIP >>> filtered_cities = filtered_cities.filter(geometry=aoi_geometry) # doctest: +SKIP >>> filtered_cities = filtered_cities.filter(properties=(p.area_land_meters > 1000)) # doctest: +SKIP >>> filtered_cities_fc = filtered_cities.copy(product_id='filtered-cities', ... title='My Filtered US Cities Vector Collection', ... description='A collection of cities in the US') # doctest: +SKIP """ params = dict( product_id=self.id, geometry=self._query_geometry, query_expr=self._query_property_expression, query_limit=self._query_limit, new_product_id=product_id, title=title, description=description, owners=owners, readers=readers, writers=writers, ) return self._from_jsonapi( self.vector_client.create_product_from_query(**params).data )
[docs] def wait_for_copy(self, timeout=None): """ Wait for a copy operation to complete. Copies occur asynchronously and can take a long time to complete. Features will not be accessible in the FeatureCollection until the copy completes. If the product was not created using a copy job, a ``BadRequestError`` is raised. If the copy job ran, but failed, a FailedJobError is raised. If a timeout is specified and the timeout is reached, a ``WaitTimeoutError`` is raised. Parameters ---------- timeout : int Number of seconds to wait before the wait times out. If not specified, will wait indefinitely. Raises ------ ~descarteslabs.vectors.exceptions.FailedJobError Raised when the copy job fails to complete successfully. ~descarteslabs.client.exceptions.NotFoundError Raised if the product or status cannot be found. ~descarteslabs.client.exceptions.RateLimitError Raised when too many requests have been made within a given time period. ~descarteslabs.client.exceptions.ServerError Raised when a unknown error occurred on the server. ~descarteslabs.vectors.exceptions.WaitTimeoutError Raised when the copy job doesn't complete before the timeout is reached. Example ------- >>> from descarteslabs.vectors import FeatureCollection, properties as p >>> aoi_geometry = { ... 'type': 'Polygon', ... 'coordinates': [[[-109, 31], [-102, 31], [-102, 37], [-109, 37], [-109, 31]]]} >>> all_us_cities = FeatureCollection('d1349cc2d8854d998aa6da92dc2bd24') # doctest: +SKIP >>> filtered_cities = all_us_cities.filter(properties=(p.name.like("S%"))) # doctest: +SKIP >>> filtered_cities_fc = filtered_cities.copy(product_id='filtered-cities', ... title='My Filtered US Cities Vector Collection', ... description='A collection of cities in the US') # doctest: +SKIP >>> filtered_cities_fc.wait_for_copy(timeout=120) # doctest: +SKIP """ job = CopyJob(self.id, self.vector_client) job.wait_for_completion(timeout)
[docs] def export(self, key): """ Either export the full product, or the result of a filter chain. The exported geojson features will be stored as a ``data`` file in Descartes Labs Storage. The export will occur asynchronously and can take a long time to complete. The data file will not be accessible until the export is complete. Parameters ---------- key : str The name under which the export will be available in the Storage service. The ``storage_type`` will be ``data``. Note that this will overwrite any existing data if the key already exists. Returns ------- :class:`ExportTask <descarteslabs.common.tasks.exporttask.ExportTask>` The export task. Raises ------ ~descarteslabs.client.exceptions.BadRequestError Raised when the request is malformed, e.g. the query limit is not a number. ~descarteslabs.client.exceptions.NotFoundError Raised if the product cannot be found. ~descarteslabs.client.exceptions.RateLimitError Raised when too many requests have been made within a given time period. ~descarteslabs.client.exceptions.ServerError Raised when a unknown error occurred on the server. Example ------- >>> from descarteslabs.vectors import FeatureCollection, properties as p >>> from descarteslabs import Storage # doctest: +SKIP >>> aoi_geometry = { ... "type": "Polygon", ... "coordinates": [[ # A small area in Washington DC ... [-77.05501556396483, 38.90946877327506], ... [-77.0419692993164, 38.90946877327506], ... [-77.0419692993164, 38.91855139233948], ... [-77.05501556396483, 38.91855139233948], ... [-77.05501556396483, 38.90946877327506] ... ]] ... } >>> buildings = FeatureCollection( ... "a35126a241bd022c026e96ab9fe5e0ea23967d08:USBuildingFootprints") # doctest: +SKIP >>> filtered_buildings = buildings.filter(geometry=aoi_geometry) # doctest: +SKIP >>> task = filtered_buildings.export("my_export") # doctest: +SKIP >>> if task.is_success: # This waits for the task to complete ... task.get_file("some_local_file.geojson") # doctest: +SKIP """ params = dict( product_id=self.id, key=key, geometry=self._query_geometry, query_expr=self._query_property_expression, query_limit=self._query_limit, ) task_id = self.vector_client.export_product_from_query(**params) return ExportTask(self.id, task_id, client=self.vector_client, key=key)
[docs] def list_exports(self): """ Get all the export tasks for this product. Returns ------- list(:class:`ExportTask <descarteslabs.common.tasks.exporttask.ExportTask>`) The list of tasks for the product. Raises ------ ~descarteslabs.client.exceptions.NotFoundError Raised if the product cannot be found. ~descarteslabs.client.exceptions.RateLimitError Raised when too many requests have been made within a given time period. ~descarteslabs.client.exceptions.ServerError Raised when a unknown error occurred on the server. Example ------- >>> from descarteslabs.vectors import FeatureCollection, properties as p >>> aoi_geometry = { ... "type": "Polygon", ... "coordinates": [[ # A small area in Washington DC ... [-77.05501556396483, 38.90946877327506], ... [-77.0419692993164, 38.90946877327506], ... [-77.0419692993164, 38.91855139233948], ... [-77.05501556396483, 38.91855139233948], ... [-77.05501556396483, 38.90946877327506] ... ]] ... } >>> buildings = FeatureCollection( ... "a35126a241bd022c026e96ab9fe5e0ea23967d08:USBuildingFootprints") # doctest: +SKIP >>> filtered_buildings = buildings.filter(geometry=aoi_geometry) # doctest: +SKIP >>> task = filtered_buildings.export("my_export") # doctest: +SKIP >>> exports = filtered_buildings.list_exports() # doctest: +SKIP """ results = [] for result in self.vector_client.get_export_results(self.id): results.append( ExportTask( self.id, tuid=result.id, result_attrs=result.attributes, client=self.vector_client, ) ) return results
[docs] def delete_features(self): """ Apply a filter to a product and delete features that match the filter criteria, taking into account calls to :meth:`filter`. Cannot be used with calls to :meth:`limit` A query of some sort must be set, otherwise a ``BadRequestError`` will be raised. Delete jobs occur asynchronously and can take a long time to complete. You can access :meth:`features` while a delete job is running, but you cannot issue another :meth:`delete_features` until the current job has completed running. Use :meth:`DeleteJob.wait_for_completion() <descarteslabs.vectors.async_job.DeleteJob.wait_for_completion>` to block until the job is done. Returns ------- :class:`DeleteJob <descarteslabs.vectors.async_job.DeleteJob>` A new `DeleteJob`. Raises ------ ~descarteslabs.client.exceptions.BadRequestError Raised when the request is malformed, e.g. the query limit is not a number. ~descarteslabs.vectors.exceptions.InvalidQueryException Raised when a limit was applied to the ``FeatureCollection``. ~descarteslabs.client.exceptions.NotFoundError Raised if the product cannot be found. ~descarteslabs.client.exceptions.RateLimitError Raised when too many requests have been made within a given time period. ~descarteslabs.client.exceptions.ServerError Raised when a unknown error occurred on the server. Example ------- >>> from descarteslabs.vectors import FeatureCollection >>> aoi_geometry = { ... 'type': 'Polygon', ... 'coordinates': [[[-109, 31], [-102, 31], [-102, 37], [-109, 37], [-109, 31]]]} >>> fc = FeatureCollection('my-vector-product-id') # doctest: +SKIP >>> fc.filter(geometry=aoi_geometry) # doctest: +SKIP >>> delete_job = fc.delete_features() # doctest: +SKIP >>> delete_job.wait_for_completion() # doctest: +SKIP """ if self._query_limit: raise InvalidQueryException("limits cannot be used when deleting features") params = dict( product_id=self.id, geometry=self._query_geometry, query_expr=self._query_property_expression, ) product = self.vector_client.delete_features_from_query(**params).data return DeleteJob(product.id)
def _repr_json_(self): return DotDict( (k, v) for k, v in self.__dict__.items() if k in FeatureCollection.ATTRIBUTES ) def __repr__(self): return "FeatureCollection({})".format(repr(self._repr_json_())) def __deepcopy__(self, memo): cls = self.__class__ result = cls.__new__(cls) memo[id(self)] = result for k, v in self.__dict__.items(): if k in ["_vector_client"]: setattr(result, k, v) else: setattr(result, k, copy.deepcopy(v, memo)) return result