예제 #1
0
        def test_that_class_referenced_is_resolved_as_soon_as_element_is_registered(
                self, test_domain):
            class Post(BaseAggregate):
                content = Text(required=True)
                comments = HasMany("Comment")

            test_domain.register(Post)

            # Still a string reference
            assert isinstance(declared_fields(Post)["comments"].to_cls, str)

            class Comment(BaseEntity):
                content = Text()
                added_on = DateTime()

                post = Reference("Post")

                class Meta:
                    aggregate_cls = Post

            # Still a string reference
            assert isinstance(declared_fields(Comment)["post"].to_cls, str)

            assert (len(test_domain._pending_class_resolutions) == 1
                    )  # Comment has not been registered yet

            # Registering `Comment` resolves references in both `Comment` and `Post` classes
            test_domain.register(Comment)

            assert declared_fields(Post)["comments"].to_cls == Comment
            assert declared_fields(Comment)["post"].to_cls == Post

            assert len(test_domain._pending_class_resolutions) == 0
예제 #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_that_meta_is_loaded_with_attributes(self):
        assert UserSchema.meta_.aggregate_cls is not None
        assert UserSchema.meta_.aggregate_cls == User

        assert declared_fields(UserSchema) is not None
        assert all(key in declared_fields(UserSchema)
                   for key in ["name", "age"])
예제 #4
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
예제 #5
0
        def test_that_class_reference_is_resolved_on_domain_activation(self):
            domain = Domain("Inline Domain")

            class Post(BaseAggregate):
                content = Text(required=True)
                comments = HasMany("Comment")

            domain.register(Post)

            # Still a string
            assert isinstance(declared_fields(Post)["comments"].to_cls, str)

            class Comment(BaseEntity):
                content = Text()
                added_on = DateTime()

                post = Reference("Post")

                class Meta:
                    aggregate_cls = Post

            domain.register(Comment)

            with domain.domain_context():
                # Resolved references
                assert declared_fields(Post)["comments"].to_cls == Comment
                assert declared_fields(Comment)["post"].to_cls == Post

                assert len(domain._pending_class_resolutions) == 0
예제 #6
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"]
예제 #7
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"]
예제 #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
예제 #9
0
    def test_field_order_when_overridden_after_inheritance(self):
        class ChildCustomContainer(CustomContainer):
            baz = String()
            foo = Integer()

        assert list(declared_fields(ChildCustomContainer).keys()) == [
            "foo",
            "bar",
            "baz",
        ]
        assert isinstance(declared_fields(ChildCustomContainer)["foo"], Integer)
예제 #10
0
        def test_that_class_reference_is_tracked_at_the_domain_level(self):
            domain = Domain()

            class Post(BaseAggregate):
                content = Text(required=True)
                comments = HasMany("Comment")

            domain.register(Post)

            # Still a string
            assert isinstance(declared_fields(Post)["comments"].to_cls, str)

            class Comment(BaseEntity):
                content = Text()
                added_on = DateTime()

                post = Reference("Post")

                class Meta:
                    aggregate_cls = Post

            domain.register(Comment)

            assert len(domain._pending_class_resolutions) == 2
            assert all(field_name in domain._pending_class_resolutions
                       for field_name in ["Comment", "Post"])
예제 #11
0
    def __track_id_field(subclass):
        """Check if an identifier field has been associated with the command.

        When an identifier is provided, its value is used to construct
        unique stream name."""
        if declared_fields(subclass):
            try:
                id_field = next(
                    field for _, field in declared_fields(subclass).items()
                    if isinstance(field, (Field)) and field.identifier)

                setattr(subclass, _ID_FIELD_NAME, id_field.field_name)

            except StopIteration:
                # No Identity fields declared
                pass
    def test_that_fields_in_base_classes_are_inherited(self):
        declared_fields_keys = list(
            OrderedDict(sorted(declared_fields(ConcreteRole).items())).keys())
        assert declared_fields_keys == ["bar", "foo", "id"]

        role = ConcreteRole(id=3, foo="foo", bar="bar")
        assert role is not None
        assert role.foo == "foo"
    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"])
예제 #15
0
    def test_field_order_after_inheritance(self):
        class ChildCustomContainer(CustomContainer):
            baz = String()

        assert list(declared_fields(ChildCustomContainer).keys()) == [
            "foo",
            "bar",
            "baz",
        ]
예제 #16
0
    def test_that_abstract_aggregates_do_not_have_id_field(self, test_domain):
        @test_domain.aggregate
        class TimeStamped:
            created_at = DateTime(default=datetime.utcnow)
            updated_at = DateTime(default=datetime.utcnow)

            class Meta:
                abstract = True

        assert "id" not in declared_fields(TimeStamped)
예제 #17
0
        def test_that_has_many_field_references_are_resolved(
                self, test_domain):
            class Post(BaseAggregate):
                content = Text(required=True)
                comments = HasMany("Comment")

            class Comment(BaseEntity):
                content = Text()
                added_on = DateTime()

                post = Reference("Post")

                class Meta:
                    aggregate_cls = Post

            test_domain.register(Post)
            test_domain.register(Comment)

            assert declared_fields(Post)["comments"].to_cls == Comment
            assert declared_fields(Comment)["post"].to_cls == Post

            assert len(test_domain._pending_class_resolutions) == 0
예제 #18
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"]
예제 #19
0
        def test_that_unknown_class_reference_is_tracked_at_the_domain_level(
                self, test_domain):
            class Post(BaseAggregate):
                content = Text(required=True)
                comments = HasMany("Comment")

            test_domain.register(Post)

            assert "Comment" in test_domain._pending_class_resolutions
            # The content in _pending_class_resolutions is dict -> tuple array
            # key: field name
            # value: tuple of (Field Object, Owning Domain Element)
            assert (test_domain._pending_class_resolutions["Comment"][0][0] ==
                    declared_fields(Post)["comments"])
예제 #20
0
파일: dao.py 프로젝트: proteanhq/protean
    def create(self, *args, **kwargs) -> "BaseEntity":
        """Create a new record in the data store.

        Performs validations for unique attributes before creating the entity

        Returns the created entity object.

        Throws `ValidationError` for validation failures on attribute values or uniqueness constraints.

        :param args: Dictionary object containing the object's data.
        :param kwargs: named attribute names and values
        """
        logger.debug(
            f"Creating new `{self.entity_cls.__name__}` object using data {kwargs}"
        )

        try:
            # Build the entity from input arguments
            # Raises validation errors, if any, at this point
            entity_obj = self.entity_cls(*args, **kwargs)

            # Perform unique checks. Raises validation errors if unique constraints are violated.
            self._validate_unique(entity_obj)

            # Build the model object and persist into data store
            model_obj = self._create(self.model_cls.from_entity(entity_obj))

            # Reverse update auto fields into entity
            for field_name, field_obj in declared_fields(entity_obj).items():
                if isinstance(field_obj,
                              Auto) and not getattr(entity_obj, field_name):
                    if isinstance(model_obj, dict):
                        field_val = model_obj[field_name]
                    else:
                        field_val = getattr(model_obj, field_name)

                    setattr(entity_obj, field_name, field_val)

            # Set Entity status to saved to let everybody know it has been persisted
            entity_obj.state_.mark_saved()

            # Track aggregate at the UoW level, to be able to perform actions on UoW commit,
            #   like persisting events raised by the aggregate.
            if current_uow and entity_obj.element_type == DomainObjects.AGGREGATE:
                current_uow._seen.add(entity_obj)

            return entity_obj
        except ValidationError as exc:
            logger.error(f"Failed creating entity because of {exc}")
            raise
예제 #21
0
        def test_that_has_one_field_references_are_resolved(self, test_domain):
            class Account(BaseAggregate):
                email = String(required=True,
                               max_length=255,
                               unique=True,
                               identifier=True)
                author = HasOne("Author")

            class Author(BaseEntity):
                first_name = String(required=True, max_length=25)
                last_name = String(max_length=25)
                account = Reference("Account")

                class Meta:
                    aggregate_cls = Account

            test_domain.register(Account)
            test_domain.register(Author)

            assert declared_fields(Account)["author"].to_cls == Author
            assert declared_fields(Author)["account"].to_cls == Account

            assert len(test_domain._pending_class_resolutions) == 0
예제 #22
0
 def _set_embedded_values(self, instance, value):
     if value is None:
         for field_name in self.embedded_fields:
             attribute_name = self.embedded_fields[
                 field_name].attribute_name
             instance.__dict__.pop(attribute_name, None)
             self.embedded_fields[field_name].value = None
     else:
         for field_name in declared_fields(value):
             self.embedded_fields[field_name].value = getattr(
                 value, field_name)
             attribute_name = self.embedded_fields[
                 field_name].attribute_name
             instance.__dict__[attribute_name] = getattr(value, field_name)
예제 #23
0
파일: entity.py 프로젝트: proteanhq/protean
    def to_dict(self):
        """Return entity data as a dictionary"""
        # FIXME Memoize this function
        field_values = {}

        for field_name, field_obj in declared_fields(self).items():
            if (not isinstance(field_obj, (ValueObject, Reference))
                    and getattr(self, field_name, None) is not None):
                field_values[field_name] = field_obj.as_dict(
                    getattr(self, field_name, None))
            elif isinstance(field_obj, ValueObject):
                value = field_obj.as_dict(getattr(self, field_name, None))
                if value:
                    field_values[field_name] = field_obj.as_dict(
                        getattr(self, field_name, None))

        return field_values
예제 #24
0
    def __validate_id_field(subclass):
        """Lookup the id field for this view and assign"""
        # FIXME What does it mean when there are no declared fields?
        #   Does it translate to an abstract view?
        if has_fields(subclass):
            try:
                id_field = next(
                    field for _, field in declared_fields(subclass).items()
                    if isinstance(field, (Field)) and field.identifier)

                setattr(subclass, _ID_FIELD_NAME, id_field.field_name)

            except StopIteration:
                raise IncorrectUsageError({
                    "_entity": [
                        f"Event Sourced Aggregate `{subclass.__name__}` needs to have at least one identifier"
                    ]
                })
예제 #25
0
    def __init__(self, value_object_cls, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._value_object_cls = value_object_cls

        self.embedded_fields = {}
        for (
                field_name,
                field_obj,
        ) in declared_fields(self._value_object_cls).items():
            self.embedded_fields[field_name] = _ShadowField(
                self,
                field_name,
                field_obj,
                # FIXME Pass all other kwargs here
                #   Because we want the shadow field to mimic the behavior of the actual field
                #   Which means that ShadowField somehow has to become an Integer, Float, String, etc.
                referenced_as=field_obj.referenced_as,
            )
    def test_that_abstract_entities_can_be_created_with_annotations(
            self, test_domain):
        from protean import BaseAggregate
        from protean.fields import String

        class CustomBaseClass(BaseAggregate):
            foo = String(max_length=25)

            class Meta:
                abstract = True

        @test_domain.aggregate
        class ConcreateSubclass(CustomBaseClass):
            bar = String(max_length=25)

        assert all(key in declared_fields(ConcreateSubclass)
                   for key in ["foo", "bar"])

        concrete = ConcreateSubclass(foo="Saturn", bar="Titan")
        assert concrete is not None
        assert concrete.foo == "Saturn"
        assert concrete.bar == "Titan"
예제 #27
0
파일: dao.py 프로젝트: proteanhq/protean
    def _validate_unique(self, entity_obj, create=True):
        """Validate the unique constraints for the entity. Raise ValidationError, if constraints were violated.

        This method internally uses each field object's fail method to construct a valid error message.

        :param entity_obj: Entity object to be validated
        :param create: boolean value to indicate that the validation is part of a create operation
        """
        # Build the filters from the unique constraints
        filters, excludes = {}, {}

        # Construct filter criteria based on unique fields defined in Entity class
        for field_name, field_obj in unique_fields(self.entity_cls).items():
            lookup_value = getattr(entity_obj, field_name, None)

            # Ignore empty lookup values
            if lookup_value in Field.empty_values:
                continue

            # Ignore identifiers on updates
            if not create and field_obj.identifier:
                excludes[field_name] = lookup_value
                continue

            filters[field_name] = lookup_value

        # Lookup the objects by filters and raise error if objects exist
        for filter_key, lookup_value in filters.items():
            if self.exists(excludes, **{filter_key: lookup_value}):
                field_obj = declared_fields(self.entity_cls)[filter_key]
                field_obj.fail(
                    "unique",
                    entity_name=self.entity_cls.__name__,
                    field_name=filter_key,
                    value=lookup_value,
                )
예제 #28
0
        def test_that_domain_throws_exception_on_unknown_class_references_during_activation(
            self, ):
            domain = Domain("Inline Domain")

            class Post(BaseAggregate):
                content = Text(required=True)
                comments = HasMany("Comment")

            domain.register(Post)

            # Still a string
            assert isinstance(declared_fields(Post)["comments"].to_cls, str)

            class Comment(BaseEntity):
                content = Text()
                added_on = DateTime()

                post = Reference("Post")
                foo = Reference("Foo")

                class Meta:
                    aggregate_cls = Post

            domain.register(Comment)

            with pytest.raises(ConfigurationError) as exc:
                with domain.domain_context():
                    pass

            assert (exc.value.args[0]["element"] ==
                    "Element Foo not registered in domain Inline Domain")

            # Remove domain context manually, as we lost it when the exception was raised
            from protean.globals import _domain_context_stack

            _domain_context_stack.pop()
예제 #29
0
파일: entity.py 프로젝트: proteanhq/protean
    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)
예제 #30
0
파일: entity.py 프로젝트: proteanhq/protean
 def __set_up_reference_fields(subclass):
     """Walk through relation fields and setup shadow attributes"""
     for _, field in declared_fields(subclass).items():
         if isinstance(field, Reference):
             shadow_field_name, shadow_field = field.get_shadow_field()
             shadow_field.__set_name__(subclass, shadow_field_name)