예제 #1
0
    def test_non_default_auto_id_field_construction(self):
        assert "id" not in declared_fields(PersonAutoSSN)
        assert "id" not in attributes(PersonAutoSSN)

        assert type(declared_fields(PersonAutoSSN)["ssn"]) is Auto
        assert id_field(PersonAutoSSN).field_name == "ssn"
        assert id_field(PersonAutoSSN) == declared_fields(PersonAutoSSN)["ssn"]
예제 #2
0
    def test_id_field_in_meta(self):
        assert hasattr(Person, _ID_FIELD_NAME)
        assert id_field(Person) is not None
        assert id_field(Person) == declared_fields(Person)["person_id"]

        assert type(id_field(Person)) is Identifier
        declared_fields(Person)["person_id"].identifier is True
예제 #3
0
    def test_non_default_explicit_id_field_construction(self, test_domain):
        assert "id" not in declared_fields(PersonExplicitID)
        assert "id" not in attributes(PersonExplicitID)

        assert type(declared_fields(PersonExplicitID)["ssn"]) is String
        assert id_field(PersonExplicitID).field_name == "ssn"
        assert id_field(PersonExplicitID) == declared_fields(
            PersonExplicitID)["ssn"]
예제 #4
0
    def test_view_meta_attributes(self):
        assert hasattr(Person, "meta_")
        assert type(Person.meta_) is Options

        # Persistence attributes
        # FIXME Should these be present as part of Views, or a separate Model?
        assert hasattr(Person.meta_, "abstract")
        assert hasattr(Person.meta_, "schema_name")
        assert hasattr(Person.meta_, "provider")
        assert hasattr(Person.meta_, "cache")

        assert id_field(Person) is not None
        assert id_field(Person) == declared_fields(Person)["person_id"]
예제 #5
0
    def test_id_field_recognition(self):
        assert "person_id" in declared_fields(Person)
        assert "person_id" in attributes(Person)

        assert type(declared_fields(Person)["person_id"]) is Identifier
        assert id_field(Person) == declared_fields(Person)["person_id"]
        declared_fields(Person)["person_id"].identifier is True
예제 #6
0
    def __set__(self, instance, value):
        """Setup relationship to be persisted/updated"""
        if value is not None:
            # This updates the parent's unique identifier in the child
            #   so that the foreign key relationship is preserved
            id_value = getattr(instance, id_field(instance).field_name)
            linked_attribute = self._linked_attribute(instance.__class__)
            if hasattr(value, linked_attribute):
                setattr(
                    value, linked_attribute, id_value
                )  # This overwrites any existing linkage, which is correct

        current_value = getattr(instance, self.field_name)
        if current_value is None:
            self.change = "ADDED"
        elif value is None:
            self.change = "DELETED"
            self.change_old_value = self.value
        elif current_value.id != value.id:
            self.change = "UPDATED"
            self.change_old_value = self.value
        elif current_value.id == value.id and value.state_.is_changed:
            self.change = "UPDATED"
        else:
            self.change = None  # The same object has been assigned, No-Op

        self._set_own_value(instance, value)
예제 #7
0
파일: mixins.py 프로젝트: proteanhq/protean
    def to_event_message(cls, event: BaseEvent) -> Message:
        # FIXME Should one of `aggregate_cls` or `stream_name` be mandatory?
        if not (event.meta_.aggregate_cls or event.meta_.stream_name):
            raise IncorrectUsageError({
                "_entity": [
                    f"Event `{event.__class__.__name__}` needs to be associated with an aggregate or a stream"
                ]
            })

        if has_id_field(event):
            identifier = getattr(event, id_field(event).field_name)
        else:
            identifier = str(uuid4())

        # Use explicit stream name if provided, or fallback on Aggregate's stream name
        stream_name = (event.meta_.stream_name
                       or event.meta_.aggregate_cls.meta_.stream_name)

        return cls(
            stream_name=f"{stream_name}-{identifier}",
            type=fully_qualified_name(event.__class__),
            data=event.to_dict(),
            metadata=MessageMetadata(
                kind=MessageType.EVENT.value,
                owner=current_domain.domain_name,
                **cls.derived_metadata(MessageType.EVENT.value),
            )
            # schema_version=command.meta_.version,  # FIXME Maintain version for event
        )
예제 #8
0
    def _create(self, model_obj):
        """Write a record to the dict repository"""
        conn = self._get_session()

        # Update the value of the counters
        model_obj = self._set_auto_fields(model_obj)

        # Add the entity to the repository
        identifier = model_obj[id_field(self.entity_cls).field_name]
        with conn._db["lock"]:
            # Check if object is present
            if identifier in conn._db["data"][self.schema_name]:
                raise ValidationError({
                    "_entity":
                    f"`{self.__class__.__name__}` object with identifier {identifier} "
                    f"is already present."
                })

            conn._db["data"][self.schema_name][identifier] = model_obj

        if not current_uow:
            conn.commit()
            conn.close()

        return model_obj
예제 #9
0
    def to_entity(cls, item: "ElasticsearchModel"):
        """Convert the elasticsearch document to an entity"""
        item_dict = {}

        # Convert the values in ES Model as a dictionary
        values = item.to_dict()
        for field_name in attributes(cls.meta_.entity_cls):
            item_dict[field_name] = values.get(field_name, None)

        identifier = None
        if (current_domain.config["IDENTITY_STRATEGY"]
                == IdentityStrategy.UUID.value
                and current_domain.config["IDENTITY_TYPE"]
                == IdentityType.UUID.value and isinstance(item.meta.id, str)):
            identifier = UUID(item.meta.id)
        else:
            identifier = item.meta.id

        # Elasticsearch stores identity in a special field `meta.id`.
        # Extract identity from `meta.id` and set identifier
        id_field_name = id_field(cls.meta_.entity_cls).field_name
        item_dict[id_field_name] = identifier

        # Set version from document meta, only if `_version` attr is present
        if hasattr(cls.meta_.entity_cls, "_version"):
            item_dict["_version"] = item.meta.version

        entity_obj = cls.meta_.entity_cls(item_dict)

        return entity_obj
예제 #10
0
파일: mixins.py 프로젝트: proteanhq/protean
    def to_command_message(cls, command: BaseCommand) -> Message:
        # FIXME Should one of `aggregate_cls` or `stream_name` be mandatory?
        if not (command.meta_.aggregate_cls or command.meta_.stream_name):
            raise IncorrectUsageError({
                "_entity": [
                    f"Command `{command.__class__.__name__}` needs to be associated with an aggregate or a stream"
                ]
            })

        # Use the value of an identifier field if specified, or generate a new uuid
        if has_id_field(command):
            identifier = getattr(command, id_field(command).field_name)
        else:
            identifier = str(uuid4())

        # Use explicit stream name if provided, or fallback on Aggregate's stream name
        stream_name = (command.meta_.stream_name
                       or command.meta_.aggregate_cls.meta_.stream_name)

        return cls(
            stream_name=f"{stream_name}:command-{identifier}",
            type=fully_qualified_name(command.__class__),
            data=command.to_dict(),
            metadata=MessageMetadata(
                kind=MessageType.COMMAND.value,
                owner=current_domain.domain_name,
                **cls.derived_metadata(MessageType.COMMAND.value),
            )
            # schema_version=command.meta_.version,  # FIXME Maintain version for command
        )
예제 #11
0
def test_id_field_for_command_with_identifier():
    assert has_id_field(Registered) is True

    field = id_field(Registered)

    assert field is not None
    assert field.field_name == "user_id"
예제 #12
0
    def _linked_attribute(self, owner):
        """Choose the Linkage attribute between `via` and own entity's `id_field`

        FIXME Explore converting this method into an attribute, and treating it
        uniformly at `association` level.
        """
        return self.via or (utils.inflection.underscore(owner.__name__) + "_" +
                            id_field(owner).attribute_name)
예제 #13
0
파일: entity.py 프로젝트: proteanhq/protean
    def __eq__(self, other):
        """Equivalence check to be based only on Identity"""

        # FIXME Enhanced Equality Checks
        #   * Ensure IDs have values and both of them are not null
        #   * Ensure that the ID is of the right type
        #   * Ensure that Objects belong to the same `type`
        #   * Check Reference equality

        # FIXME Check if `==` and `in` operator work with __eq__

        if type(other) is type(self):
            self_id = getattr(self, id_field(self).field_name)
            other_id = getattr(other, id_field(other).field_name)

            return self_id == other_id

        return False
예제 #14
0
    def _update(self, model_obj):
        """Update a record in the sqlalchemy database"""
        conn = self._get_session()
        db_item = None

        # Fetch the record from database
        try:
            identifier = getattr(model_obj,
                                 id_field(self.entity_cls).attribute_name)
            db_item = conn.query(self.model_cls).get(
                identifier
            )  # This will raise exception if object was not found
        except DatabaseError as exc:
            logger.error(f"Database Record not found: {exc}")
            raise

        if db_item is None:
            conn.rollback()
            conn.close()
            raise ObjectNotFoundError({
                "_entity":
                f"`{self.entity_cls.__name__}` object with identifier {identifier} "
                f"does not exist."
            })

        # Sync DB Record with current changes. When the session is committed, changes are automatically synced
        try:
            for attribute in attributes(self.entity_cls):
                if attribute != id_field(
                        self.entity_cls).attribute_name and getattr(
                            model_obj, attribute) != getattr(
                                db_item, attribute):
                    setattr(db_item, attribute, getattr(model_obj, attribute))
        except DatabaseError as exc:
            logger.error(f"Error while updating: {exc}")
            raise
        finally:
            if not current_uow:
                conn.commit()
                conn.close()

        return model_obj
예제 #15
0
    def add(self, instance, items):
        data = getattr(instance, self.field_name)

        # Convert a single item into a list of items, if necessary
        items = [items] if not isinstance(items, list) else items

        current_value_ids = [value.id for value in data]

        for item in items:
            # Items to add
            if item.id not in current_value_ids:
                # If the same item is added multiple times, the last item added will win
                instance._temp_cache[self.field_name]["added"][item.id] = item

                setattr(
                    item,
                    self._linked_attribute(type(instance)),
                    getattr(instance,
                            id_field(instance).field_name),
                )

                # Reset Cache
                self.delete_cached_value(instance)
            # Items to update
            elif (item.id in current_value_ids and item.state_.is_persisted
                  and item.state_.is_changed):
                setattr(
                    item,
                    self._linked_attribute(type(instance)),
                    getattr(instance,
                            id_field(instance).field_name),
                )

                instance._temp_cache[self.field_name]["updated"][
                    item.id] = item

                # Reset Cache
                self.delete_cached_value(instance)
예제 #16
0
    def linked_attribute(self):
        """Choose the Linkage attribute between `via` and designated `id_field` of the target class

        This method is initially called from `__set_name__()` -> `get_attribute_name()`
        at which point, the `to_cls` has not been initialized properly. We simply default
        the linked attribute to 'id' in that case.

        Eventually, when setting value the first time, the `to_cls` entity is initialized
        and the attribute name is reset correctly.
        """
        if isinstance(self.to_cls, str):
            return "id"
        else:
            return self.via or id_field(self.to_cls).attribute_name
예제 #17
0
    def __get__(self, instance, owner):
        """Retrieve associated objects"""

        try:
            reference_obj = self.get_cached_value(instance)
        except KeyError:
            # Fetch target object by own Identifier
            id_value = getattr(instance, id_field(instance).field_name)
            reference_obj = self._fetch_objects(instance,
                                                self._linked_attribute(owner),
                                                id_value)

            self._set_own_value(instance, reference_obj)

        return reference_obj
예제 #18
0
파일: dao.py 프로젝트: proteanhq/protean
    def _validate_and_update_version(self, entity_obj) -> None:
        if entity_obj.state_.is_persisted:
            identifier = getattr(entity_obj,
                                 id_field(self.entity_cls).field_name)
            persisted_entity = self.get(identifier)

            if persisted_entity._version != entity_obj._version:
                raise ExpectedVersionError(
                    f"Wrong expected version: {entity_obj._version} "
                    f"(Aggregate: {self.entity_cls.__name__}({identifier}), Version: {persisted_entity._version})"
                )

            entity_obj._version += 1
        else:
            entity_obj._version = 0
예제 #19
0
    def __set__(self, instance, value):
        """Override `__set__` to coordinate between relation field and its shadow attribute"""
        value = self._load(value)

        if value:
            # Check if the reference object has been saved. Otherwise, throw ValueError
            # FIXME not a comprehensive check. Should refer to state
            if getattr(value, id_field(value).field_name) is None:
                raise ValueError(
                    "Target Object must be saved before being referenced",
                    self.field_name,
                )
            else:
                self._set_own_value(instance, value)
                self._set_relation_value(instance,
                                         getattr(value, self.linked_attribute))
        else:
            self._reset_values(instance)
예제 #20
0
파일: memory.py 프로젝트: proteanhq/protean
    def add(self, view: BaseView, ttl: Optional[Union[int, float]] = None) -> None:
        """Add view record to cache

        KEY: View ID
        Value: View Data (derived from `to_dict()`)

        TTL is in seconds.

        Args:
            view (BaseView): View Instance containing data
            ttl (int, float, optional): Timeout in seconds. Defaults to None.
        """
        identifier = getattr(view, id_field(view).field_name)
        key = f"{underscore(view.__class__.__name__)}:::{identifier}"

        self._db[key] = view.to_dict()

        if ttl:
            self._db.set_ttl(key, ttl)
예제 #21
0
파일: mixins.py 프로젝트: proteanhq/protean
    def to_aggregate_event_message(cls, aggregate: BaseEventSourcedAggregate,
                                   event: BaseEvent) -> Message:
        identifier = getattr(aggregate, id_field(aggregate).field_name)

        # Use explicit stream name if provided, or fallback on Aggregate's stream name
        stream_name = (event.meta_.stream_name
                       or event.meta_.aggregate_cls.meta_.stream_name)

        return cls(
            stream_name=f"{stream_name}-{identifier}",
            type=fully_qualified_name(event.__class__),
            data=event.to_dict(),
            metadata=MessageMetadata(
                kind=MessageType.EVENT.value,
                owner=current_domain.domain_name,
                **cls.derived_metadata(MessageType.EVENT.value)
                # schema_version=event.meta_.version,  # FIXME Maintain version for event
            ),
            expected_version=aggregate.
            _version,  # FIXME Maintain version for Aggregates
        )
예제 #22
0
    def from_entity(cls, entity) -> "ElasticsearchModel":
        """Convert the entity to a Elasticsearch record"""
        item_dict = {}
        for attribute_obj in attributes(cls.meta_.entity_cls).values():
            if isinstance(attribute_obj, Reference):
                item_dict[attribute_obj.relation.
                          attribute_name] = attribute_obj.relation.value
            else:
                item_dict[attribute_obj.attribute_name] = getattr(
                    entity, attribute_obj.attribute_name)

        model_obj = cls(**item_dict)

        # Elasticsearch stores identity in a special field `meta.id`.
        # Set `meta.id` to the identifier set in entity
        id_field_name = id_field(cls.meta_.entity_cls).field_name

        if id_field_name in item_dict:
            model_obj.meta.id = item_dict[id_field_name]

        return model_obj
예제 #23
0
    def _delete(self, model_obj):
        """Delete the entity record in the dictionary"""
        conn = self._get_session()

        identifier = model_obj[id_field(self.entity_cls).field_name]
        with conn._db["lock"]:
            # Check if object is present
            if identifier not in conn._db["data"][self.schema_name]:
                raise ObjectNotFoundError({
                    "_entity":
                    f"`{self.entity_cls.__name__}` object with identifier {identifier} "
                    f"does not exist."
                })

            del conn._db["data"][self.schema_name][identifier]

        if not current_uow:
            conn.commit()
            conn.close()

        return model_obj
예제 #24
0
    def construct_model_class(self, entity_cls):
        """Return a fully-baked Model class for a given Entity class"""
        model_cls = None

        # Return the model class if it was already seen/decorated
        if entity_cls.meta_.schema_name in self._model_classes:
            model_cls = self._model_classes[entity_cls.meta_.schema_name]
        else:
            from protean.core.model import ModelMeta

            meta_ = ModelMeta()
            meta_.entity_cls = entity_cls

            # Construct Inner Index class with options
            options = {}
            options["name"] = self.derive_schema_name(entity_cls)
            if "SETTINGS" in self.conn_info and self.conn_info["SETTINGS"]:
                options["settings"] = self.conn_info["SETTINGS"]

            index_cls = type("Index", (object, ), options)

            attrs = {"meta_": meta_, "Index": index_cls}

            # FIXME Ensure the custom model attributes are constructed properly
            model_cls = type(entity_cls.__name__ + "Model",
                             (ElasticsearchModel, ), attrs)

            # Create Dynamic Mapping and associate with index
            # FIXME Expand to all types of fields
            id_field_name = id_field(entity_cls).field_name
            m = Mapping()
            m.field(id_field_name, Keyword())

            model_cls._index.mapping(m)

            # Memoize the constructed model class
            self._model_classes[entity_cls.meta_.schema_name] = model_cls

        # Set Entity Class as a class level attribute for the Model, to be able to reference later.
        return model_cls
예제 #25
0
    def add(self,
            view: BaseView,
            ttl: Optional[Union[int, float]] = None) -> None:
        """Add view record to cache

        KEY: View ID
        Value: View Data (derived from `to_dict()`)

        TTL is in seconds. If not specified explicitly in method call,
        it is picked up from Redis broker configuration. In the absence of
        configuration, it is set to 300 seconds.

        Args:
            view (BaseView): View Instance containing data
            ttl (int, float, optional): Timeout in seconds. Defaults to None.
        """
        identifier = getattr(view, id_field(view).field_name)
        key = f"{underscore(view.__class__.__name__)}:::{identifier}"

        ttl = ttl or self.conn_info.get("TTL") or 300

        self.r.psetex(key, int(ttl * 1000), json.dumps(view.to_dict()))
예제 #26
0
파일: dao.py 프로젝트: proteanhq/protean
    def get(self, identifier: Any) -> BaseEntity:
        """Retrieve a specific Record from the Repository by its `identifier`.

        This method internally uses the `filter` method to fetch records.

        Returns exactly one record that matches the identifier.

        Throws `ObjectNotFoundError` if no record was found for the identifier.

        Throws `TooManyObjectsError` if multiple records were found for the identifier.

        :param identifier: id of the record to be fetched from the data store.
        """
        logger.debug(
            f"Lookup `{self.entity_cls.__name__}` object with identifier {identifier}"
        )

        # Filter on the ID field of the entity
        filters = {
            id_field(self.entity_cls).field_name: identifier,
        }

        results = self.query.filter(**filters).all()
        if not results:
            raise ObjectNotFoundError({
                "_entity":
                f"`{self.entity_cls.__name__}` object with identifier {identifier} "
                f"does not exist."
            })

        if len(results) > 1:
            raise TooManyObjectsError(
                f"More than one object of `{self.entity_cls.__name__}` exist with identifier {identifier}",
            )

        # Return the first result, because `filter` would have returned an array
        return results.first
예제 #27
0
    def _delete(self, model_obj):
        """Delete the entity record in the dictionary"""
        conn = self._get_session()
        db_item = None

        # Fetch the record from database
        try:
            identifier = getattr(model_obj,
                                 id_field(self.entity_cls).attribute_name)
            db_item = conn.query(self.model_cls).get(
                identifier
            )  # This will raise exception if object was not found
        except DatabaseError as exc:
            logger.error(f"Database Record not found: {exc}")
            raise

        if db_item is None:
            conn.rollback()
            conn.close()
            raise ObjectNotFoundError({
                "_entity":
                f"`{self.entity_cls.__name__}` object with identifier {identifier} "
                f"does not exist."
            })

        try:
            conn.delete(db_item)
        except DatabaseError as exc:
            logger.error(f"Error while deleting: {exc}")
            raise
        finally:
            if not current_uow:
                conn.commit()
                conn.close()

        return model_obj
예제 #28
0
파일: entity.py 프로젝트: proteanhq/protean
 def __str__(self):
     identifier = getattr(self, id_field(self).field_name)
     return "%s object (%s)" % (
         self.__class__.__name__,
         "{}: {}".format(id_field(self).field_name, identifier),
     )
예제 #29
0
파일: entity.py 프로젝트: proteanhq/protean
    def __hash__(self):
        """Overrides the default implementation and bases hashing on identity"""

        # FIXME Add Object Class Type to hash
        return hash(getattr(self, id_field(self).field_name))
예제 #30
0
 def remove(self, view):
     identifier = getattr(view, id_field(view).field_name)
     key = f"{underscore(view.__class__.__name__)}:::{identifier}"
     self.r.delete(key)