Source code for descarteslabs.vector.vector

from __future__ import annotations

from datetime import datetime
from typing import List, Literal, Optional, Tuple, Union

import geopandas as gpd
import pandas as pd
import shapely

from descarteslabs.exceptions import NotFoundError

from ..common.geo import GeoContext
from ..common.property_filtering import Properties
from ..common.vector.models import GenericFeatureBaseModel, VectorBaseModel

# To avoid confusion we import these as <module>_<function>
from .features import Statistic
from .features import add as features_add
from .features import aggregate as features_aggregate
from .features import delete as features_delete
from .features import get as features_get
from .features import join as features_join
from .features import query as features_query
from .features import sjoin as features_sjoin
from .features import update as features_update
from .products import create as products_create
from .products import delete as products_delete
from .products import get as products_get
from .products import list as products_list
from .products import update as products_update
from .vector_client import VectorClient

ACCEPTED_GEOM_TYPES = [
    "Point",
    "MultiPoint",
    "Line",
    "LineString",
    "MultiLine",
    "MultiLineString",
    "Polygon",
    "MultiPolygon",
    "GeometryCollection",
]


# Supporting functions for geometry filtering.


def _geojson_to_shape(gj: dict) -> shapely.geometry.base.BaseGeometry:
    """
    Convert a GeoJSON dict into a shapely shape.

    Parameters
    ----------
    gj: dict
        GeoJSON object

    Returns
    -------
    shp: shapely.geometry.base.BaseGeometry
        Shapely shape for the geojson.
    """
    return shapely.geometry.shape(gj)


def _dl_aoi_to_shape(aoi: GeoContext) -> shapely.geometry.base.BaseGeometry:
    """
    Convert an AOI object into a shapely shape.

    Parameters
    ----------
    aoi: descarteslabs.geo.GeoContext
        AOI for which we want a shapely shape.

    Returns
    -------
    shp: shapely.geometry.base.BaseGeometry
        Shapely shape for this AOI.
    """
    # This only works if we have a geometry or the aoi.bounds_crs is 4326!
    # We have no way in the client to do the right thing otherwise.
    if aoi.geometry is not None:
        return aoi.geometry
    if aoi.bounds_crs == "EPSG:4326":
        return shapely.geometry.box(*list(aoi.bounds))
    raise ValueError(
        "Can't convert aoi to shape. Please provide aoi.geometry or aoi.bounds_crs='EPSG:4326'"
    )


def _to_shape(
    aoi: Optional[Union[GeoContext, dict, shapely.geometry.base.BaseGeometry]] = None
) -> Union[shapely.geometry.base.BaseGeometry, None]:
    """
    Attempt to convert input to a shapely object.

    Raise an exception for non-None values that can't be converted.

    Parameters
    ----------
    aoi: Optional[Union[descarteslabs.geo.GeoContext, dict, shapely.geometry.base.BaseGeometry]]
        Optional AOI to convert to a shapely object.

    Returns
    -------
    shp: Union[shapely.geometry.base.BaseGeometry, None]
        None if aoi is None, or a shapely representation of the aoi.
    """

    if not aoi:
        return None

    # Convert the AOI object to a shapely object so we can
    # perform intersections.
    if isinstance(aoi, dict):
        aoi = _geojson_to_shape(aoi)
    elif issubclass(type(aoi), GeoContext):
        aoi = _dl_aoi_to_shape(aoi)
    elif issubclass(type(aoi), shapely.geometry.base.BaseGeometry):
        return aoi
    else:
        raise TypeError(f"'{aoi}' not recognized as an aoi")

    return aoi


def _shape_to_geojson(shp: shapely.geometry.base.BaseGeometry) -> dict:
    """
    Convert a shapely object into a GeoJSON.

    Parameters
    ----------
    shp: shapely.geometry.base.BaseGeometry

    Returns
    -------
    gj: dict
        GeoJSON dict for this shape
    """
    if shp:
        return shapely.geometry.mapping(shp)
    return None


[docs]class TableOptions: """ A class for controlling Table options and parameters. """ def __init__( self, product_id: str, aoi: Optional[ Union[GeoContext, dict, shapely.geometry.base.BaseGeometry] ] = None, property_filter: Optional[Properties] = None, columns: Optional[List[str]] = None, ): """ Initialize a TableOptions instance. Parameters ---------- product_id: str Product ID of a Vector Table. aoi: Optional[Union[descarteslabs.geo.GeoContext, dict, shapely.geometry.base.BaseGeometry]] AOI to associate with this TableOptions. property_filter: Optional[Properties] Property filter to associate with this TableOptions. columns: Optional[List[str]] List of columns to include with this TableOptions. """ self._product_id = product_id self._aoi = _to_shape(aoi) self._property_filter = property_filter self._columns = columns @property def product_id(self) -> str: """ Return the product ID of this TableOptions. Parameters ---------- None Returns ------- str """ return self._product_id @product_id.setter def product_id(self, product_id: str) -> None: """ Set the product ID of this TableOptions. Parameters ---------- product_id: str Product ID of a Vector Table. Returns ------- None """ if not isinstance(product_id, str): raise TypeError("'product_id' must be of type <str>") self._product_id = product_id @property def aoi(self) -> shapely.geometry.shape: """ Return the AOI option of this TableOptions. Parameters ---------- None Returns ------- shapely.geometry.shape """ return self._aoi @aoi.setter def aoi( self, aoi: Optional[ Union[GeoContext, dict, shapely.geometry.base.BaseGeometry] ] = None, ) -> None: """ Set the AOI option of this TableOptions. Parameters ---------- aoi: Union[descarteslabs.geo.GeoContext, dict, shapely.geometry.base.BaseGeometry] AOI of this TableOptions. Returns ------- None """ self._aoi = _to_shape(aoi) @property def property_filter(self) -> Properties: """ Return the property_filter option of this TableOptions. Parameters ---------- None Returns ------- Properties """ return self._property_filter @property_filter.setter def property_filter(self, property_filter: Optional[Properties] = None) -> None: """ Set the property_filter option of this TableOptions. Parameters ---------- property_filter: Properties property_filter option of this TableOptions. Returns ------- None """ if hasattr(property_filter, "jsonapi_serialize"): self._property_filter = property_filter elif not property_filter: self._property_filter = None else: raise TypeError("'property_filter' must be of type <None> or <Properties>") @property def columns(self) -> List[str]: """ Return the columns option of this TableOptions. Parameters ---------- None Returns ------- list """ return self._columns @columns.setter def columns(self, columns: Optional[List[str]] = None) -> None: """ Set the columns option of this TableOptions. Parameters ---------- columns: List[str] List of columns to include. Returns ------- None """ if isinstance(columns, list): self._columns = columns elif not columns: self._columns = None else: raise TypeError("'columns' must be of type <None> or <list>") self._columns = columns
[docs]class Table: """ A class for creating and interacting with Vector Tables. """ def __init__( self, table_parameters: Union[dict, str], options: TableOptions = None, client: Optional[VectorClient] = None, ): """ Initialize a Vector Table instance. Users should create a Table instance via `Table.get` or `Table.create`. Parameters ---------- product_parameters: Union[dict, str] Dictionary of product parameters or the product ID of a Vector Table. client : VectorClient, optional Client to use for requests. If not provided, the default client will be used. """ if client is None: client = VectorClient.get_default_client() if isinstance(table_parameters, str): table_parameters = products_get(table_parameters, client=client) for k, v in table_parameters.items(): setattr(self, f"_{k}", v) if not options: options = TableOptions(self.id) if not isinstance(options, TableOptions): raise TypeError(("'options' must be of type <TableOptions>")) self.options = options self._client = client
[docs] @staticmethod def get( product_id: str, aoi: Optional[ Union[GeoContext, dict, shapely.geometry.base.BaseGeometry] ] = None, property_filter: Optional[Properties] = None, columns: Optional[List[str]] = [], # noqa: M511 client: Optional[VectorClient] = None, ) -> Table: """ Get a Vector Table instance from a Vector Table product ID. Raise an exception if this `product_id` doesn't exit. Parameters ---------- product_id: str Product ID of the Vector Table. aoi: Optional[Union[descarteslabs.geo.GeoContext, dict, shapely.geometry.base.BaseGeometry]] AOI to associate with this Vector Table. property_filter: Optional[Properties] Property filter to associate with this Vector Table. columns: Optional[List[str]] List of columns to include. client : VectorClient, optional Client to use for requests. If not provided, the default client will be used. Returns ------- Table """ options = TableOptions( product_id=product_id, aoi=aoi, property_filter=property_filter, columns=columns, ) if client is None: client = VectorClient.get_default_client() return Table( table_parameters=products_get(product_id, client=client), options=options, client=client, )
[docs] @staticmethod def create( product_id: str, name: str, description: Optional[str] = None, tags: Optional[List[str]] = None, readers: Optional[List[str]] = None, writers: Optional[List[str]] = None, owners: Optional[List[str]] = None, model: Optional[VectorBaseModel] = GenericFeatureBaseModel, client: Optional[VectorClient] = None, ) -> Table: """ Create a Vector Table. Parameters ---------- product_id : str Product ID of the Vector Table. name : str Name of the Vector Table. description : str, optional Description of the Vector Table. tags : list of str, optional A list of tags to associate with the Vector Table. readers : list of str, optional A list of Vector Table readers. Can take the form "user:{namespace}", "group:{group}", "org:{org}", or "email:{email}". writers : list of str, optional A list of Vector Table writers. Can take the form "user:{namespace}", "group:{group}", "org:{org}", or "email:{email}". owners : list of str, optional A list of Vector Table owners. Can take the form "user:{namespace}", "group:{group}", "org:{org}", or "email:{email}". model : VectorBaseModel, optional A model that provides a user provided schema for the Vector Table. client : VectorClient, optional Client to use for requests. If not provided, the default client will be used. Returns ------- Table """ if client is None: client = VectorClient.get_default_client() return Table( table_parameters=products_create( product_id=product_id, name=name, description=description, tags=tags, readers=readers, writers=writers, owners=owners, model=model, client=client, ), client=client, )
[docs] @staticmethod def list( tags: Optional[List[str]] = None, client: Optional[VectorClient] = None, ) -> List[Table]: """ List available Vector Tables. Parameters ---------- tags: list of str Optional list of tags a Vector Table must have to be returned. client : VectorClient, optional Client to use for requests. If not provided, the default client will be used. Returns ------- List[Table] """ if client is None: client = VectorClient.get_default_client() return [ Table(table_parameters=d, client=client) for d in products_list(tags=tags, client=client) ]
@property def id(self) -> str: """ Return the product ID of this Vector Table. Parameters ---------- None Returns ------- str """ return self._id @property def created(self) -> datetime: """ Return the datetime this Vector Table was created. Parameters ---------- None Returns ------- datetime """ try: return datetime.fromisoformat(self._created) except ValueError: return datetime.strptime(self._created, "%Y-%m-%dT%H:%M:%S.%f") @property def is_spatial(self) -> bool: """ Return a boolean indicating whether or not this Vector Table is spatial. Parameters ---------- None Returns ------- bool """ return self._is_spatial @property def name(self) -> str: """ Return the name of this Vector Table. Parameters ---------- None Returns ------- str """ return self._name @name.setter def name(self, value: str) -> None: """ Set the name of this Vector Table. Parameters ---------- value: str Name of the Vector Table. Returns ------- None """ if isinstance(value, str): self._name = value else: raise TypeError("Table 'name' must be of type <str>") @property def description(self) -> str: """ Return the description of this Vector Table. Parameters ---------- None Returns ------- str """ return self._description @description.setter def description(self, value: str) -> None: """ Set the description of this Vector Table. Parameters ---------- value: str Description of the Vector Table. Returns ------- None """ if isinstance(value, str): self._description = value else: raise TypeError("Table 'description' must be of type <str>") @property def tags(self) -> List[str]: """ Return the tags of this Vector Table. Parameters ---------- None Returns ------- List[str] """ return self._tags @tags.setter def tags(self, value: List[str]) -> None: """ Set the tags for this Vector Table. Parameters ---------- value: List[str] A list of tags to associate with the Vector Table. Returns ------- None """ if isinstance(value, list): self._tags = value else: raise TypeError("Table 'tags' must be of type <list>") @property def readers(self) -> List[str]: """ Return the readers of this Vector Table. Parameters ---------- None Returns ------- List[str] """ return self._readers @readers.setter def readers(self, value: List[str]) -> None: """ Set the readers for this Vector Table. Parameters ---------- value: List[str] Readers for this Vector Table. Returns ------- None """ if isinstance(value, list): self._readers = value else: raise TypeError("Table 'readers' must be of type <list>") @property def writers(self) -> List[str]: """ Return the writers of this Vector Table. Parameters ---------- None Returns ------- List[str] """ return self._writers @writers.setter def writers(self, value: List[str]) -> None: """ Set the writers for the Vector Table. Parameters ---------- value: List[str] Writers for the Vector Table Returns ------- None """ if isinstance(value, list): self._writers = value else: raise TypeError("Table 'writers' must be of type <list>") @property def owners(self) -> List[str]: """ Return the owners of this Vector Table. Parameters ---------- None Returns ------- List[str] """ return self._owners @owners.setter def owners(self, value: List[str]) -> None: """ Set the owners for this Vector Table. Parameters ---------- value: List[str] Owners of this Vector Table. Returns ------- None """ if isinstance(value, list): self._owners = value else: raise TypeError("Table 'owners' must be of type <list>") @property def model(self) -> dict: """ Return the model of this Vector Table. Parameters ---------- None Returns ------- dict """ return self._model @property def columns(self) -> List[str]: """ Return the column names of this Vector Table. Parameters ---------- None Returns ------- List[str] """ return list(self._model["properties"].keys()) @property def parameters(self) -> dict: """ Return the Vector Table parameters as dictionary. Parameters ---------- None Returns ------- dict """ keys = [ "_id", "_name", "_description", "_tags", "_readers", "_writers", "_owners", "_model", ] params = {} for k in keys: params[k.lstrip("_")] = self.__dict__[k] return params def __repr__(self) -> str: """ Generate a string representation of this Vector Table. Parameters ---------- None Return ------ str """ return f"Table: {self.name}\n id: {self.id}\n created: {self.created.strftime('%a %b %d %H:%M:%S %Y')}" def __str__(self) -> str: """ Generate a string representation of this Vector Table. Parameters ---------- None Return ------ str """ return self.__repr__()
[docs] def save(self) -> None: """ Save/update this Vector Table. Parameters ---------- None Return ------ None """ products_update( product_id=self.id, name=self.name, description=self.description, tags=self.tags, readers=self.readers, writers=self.writers, owners=self.owners, client=self._client, )
[docs] def add( self, dataframe: Union[pd.DataFrame, gpd.GeoDataFrame], ) -> Union[pd.DataFrame, gpd.GeoDataFrame]: """ Add a dataframe to this table. If the Vector Table has a `geometry` column the dataframe must be a GeoPandas GeoDataFrame, otherwise a Pandas DataFrame must be provided. Note that the returned dataframe will have UUID attribution for each row. Parameters ---------- dataframe:gpd.GeoDataFrame|pd.DataFrame GeoPandas dataframe to add to this table. Returns ------- Union[pd.DataFrame, gpd.GeoDataFrame] """ return features_add( product_id=self.id, dataframe=dataframe, client=self._client, )
[docs] def get_feature( self, feature_id: str, ) -> Feature: """ Get a Vector Feature from this Vector Table instance. Parameters ---------- feature_id: str Vector Feature ID for the feature to get. Returns ------- Feature """ return Feature.get(id=f"{self.id}:{feature_id}", client=self._client)
[docs] def try_get_feature(self, feature_id: str) -> Feature: """ Get a Vector Feature from this Vector Table instance. Parameters ---------- feature_id: str Vector Feature ID for the feature to get. Returns ------- Feature """ try: return Feature.get(id=f"{self.id}:{feature_id}", client=self._client) except NotFoundError: return None
[docs] def visualize( self, name: str, map, # No type hint because we don't want to require ipyleaflet vector_tile_layer_styles: Optional[dict] = None, override_options: TableOptions = None, ) -> None: """ Visualize this Vector Table as an `ipyleaflet` VectorTileLayer. The property_filter and the columns specified with the Table options will be honored but the AOI option will be ignored. To use this method, you must have the `ipyleaflet` package installed, it is not installed by default when installing the Descartes Labs python client. Parameters ---------- name : str Name to give to the ipyleaflet VectorTileLayer. map: ipyleaflet.leaflet.Map Map to which to add the layer vector_tile_layer_styles : dict, optional Vector tile styles to apply. See https://ipyleaflet.readthedocs.io/en/latest/layers/vector_tile.html for more details. override_options: TableOptions Override options for this query. AOI option is ignored when invoking this method. Returns ------- DLVectorTileLayer Vector tile layer that can be added to an ipyleaflet map. """ # Avoid circular import from .tiles import create_layer options = override_options if override_options else self.options if not isinstance(options, TableOptions): raise TypeError("'options' must be of type <TableOptions>") lyr = create_layer( product_id=self.id, name=name, property_filter=options.property_filter, columns=options.columns, vector_tile_layer_styles={self.id: vector_tile_layer_styles}, ) for layer in map.layers: if layer.name == name: map.remove_layer(layer) break map.add_layer(lyr)
[docs] def collect( self, override_options: TableOptions = None ) -> Union[pd.DataFrame, gpd.GeoDataFrame]: """ Method to execute a query/collect on this Vector Table, returning a dataframe. Table options will be honored when executing the query. If the Vector Table has a `geometry` column and the `geometry` column is included in the Table options, a GeoPandas GeoDataFrame will be returned, otherwise a Pandas DataFrame will be returned. Parameters ---------- override_options: TableOptions Override options for this query. Returns ------- Union[pd.DataFrame, gpd.GeoDataFrame] """ options = override_options if override_options else self.options if not isinstance(options, TableOptions): raise TypeError("'options' must be of type <TableOptions>") return features_query( options.product_id, property_filter=options.property_filter, aoi=_shape_to_geojson(options.aoi), columns=options.columns, client=self._client, )
[docs] def join( self, join_table: [Union[Table, TableOptions]], join_type: Literal["INNER", "LEFT", "RIGHT"], join_columns: List[Tuple[str, str]], override_options: Optional[TableOptions] = None, ) -> Union[pd.DataFrame, gpd.GeoDataFrame]: """ Method to execute a relational join between two Vector Tables, returning a dataframe. Table options will be honored when executing the query. If either Vector Table has a `geometry` column and either Vector Table included the 'geometry' column in the Table options, a GeoPandas GeoDataFrame will be returned, otherwise a Pandas DataFrame will be returned. Parameters ---------- join_table: [Union[Table, TableOptions]] The Vector Table or TableOptions to join. join_type: Literal["INNER", "LEFT", "RIGHT"] The type of join to perform. Must be one of INNER, LEFT, or RIGHT. join_columns: List[Tuple[str, str]] List of column names to join on. Must be formatted as [(table1_col1, table2_col2), ...]. override_options: TableOptions Override options for this query. Returns ------- Union[pd.DataFrame, gpd.GeoDataFrame] """ options = override_options if override_options else self.options if not isinstance(options, TableOptions): raise TypeError("'override_options' must be of type <TableOptions>") if isinstance(join_table, TableOptions): pass elif isinstance(join_table, Table): join_table = join_table.options else: raise TypeError("'join_table' must be of type <TableOptions>") include_columns = [tuple(options.columns), tuple(join_table.columns)] return features_join( input_product_id=options.product_id, join_product_id=join_table.product_id, join_type=join_type, join_columns=join_columns, include_columns=include_columns, input_property_filter=options.property_filter, input_aoi=_shape_to_geojson(options.aoi), join_property_filter=join_table.property_filter, join_aoi=_shape_to_geojson(join_table.aoi), client=self._client, )
[docs] def sjoin( self, join_table: [Union[Table, TableOptions]], join_type: Literal["INTERSECTS", "CONTAINS", "OVERLAPS", "WITHIN"], override_options: Optional[TableOptions] = None, keep_all_input_rows: Optional[bool] = False, ) -> Union[pd.DataFrame, gpd.GeoDataFrame]: """ Method to execute a spatial join between two Vector Tables, returning a dataframe. Table options will be honored when executing the query. Both Vector Tables must have a `geometry` column. If either Vector Table included the 'geometry' column in the Table options, a GeoPandas GeoDataFrame will be returned, otherwise a Pandas DataFrame will be returned. Parameters ---------- join_table: [Union[Table, TableOptions]] The Vector Table or TableOptions to join. join_type: Literal["INTERSECTS", "CONTAINS", "OVERLAPS", "WITHIN"] The type of join to perform. Must be one of INTERSECTS, CONTAINS, OVERLAPS, WITHIN. override_options: TableOptions Override options for this query. Returns ------- Union[pd.DataFrame, gpd.GeoDataFrame] """ options = override_options if override_options else self.options if not isinstance(options, TableOptions): raise TypeError("'override_options' must be of type <TableOptions>") if isinstance(join_table, TableOptions): join_is_spatial = Table.get(product_id=join_table.product_id).is_spatial elif isinstance(join_table, Table): join_is_spatial = join_table.is_spatial join_table = join_table.options else: raise TypeError("'join_table' must be of type <TableOptions>") if not self.is_spatial and not join_is_spatial: raise TypeError("Both Tables must have a geometry column for spatial joins") include_columns = [tuple(options.columns), tuple(join_table.columns)] return features_sjoin( input_product_id=options.product_id, join_product_id=join_table.product_id, join_type=join_type, include_columns=include_columns, input_property_filter=options.property_filter, input_aoi=_shape_to_geojson(options.aoi), join_property_filter=join_table.property_filter, join_aoi=_shape_to_geojson(join_table.aoi), keep_all_input_rows=keep_all_input_rows, client=self._client, )
def _aggregate( self, statistic: Statistic, override_options: TableOptions ) -> Union[int, dict]: """ Private method for handling aggregate functions. The statistic COUNT will always return an integer. All other statistics will return a dictionary of results. Keys of the dictionary will be the column names requested appended with the statistic ('column_1.STATISTIC') and values are the result of the aggregate statistic. Parameters ---------- statistic: Statistic Statistic to calculate. override_options: TableOptions Override options for this query. Returns ------- Union[int, dict] """ options = override_options if override_options else self.options if not isinstance(statistic, Statistic): raise TypeError("'statistic' must be of type <Statistic>") if not isinstance(options, TableOptions): raise TypeError("'options' must be of type <TableOptions>") return features_aggregate( product_id=options.product_id, statistic=statistic, columns=options.columns, property_filter=options.property_filter, aoi=_shape_to_geojson(options.aoi), client=self._client, )
[docs] def count( self, override_options: Optional[TableOptions] = None, ) -> int: """ Method to return the row count of a Vector Table. Table options will be honored when counting rows. Parameters ---------- override_options: TableOptions Override options for this query. Returns ------- int """ return self._aggregate( statistic=Statistic.COUNT, override_options=override_options )
[docs] def sum( self, override_options: Optional[TableOptions] = None, ) -> dict: """ Method to calculate the column sum for this Vector Table. Table options will be honored when calculating the sum. The keys of the returned dictionary correspond to the columns requested, appended with the statistic ('column_1.SUM') and the values are the result of the aggregate statistic. Parameters ---------- override_options: TableOptions Override options for this query. Returns ------- dict """ return self._aggregate( statistic=Statistic.SUM, override_options=override_options )
[docs] def min( self, override_options: Optional[TableOptions] = None, ) -> dict: """ Method to calculate the column minumum for this Vector Table. Table options will be honored when calculating the min. The keys of the returned dictionary correspond to the columns requested, appended with the statistic ('column_1.MIN') and the values are the result of the aggregate statistic. Parameters ---------- override_options: TableOptions Override options for this query. Returns ------- dict """ return self._aggregate( statistic=Statistic.MIN, override_options=override_options )
[docs] def max( self, override_options: Optional[TableOptions] = None, ) -> dict: """ Method to calculate the column maximum for this Vector Table. Table options will be honored when calculating the max. The keys of the returned dictionary correspond to the columns requested, appended with the statistic ('column_1.MAX') and the values are the result of the aggregate statistic. Parameters ---------- override_options: TableOptions Override options for this query. Returns ------- dict """ return self._aggregate( statistic=Statistic.MAX, override_options=override_options )
[docs] def mean( self, override_options: Optional[TableOptions] = None, ) -> dict: """ Method to calculate the column mean/average for this Vector Table. Table options will be honored when calculating the mean. The keys of the returned dictionary correspond to the columns requested, appended with the statistic ('column_1.MEAN') and the values are the result of the aggregate statistic. Parameters ---------- override_options: TableOptions Override options for this query. Returns ------- dict """ return self._aggregate( statistic=Statistic.MEAN, override_options=override_options )
[docs] def reset_options(self) -> None: """ Method to reset/clear current TableOptions. Parameters ---------- None Returns ------- None """ self.options.property_filter = None self.options.columns = None self.options.aoi = None
[docs] def delete(self) -> None: """ Delete this Vector Table. This method will disable all subsequent non-static method calls. Parameters ---------- None Returns ------- None """ products_delete(product_id=self.id, client=self._client)
[docs]class Feature: """ A class for interacting with a Vector Feature. """ def __init__( self, id: str, dataframe: Union[pd.DataFrame, gpd.GeoDataFrame], client: Optional[VectorClient] = None, ): """ Initialize a Vector Feature instance. Users should create a Vector Feature instance via `Table.get_feature` or `Feature.get`. Parameters ---------- id: str ID of the Vector Feature. dataframe: Union[pd.DataFrame, gpd.GeoDataFrame] Pandas DataFrame or a GeoPandas GeoDataFrame. client : VectorClient, optional Client to use for requests. If not provided, the default client will be used. """ try: pid, fid = id.rsplit(":", 1) except ValueError: raise ValueError("Invalid Feature ID") from None if isinstance(dataframe, gpd.GeoDataFrame): self._is_spatial = True elif isinstance(dataframe, pd.DataFrame): self._is_spatial = False else: raise TypeError( "'dataframe' must be of type <pd.DataFrame> or <gpd.GeoDataFrame>" ) self._id = id self._values = {} for k, v in dataframe.to_dict().items(): self._values[k] = v[0] # I don't think we need to initialize this if it is None self._client = client def __repr__(self) -> str: """ Generate a string representation of this Vector Feature. Parameters ---------- None Return ------ str """ return f"Feature: {self.name}\n id: {self.id}\n table: {self.product_id}" def __str__(self) -> str: """ Generate a string representation of this Vector Feature. Parameters ---------- None Return ------ str """ return self.__repr__() @property def is_spatial(self) -> bool: """ Return a boolean indicating whether or not this Vector Feature is spatial. Parameters ---------- None Returns ------- bool """ return self._is_spatial @property def values(self) -> dict: """ Return a dictionary of colum/value pairs for this Vector Feature. Returns ------- dict """ return self._values @values.setter def values(self, key, value) -> None: """ Set a colum/value pair for this Vector Feature. Returns ------- None """ self._values[key] = value @property def id(self) -> str: """ Return the ID of this Vector Feature. Returns ------- str """ return self._id @property def product_id(self) -> str: """ Return the Vector Table product ID of this Vector Feature. Returns ------- str """ return self._id.rsplit(":", 1)[0] @property def name(self) -> str: """ Return the name/uuid of ths Vector Feature. Returns ------- str """ return self._id.rsplit(":", 1)[1] @property def table(self) -> Table: """ Return the Vector Table of this Vector Feature. Returns ------- Table """ return Table.get(product_id=self.product_id)
[docs] @staticmethod def get( id: str, client: Optional[VectorClient] = None, ) -> Feature: """ Get a Vector Feature instance associated with an ID. Parameters ---------- id: str ID of the Vector Feature. client : VectorClient, optional Client to use for requests. If not provided, the default client will be used. Returns ------- Feature """ try: pid, fid = id.rsplit(":", 1) except ValueError: raise ValueError("Invalid Feature ID") from None if client is None: client = VectorClient.get_default_client() dataframe = features_get(product_id=pid, feature_id=fid, client=client) return Feature(id=id, dataframe=dataframe, client=client)
[docs] def save(self) -> None: """ Save/update this Vector Feature. Parameters ---------- None Returns ------- None """ if self.is_spatial: dataframe = gpd.GeoDataFrame.from_features([self], crs="EPSG:4326") else: dataframe = pd.DataFrame([self.values]) features_update( product_id=self.product_id, feature_id=self.name, dataframe=dataframe, client=self._client, )
[docs] def delete(self) -> None: """ Delete this Vector Feature. Parameters ---------- None Returns ------- None """ features_delete( product_id=self.product_id, feature_id=self.name, client=self._client )
@property def __geo_interface__(self) -> Union[dict, None]: if self.is_spatial: return { "geometry": self.values["geometry"].__geo_interface__, "properties": { c: self.values[c] for c in self.table.columns if c != "geometry" }, } return None