Ejemplo n.º 1
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
Ejemplo n.º 2
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
Ejemplo n.º 3
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"]
Ejemplo n.º 4
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"]
    def test_declared_has_one_fields_in_an_aggregate(self, test_domain):
        # `author` is a HasOne field, so it should be:
        #   absent in attributes
        #   present as `author` in declared_fields
        assert all(key in declared_fields(AccountWithId)
                   for key in ["author", "email", "id", "password"])
        assert all(key in attributes(AccountWithId)
                   for key in ["email", "id", "password"])
        assert "author" not in attributes(AccountWithId)

        # `account` is a Reference field, so it should be present as:
        #   `account_id` in attributes
        #   `account` in declared_fields
        assert all(key in declared_fields(ProfileWithAccountId)
                   for key in ["about_me", "account"])
        assert all(key in attributes(ProfileWithAccountId)
                   for key in ["about_me", "account_id"])
    def test_declared_has_many_fields_in_an_aggregate(self, test_domain):
        # `comments` is a HasMany field, so it should be:
        #   absent in attributes
        #   present as `comments` in declared_fields
        assert all(key in declared_fields(Post)
                   for key in ["comments", "content", "id", "author"])
        assert all(key in attributes(Post)
                   for key in ["content", "id", "author_id"])
        assert "comments" not in attributes(Post)

        # `post` is a Reference field, so it should be present as:
        #   `post_id` in attributes
        #   `post` in declared_fields
        assert all(key in declared_fields(Comment)
                   for key in ["added_on", "content", "id", "post"])
        assert all(key in attributes(Comment)
                   for key in ["added_on", "content", "id", "post_id"])
Ejemplo n.º 7
0
 def test_entity_meta_has_attributes_on_construction(self):
     assert list(attributes(Person).keys()) == [
         "first_name",
         "last_name",
         "age",
         "id",
     ]
     assert list(attributes(PersonAutoSSN).keys()) == [
         "ssn",
         "first_name",
         "last_name",
         "age",
     ]
     assert list(attributes(Relative).keys()) == [
         "first_name",
         "last_name",
         "age",
         "id",
     ]  # `relative_of` is ignored
Ejemplo n.º 8
0
    def test_that_explicit_names_are_preserved_in_aggregate(self):
        assert len(declared_fields(PolymorphicOwner)) == 2
        assert "id" in declared_fields(PolymorphicOwner)
        assert "connector" in declared_fields(PolymorphicOwner)

        owner = PolymorphicOwner()
        attrs = attributes(owner)
        assert attrs is not None
        assert "connected_id" in attrs
        assert "connected_type" in attrs
Ejemplo n.º 9
0
 def from_entity(cls, entity):
     """Convert the entity to a model object"""
     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)
     return cls(**item_dict)
    def test_that_setting_shadow_attribute_to_none_resets_reference_field_too(
            self, test_domain):
        account = Account(email="*****@*****.**", password="******")
        test_domain.repository_for(Account)._dao.save(account)
        author = Author(first_name="John", last_name="Doe", account=account)

        assert author.account.email == account.email
        assert author.account_email == account.email

        assert "account_email" in attributes(author)
        author.account_email = None

        assert (any(key in author.__dict__
                    for key in ["account", "account_email"]) is False)
        assert author.account is None
        assert author.account_email is None
        assert "account_email" not in author.__dict__
Ejemplo n.º 11
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
Ejemplo n.º 12
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
Ejemplo n.º 13
0
 def to_entity(cls, model_obj: "SqlalchemyModel"):
     """Convert the model object to an entity"""
     item_dict = {}
     for field_name in attributes(cls.meta_.entity_cls):
         item_dict[field_name] = getattr(model_obj, field_name, None)
     return cls.meta_.entity_cls(item_dict)
Ejemplo n.º 14
0
 def test_that_reference_field_has_a_shadow_attribute(self):
     assert "post_id" in attributes(Comment)
Ejemplo n.º 15
0
    def __init__(cls, classname, bases, dict_):  # noqa: C901
        field_mapping = {
            Boolean: sa_types.Boolean,
            Date: sa_types.Date,
            DateTime: sa_types.DateTime,
            Dict: sa_types.PickleType,
            Float: sa_types.Float,
            Identifier: _get_identity_type(),
            Integer: sa_types.Integer,
            List: sa_types.PickleType,
            String: sa_types.String,
            Text: sa_types.Text,
            _ReferenceField: _get_identity_type(),
        }

        def field_mapping_for(field_obj: Field):
            """Return SQLAlchemy-equivalent type for Protean's field"""
            field_cls = type(field_obj)

            if field_cls is Auto:
                if field_obj.increment is True:
                    return sa_types.Integer
                else:
                    return _get_identity_type()

            return field_mapping.get(field_cls)

        # Update the class attrs with the entity attributes
        if "meta_" in dict_:
            entity_cls = dict_["meta_"].entity_cls
            for _, field_obj in attributes(entity_cls).items():
                attribute_name = field_obj.attribute_name

                # Map the field if not in attributes
                if attribute_name not in cls.__dict__:
                    # Derive field based on field enclosed within ShadowField
                    if isinstance(field_obj, _ShadowField):
                        field_obj = field_obj.field_obj

                    field_cls = type(field_obj)
                    type_args = []
                    type_kwargs = {}

                    # Get the SA type
                    sa_type_cls = field_mapping_for(field_obj)

                    # Upgrade to Postgresql specific Data Types
                    if cls.metadata.bind.dialect.name == "postgresql":

                        if field_cls == Dict and not field_obj.pickled:
                            sa_type_cls = psql.JSON

                        if field_cls == List and not field_obj.pickled:
                            sa_type_cls = psql.ARRAY

                            # Associate Content Type
                            if field_obj.content_type:
                                type_args.append(
                                    field_mapping.get(field_obj.content_type))
                            else:
                                type_args.append(sa_types.Text)

                    # Default to the text type if no mapping is found
                    if not sa_type_cls:
                        sa_type_cls = sa_types.String

                    # Build the column arguments
                    col_args = {
                        "primary_key": field_obj.identifier,
                        "nullable": not field_obj.required,
                        "unique": field_obj.unique,
                    }

                    # Update the arguments based on the field type
                    if issubclass(field_cls, String):
                        type_kwargs["length"] = field_obj.max_length

                    # Update the attributes of the class
                    dict_[attribute_name] = Column(
                        sa_type_cls(*type_args, **type_kwargs), **col_args)
        super().__init__(classname, bases, dict_)
Ejemplo n.º 16
0
 def from_entity(cls, entity) -> "MemoryModel":
     """Convert the entity to a dictionary record"""
     dict_obj = {}
     for attribute_name in attributes(entity):
         dict_obj[attribute_name] = getattr(entity, attribute_name)
     return dict_obj
Ejemplo n.º 17
0
 def test_that_reference_field_attribute_name_is_set_properly(self):
     assert attributes(Author)["account_email"].attribute_name is not None
Ejemplo n.º 18
0
    def test_default_id_field_construction(self):
        assert "id" in declared_fields(Role)
        assert "id" in attributes(Role)

        assert type(declared_fields(Role)["id"]) is Auto
        assert id_field(Role) == declared_fields(Role)["id"]
Ejemplo n.º 19
0
 def test_that_has_many_field_does_not_appear_in_attributes(self):
     assert "comments" not in attributes(Post)
Ejemplo n.º 20
0
    def __init__(self, *template, **kwargs):  # noqa: C901
        """
        Initialise the entity object.

        During initialization, set value on fields if validation passes.

        This initialization technique supports keyword arguments as well as dictionaries. The objects initialized
        in the following example have the same structure::

            user1 = User({'first_name': 'John', 'last_name': 'Doe'})

            user2 = User(first_name='John', last_name='Doe')

        You can also specify a template for initial data and override specific attributes::

            base_user = User({'age': 15})

            user = User(base_user.to_dict(), first_name='John', last_name='Doe')
        """

        if self.meta_.abstract is True:
            raise NotSupportedError(
                f"{self.__class__.__name__} class has been marked abstract"
                f" and cannot be instantiated")

        self.errors = defaultdict(list)

        # Set up the storage for instance state
        self.state_ = _EntityState()

        # Placeholder for temporary association values
        self._temp_cache = defaultdict(lambda: defaultdict(dict))

        # Collect Reference field attribute names to prevent accidental overwriting
        # of shadow fields.
        reference_attributes = {
            field_obj.attribute_name: field_obj.field_name
            for field_obj in declared_fields(self).values()
            if isinstance(field_obj, Reference)
        }

        # Load the attributes based on the template
        loaded_fields = []
        for dictionary in template:
            if not isinstance(dictionary, dict):
                raise AssertionError(
                    f'Positional argument "{dictionary}" passed must be a dict.'
                    f"This argument serves as a template for loading common "
                    f"values.", )
            for field_name, val in dictionary.items():
                if field_name not in kwargs:
                    kwargs[field_name] = val

        # Now load against the keyword arguments
        for field_name, val in kwargs.items():
            try:
                setattr(self, field_name, val)
            except ValidationError as err:
                for field_name in err.messages:
                    self.errors[field_name].extend(err.messages[field_name])
            finally:
                loaded_fields.append(field_name)

                # Also note reference field name if its attribute was loaded
                if field_name in reference_attributes:
                    loaded_fields.append(reference_attributes[field_name])

        # Load Value Objects from associated fields
        #   This block will dynamically construct value objects from field values
        #   and associated the vo with the entity
        # If the value object was already provided, it will not be overridden.
        for field_name, field_obj in declared_fields(self).items():
            if isinstance(field_obj,
                          (ValueObject)) and not getattr(self, field_name):
                attrs = [
                    (embedded_field.field_name, embedded_field.attribute_name)
                    for embedded_field in field_obj.embedded_fields.values()
                ]
                values = {name: kwargs.get(attr) for name, attr in attrs}
                try:
                    # Pass the `required` option value as defined at the parent
                    value_object = field_obj.value_object_cls(
                        **values, required=field_obj.required)

                    # Set VO value only if the value object is not None/Empty
                    if value_object:
                        setattr(self, field_name, value_object)
                        loaded_fields.append(field_name)
                except ValidationError as err:
                    for sub_field_name in err.messages:
                        self.errors["{}_{}".format(
                            field_name, sub_field_name)].extend(
                                err.messages[sub_field_name])

        # Load Identities
        for field_name, field_obj in declared_fields(self).items():
            if type(field_obj) is Auto and not field_obj.increment:
                if not getattr(self, field_obj.field_name, None):
                    setattr(self, field_obj.field_name, generate_identity())
                loaded_fields.append(field_obj.field_name)

        # Load Associations
        for field_name, field_obj in declared_fields(self).items():
            if isinstance(field_obj, Association):
                getattr(
                    self,
                    field_name)  # This refreshes the values in associations

                # Set up add and remove methods. These are pseudo methods, `add_*` and
                #   `remove_*` that point to the HasMany field's `add` and `remove`
                #   methods. They are wrapped to ensure we pass the object that holds
                #   the values and temp_cache.
                if isinstance(field_obj, HasMany):
                    setattr(self, f"add_{field_name}",
                            partial(field_obj.add, self))
                    setattr(self, f"remove_{field_name}",
                            partial(field_obj.remove, self))
                    setattr(
                        self,
                        f"_mark_changed_{field_name}",
                        partial(field_obj._mark_changed, self),
                    )

        # Now load the remaining fields with a None value, which will fail
        # for required fields
        for field_name, field_obj in fields(self).items():
            if field_name not in loaded_fields:
                if not isinstance(field_obj, Association):
                    try:
                        setattr(self, field_name, None)

                        # If field is a VO, set underlying attributes to None as well
                        if isinstance(field_obj, ValueObject):
                            for embedded_field in field_obj.embedded_fields.values(
                            ):
                                setattr(self, embedded_field.attribute_name,
                                        None)
                    except ValidationError as err:
                        for field_name in err.messages:
                            self.errors[field_name].extend(
                                err.messages[field_name])

        for field_name, field_obj in attributes(self).items():
            if field_name not in loaded_fields and not hasattr(
                    self, field_name):
                setattr(self, field_name, None)

        self.defaults()

        # `clean()` will return a `defaultdict(list)` if errors are to be raised
        custom_errors = self.clean() or {}
        for field in custom_errors:
            self.errors[field].extend(custom_errors[field])

        # Raise any errors found during load
        if self.errors:
            logger.error(f"Error during initialization: {dict(self.errors)}")
            raise ValidationError(self.errors)
Ejemplo n.º 21
0
 def test_that_has_one_field_does_not_appear_in_attributes(self):
     assert "meta" not in attributes(Post)