From 412d62516da184350de473c87dd2ab369ad4cf86 Mon Sep 17 00:00:00 2001 From: Felix Wang Date: Wed, 23 Feb 2022 17:55:25 -0800 Subject: [PATCH] feat: Add owner field to Entity and rename labels to tags * Add owner field to Entity and rename labels to tags Signed-off-by: Felix Wang * Change all entities in tests to reference tags instead of labels Signed-off-by: Felix Wang * Add deprecation warning for labels argument Signed-off-by: Felix Wang --- .../feast/serving/util/DataGenerator.java | 4 +- protos/feast/core/Entity.proto | 5 +- sdk/python/feast/entity.py | 285 +++++++----------- .../tests/integration/e2e/test_usage_e2e.py | 4 +- .../registration/test_feature_store.py | 16 +- .../integration/registration/test_registry.py | 38 +-- sdk/python/tests/unit/test_entity.py | 14 +- 7 files changed, 143 insertions(+), 223 deletions(-) diff --git a/java/serving/src/test/java/feast/serving/util/DataGenerator.java b/java/serving/src/test/java/feast/serving/util/DataGenerator.java index d53632d0d6..c6c11a9bf7 100644 --- a/java/serving/src/test/java/feast/serving/util/DataGenerator.java +++ b/java/serving/src/test/java/feast/serving/util/DataGenerator.java @@ -116,12 +116,12 @@ public static EntityProto.EntitySpecV2 createEntitySpecV2( String name, String description, ValueProto.ValueType.Enum valueType, - Map labels) { + Map tags) { return EntityProto.EntitySpecV2.newBuilder() .setName(name) .setDescription(description) .setValueType(valueType) - .putAllLabels(labels) + .putAllTags(tags) .build(); } diff --git a/protos/feast/core/Entity.proto b/protos/feast/core/Entity.proto index 7846015c73..cd54c64922 100644 --- a/protos/feast/core/Entity.proto +++ b/protos/feast/core/Entity.proto @@ -48,7 +48,10 @@ message EntitySpecV2 { string join_key = 4; // User defined metadata - map labels = 8; + map tags = 8; + + // Owner of the entity. + string owner = 10; } message EntityMeta { diff --git a/sdk/python/feast/entity.py b/sdk/python/feast/entity.py index 8066eae60a..85045b392c 100644 --- a/sdk/python/feast/entity.py +++ b/sdk/python/feast/entity.py @@ -11,40 +11,46 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import warnings from datetime import datetime from typing import Dict, Optional -import yaml -from google.protobuf import json_format -from google.protobuf.json_format import MessageToDict, MessageToJson +from google.protobuf.json_format import MessageToJson -from feast.loaders import yaml as feast_yaml -from feast.protos.feast.core.Entity_pb2 import Entity as EntityV2Proto +from feast.protos.feast.core.Entity_pb2 import Entity as EntityProto from feast.protos.feast.core.Entity_pb2 import EntityMeta as EntityMetaProto from feast.protos.feast.core.Entity_pb2 import EntitySpecV2 as EntitySpecProto from feast.usage import log_exceptions from feast.value_type import ValueType +warnings.simplefilter("once", DeprecationWarning) + class Entity: """ - Represents a collection of entities and associated metadata. - - Args: - name: Name of the entity. - value_type (optional): The type of the entity, such as string or float. - description (optional): Additional information to describe the entity. - join_key (optional): A property that uniquely identifies different entities - within the collection. Used as a key for joining entities with their - associated features. If not specified, defaults to the name of the entity. - labels (optional): User-defined metadata in dictionary form. + An entity defines a collection of entities for which features can be defined. An + entity can also contain associated metadata. + + Attributes: + name: The unique name of the entity. + value_type: The type of the entity, such as string or float. + join_key: A property that uniquely identifies different entities within the + collection. The join_key property is typically used for joining entities + with their associated features. If not specified, defaults to the name. + description: A human-readable description. + tags: A dictionary of key-value pairs to store arbitrary metadata. + owner: The owner of the feature service, typically the email of the primary + maintainer. + created_timestamp: The time when the entity was created. + last_updated_timestamp: The time when the entity was last updated. """ _name: str _value_type: ValueType - _description: str _join_key: str - _labels: Dict[str, str] + _description: str + _tags: Dict[str, str] + _owner: str _created_timestamp: Optional[datetime] _last_updated_timestamp: Optional[datetime] @@ -55,24 +61,31 @@ def __init__( value_type: ValueType = ValueType.UNKNOWN, description: str = "", join_key: Optional[str] = None, + tags: Dict[str, str] = None, labels: Optional[Dict[str, str]] = None, + owner: str = "", ): """Creates an Entity object.""" self._name = name - self._description = description self._value_type = value_type - if join_key: - self._join_key = join_key - else: - self._join_key = name + self._join_key = join_key if join_key else name + self._description = description - if labels is None: - self._labels = dict() + if labels is not None: + self._tags = labels + warnings.warn( + ( + "The parameter 'labels' is being deprecated. Please use 'tags' instead. " + "Feast 0.20 and onwards will not support the parameter 'labels'." + ), + DeprecationWarning, + ) else: - self._labels = labels + self._tags = labels or tags or {} - self._created_timestamp: Optional[datetime] = None - self._last_updated_timestamp: Optional[datetime] = None + self._owner = owner + self._created_timestamp = None + self._last_updated_timestamp = None def __hash__(self) -> int: return hash((id(self), self.name)) @@ -82,11 +95,12 @@ def __eq__(self, other): raise TypeError("Comparisons should only involve Entity class objects.") if ( - self.labels != other.labels - or self.name != other.name - or self.description != other.description + self.name != other.name or self.value_type != other.value_type or self.join_key != other.join_key + or self.description != other.description + or self.tags != other.tags + or self.owner != other.owner ): return False @@ -97,88 +111,76 @@ def __str__(self): @property def name(self) -> str: - """ - Gets the name of this entity. - """ return self._name @name.setter - def name(self, name): - """ - Sets the name of this entity. - """ + def name(self, name: str): self._name = name @property - def description(self) -> str: - """ - Gets the description of this entity. - """ - return self._description + def value_type(self) -> ValueType: + return self._value_type - @description.setter - def description(self, description): - """ - Sets the description of this entity. - """ - self._description = description + @value_type.setter + def value_type(self, value_type: ValueType): + self._value_type = value_type @property def join_key(self) -> str: - """ - Gets the join key of this entity. - """ return self._join_key @join_key.setter - def join_key(self, join_key): - """ - Sets the join key of this entity. - """ + def join_key(self, join_key: str): self._join_key = join_key @property - def value_type(self) -> ValueType: - """ - Gets the type of this entity. - """ - return self._value_type + def description(self) -> str: + return self._description - @value_type.setter - def value_type(self, value_type: ValueType): - """ - Sets the type of this entity. - """ - self._value_type = value_type + @description.setter + def description(self, description: str): + self._description = description + + @property + def tags(self) -> Dict[str, str]: + return self._tags + + @tags.setter + def tags(self, tags: Dict[str, str]): + self._tags = tags @property def labels(self) -> Dict[str, str]: - """ - Gets the labels of this entity. - """ - return self._labels + return self._tags @labels.setter - def labels(self, labels: Dict[str, str]): - """ - Sets the labels of this entity. - """ - self._labels = labels + def labels(self, tags: Dict[str, str]): + self._tags = tags + + @property + def owner(self) -> str: + return self._owner + + @owner.setter + def owner(self, owner: str): + self._owner = owner @property def created_timestamp(self) -> Optional[datetime]: - """ - Gets the created_timestamp of this entity. - """ return self._created_timestamp + @created_timestamp.setter + def created_timestamp(self, created_timestamp: datetime): + self._created_timestamp = created_timestamp + @property def last_updated_timestamp(self) -> Optional[datetime]: - """ - Gets the last_updated_timestamp of this entity. - """ return self._last_updated_timestamp + @last_updated_timestamp.setter + def last_updated_timestamp(self, last_updated_timestamp: datetime): + self._last_updated_timestamp = last_updated_timestamp + def is_valid(self): """ Validates the state of this entity locally. @@ -187,42 +189,13 @@ def is_valid(self): ValueError: The entity does not have a name or does not have a type. """ if not self.name: - raise ValueError("No name found in entity.") + raise ValueError("The entity does not have a name.") if not self.value_type: - raise ValueError("No type found in entity {self.value_type}") - - @classmethod - def from_yaml(cls, yml: str): - """ - Creates an entity from a YAML string body or a file path. - - Args: - yml: Either a file path containing a yaml file or a YAML string. - - Returns: - An EntityV2 object based on the YAML file. - """ - return cls.from_dict(feast_yaml.yaml_loader(yml, load_single=True)) + raise ValueError(f"The entity {self.name} does not have a type.") @classmethod - def from_dict(cls, entity_dict): - """ - Creates an entity from a dict. - - Args: - entity_dict: A dict representation of an entity. - - Returns: - An EntityV2 object based on the entity dict. - """ - entity_proto = json_format.ParseDict( - entity_dict, EntityV2Proto(), ignore_unknown_fields=True - ) - return cls.from_proto(entity_proto) - - @classmethod - def from_proto(cls, entity_proto: EntityV2Proto): + def from_proto(cls, entity_proto: EntityProto): """ Creates an entity from a protobuf representation of an entity. @@ -230,102 +203,46 @@ def from_proto(cls, entity_proto: EntityV2Proto): entity_proto: A protobuf representation of an entity. Returns: - An EntityV2 object based on the entity protobuf. + An Entity object based on the entity protobuf. """ entity = cls( name=entity_proto.spec.name, - description=entity_proto.spec.description, value_type=ValueType(entity_proto.spec.value_type), - labels=entity_proto.spec.labels, join_key=entity_proto.spec.join_key, + description=entity_proto.spec.description, + tags=entity_proto.spec.tags, + owner=entity_proto.spec.owner, ) if entity_proto.meta.HasField("created_timestamp"): - entity._created_timestamp = entity_proto.meta.created_timestamp.ToDatetime() + entity.created_timestamp = entity_proto.meta.created_timestamp.ToDatetime() if entity_proto.meta.HasField("last_updated_timestamp"): - entity._last_updated_timestamp = ( + entity.last_updated_timestamp = ( entity_proto.meta.last_updated_timestamp.ToDatetime() ) return entity - def to_proto(self) -> EntityV2Proto: + def to_proto(self) -> EntityProto: """ Converts an entity object to its protobuf representation. Returns: - An EntityV2Proto protobuf. + An EntityProto protobuf. """ meta = EntityMetaProto() - if self._created_timestamp: - meta.created_timestamp.FromDatetime(self._created_timestamp) - if self._last_updated_timestamp: - meta.last_updated_timestamp.FromDatetime(self._last_updated_timestamp) + if self.created_timestamp: + meta.created_timestamp.FromDatetime(self.created_timestamp) + if self.last_updated_timestamp: + meta.last_updated_timestamp.FromDatetime(self.last_updated_timestamp) spec = EntitySpecProto( name=self.name, - description=self.description, value_type=self.value_type.value, - labels=self.labels, join_key=self.join_key, - ) - - return EntityV2Proto(spec=spec, meta=meta) - - def to_dict(self) -> Dict: - """ - Converts entity to dict. - - Returns: - Dictionary object representation of entity. - """ - entity_dict = MessageToDict(self.to_proto()) - - # Remove meta when empty for more readable exports - if entity_dict["meta"] == {}: - del entity_dict["meta"] - - return entity_dict - - def to_yaml(self): - """ - Converts a entity to a YAML string. - - Returns: - An entity string returned in YAML format. - """ - entity_dict = self.to_dict() - return yaml.dump(entity_dict, allow_unicode=True, sort_keys=False) - - def to_spec_proto(self) -> EntitySpecProto: - """ - Converts an EntityV2 object to its protobuf representation. - Used when passing EntitySpecV2 object to Feast request. - - Returns: - An EntitySpecV2 protobuf. - """ - spec = EntitySpecProto( - name=self.name, description=self.description, - value_type=self.value_type.value, - labels=self.labels, - join_key=self.join_key, + tags=self.tags, + owner=self.owner, ) - return spec - - def _update_from_entity(self, entity): - """ - Deep replaces one entity with another. - - Args: - entity: Entity to use as a source of configuration. - """ - self.name = entity.name - self.description = entity.description - self.value_type = entity.value_type - self.labels = entity.labels - self.join_key = entity.join_key - self._created_timestamp = entity.created_timestamp - self._last_updated_timestamp = entity.last_updated_timestamp + return EntityProto(spec=spec, meta=meta) diff --git a/sdk/python/tests/integration/e2e/test_usage_e2e.py b/sdk/python/tests/integration/e2e/test_usage_e2e.py index 0bae973063..c7b62b3a5d 100644 --- a/sdk/python/tests/integration/e2e/test_usage_e2e.py +++ b/sdk/python/tests/integration/e2e/test_usage_e2e.py @@ -61,7 +61,7 @@ def test_usage_on(dummy_exporter, enabling_toggle): name="driver_car_id", description="Car driver id", value_type=ValueType.STRING, - labels={"team": "matchmaking"}, + tags={"team": "matchmaking"}, ) test_feature_store.apply([entity]) @@ -100,7 +100,7 @@ def test_usage_off(dummy_exporter, enabling_toggle): name="driver_car_id", description="Car driver id", value_type=ValueType.STRING, - labels={"team": "matchmaking"}, + tags={"team": "matchmaking"}, ) test_feature_store.apply([entity]) diff --git a/sdk/python/tests/integration/registration/test_feature_store.py b/sdk/python/tests/integration/registration/test_feature_store.py index d605865960..d5496a6de7 100644 --- a/sdk/python/tests/integration/registration/test_feature_store.py +++ b/sdk/python/tests/integration/registration/test_feature_store.py @@ -95,7 +95,7 @@ def test_apply_entity_success(test_feature_store): name="driver_car_id", description="Car driver id", value_type=ValueType.STRING, - labels={"team": "matchmaking"}, + tags={"team": "matchmaking"}, ) # Register Entity @@ -109,8 +109,8 @@ def test_apply_entity_success(test_feature_store): and entity.name == "driver_car_id" and entity.value_type == ValueType(ValueProto.ValueType.STRING) and entity.description == "Car driver id" - and "team" in entity.labels - and entity.labels["team"] == "matchmaking" + and "team" in entity.tags + and entity.tags["team"] == "matchmaking" ) test_feature_store.teardown() @@ -129,7 +129,7 @@ def test_apply_entity_integration(test_feature_store): name="driver_car_id", description="Car driver id", value_type=ValueType.STRING, - labels={"team": "matchmaking"}, + tags={"team": "matchmaking"}, ) # Register Entity @@ -143,8 +143,8 @@ def test_apply_entity_integration(test_feature_store): and entity.name == "driver_car_id" and entity.value_type == ValueType(ValueProto.ValueType.STRING) and entity.description == "Car driver id" - and "team" in entity.labels - and entity.labels["team"] == "matchmaking" + and "team" in entity.tags + and entity.tags["team"] == "matchmaking" ) entity = test_feature_store.get_entity("driver_car_id") @@ -152,8 +152,8 @@ def test_apply_entity_integration(test_feature_store): entity.name == "driver_car_id" and entity.value_type == ValueType(ValueProto.ValueType.STRING) and entity.description == "Car driver id" - and "team" in entity.labels - and entity.labels["team"] == "matchmaking" + and "team" in entity.tags + and entity.tags["team"] == "matchmaking" ) test_feature_store.teardown() diff --git a/sdk/python/tests/integration/registration/test_registry.py b/sdk/python/tests/integration/registration/test_registry.py index f3d2b37aac..0394a52cfb 100644 --- a/sdk/python/tests/integration/registration/test_registry.py +++ b/sdk/python/tests/integration/registration/test_registry.py @@ -74,7 +74,7 @@ def test_apply_entity_success(test_registry): name="driver_car_id", description="Car driver id", value_type=ValueType.STRING, - labels={"team": "matchmaking"}, + tags={"team": "matchmaking"}, ) project = "project" @@ -90,8 +90,8 @@ def test_apply_entity_success(test_registry): and entity.name == "driver_car_id" and entity.value_type == ValueType(ValueProto.ValueType.STRING) and entity.description == "Car driver id" - and "team" in entity.labels - and entity.labels["team"] == "matchmaking" + and "team" in entity.tags + and entity.tags["team"] == "matchmaking" ) entity = test_registry.get_entity("driver_car_id", project) @@ -99,8 +99,8 @@ def test_apply_entity_success(test_registry): entity.name == "driver_car_id" and entity.value_type == ValueType(ValueProto.ValueType.STRING) and entity.description == "Car driver id" - and "team" in entity.labels - and entity.labels["team"] == "matchmaking" + and "team" in entity.tags + and entity.tags["team"] == "matchmaking" ) test_registry.delete_entity("driver_car_id", project) @@ -123,7 +123,7 @@ def test_apply_entity_integration(test_registry): name="driver_car_id", description="Car driver id", value_type=ValueType.STRING, - labels={"team": "matchmaking"}, + tags={"team": "matchmaking"}, ) project = "project" @@ -139,8 +139,8 @@ def test_apply_entity_integration(test_registry): and entity.name == "driver_car_id" and entity.value_type == ValueType(ValueProto.ValueType.STRING) and entity.description == "Car driver id" - and "team" in entity.labels - and entity.labels["team"] == "matchmaking" + and "team" in entity.tags + and entity.tags["team"] == "matchmaking" ) entity = test_registry.get_entity("driver_car_id", project) @@ -148,8 +148,8 @@ def test_apply_entity_integration(test_registry): entity.name == "driver_car_id" and entity.value_type == ValueType(ValueProto.ValueType.STRING) and entity.description == "Car driver id" - and "team" in entity.labels - and entity.labels["team"] == "matchmaking" + and "team" in entity.tags + and entity.tags["team"] == "matchmaking" ) test_registry.teardown() @@ -437,7 +437,7 @@ def test_commit(): name="driver_car_id", description="Car driver id", value_type=ValueType.STRING, - labels={"team": "matchmaking"}, + tags={"team": "matchmaking"}, ) project = "project" @@ -454,8 +454,8 @@ def test_commit(): and entity.name == "driver_car_id" and entity.value_type == ValueType(ValueProto.ValueType.STRING) and entity.description == "Car driver id" - and "team" in entity.labels - and entity.labels["team"] == "matchmaking" + and "team" in entity.tags + and entity.tags["team"] == "matchmaking" ) entity = test_registry.get_entity("driver_car_id", project, allow_cache=True) @@ -463,8 +463,8 @@ def test_commit(): entity.name == "driver_car_id" and entity.value_type == ValueType(ValueProto.ValueType.STRING) and entity.description == "Car driver id" - and "team" in entity.labels - and entity.labels["team"] == "matchmaking" + and "team" in entity.tags + and entity.tags["team"] == "matchmaking" ) # Create new registry that points to the same store @@ -489,8 +489,8 @@ def test_commit(): and entity.name == "driver_car_id" and entity.value_type == ValueType(ValueProto.ValueType.STRING) and entity.description == "Car driver id" - and "team" in entity.labels - and entity.labels["team"] == "matchmaking" + and "team" in entity.tags + and entity.tags["team"] == "matchmaking" ) entity = test_registry.get_entity("driver_car_id", project) @@ -498,8 +498,8 @@ def test_commit(): entity.name == "driver_car_id" and entity.value_type == ValueType(ValueProto.ValueType.STRING) and entity.description == "Car driver id" - and "team" in entity.labels - and entity.labels["team"] == "matchmaking" + and "team" in entity.tags + and entity.tags["team"] == "matchmaking" ) test_registry.teardown() diff --git a/sdk/python/tests/unit/test_entity.py b/sdk/python/tests/unit/test_entity.py index b8381451fd..ec3ed70253 100644 --- a/sdk/python/tests/unit/test_entity.py +++ b/sdk/python/tests/unit/test_entity.py @@ -21,21 +21,21 @@ def test_join_key_default(): assert entity.join_key == "my-entity" -def test_entity_class_contains_labels(): +def test_entity_class_contains_tags(): entity = Entity( "my-entity", description="My entity", value_type=ValueType.STRING, - labels={"key1": "val1", "key2": "val2"}, + tags={"key1": "val1", "key2": "val2"}, ) - assert "key1" in entity.labels.keys() and entity.labels["key1"] == "val1" - assert "key2" in entity.labels.keys() and entity.labels["key2"] == "val2" + assert "key1" in entity.tags.keys() and entity.tags["key1"] == "val1" + assert "key2" in entity.tags.keys() and entity.tags["key2"] == "val2" -def test_entity_without_labels_empty_dict(): +def test_entity_without_tags_empty_dict(): entity = Entity("my-entity", description="My entity", value_type=ValueType.STRING) - assert entity.labels == dict() - assert len(entity.labels) == 0 + assert entity.tags == dict() + assert len(entity.tags) == 0 def test_entity_without_description():