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
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
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"]
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"])
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
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
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__
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
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
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)
def test_that_reference_field_has_a_shadow_attribute(self): assert "post_id" in attributes(Comment)
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_)
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
def test_that_reference_field_attribute_name_is_set_properly(self): assert attributes(Author)["account_email"].attribute_name is not None
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"]
def test_that_has_many_field_does_not_appear_in_attributes(self): assert "comments" not in attributes(Post)
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)
def test_that_has_one_field_does_not_appear_in_attributes(self): assert "meta" not in attributes(Post)