From 6c45277218240ff32a3226ec90968358a3127ab2 Mon Sep 17 00:00:00 2001 From: Subhash Bhushan Date: Thu, 20 Jun 2024 15:35:07 -0700 Subject: [PATCH] Remove `provider` meta option from Entities Entities are to be persisted in the same persistence store as their aggregates. This way, the aggregate cluster is kept together and transaction boundaries can be honored. This commit removes the explicit `provider` option from Entities. A new method has been introduced at the domain level, that is invoked by the `domain.init()` method, that takes care of setting `provider` for each entity. --- docs/guides/domain-definition/aggregates.md | 71 ++++++++++++++++ docs/guides/domain-definition/entities.md | 84 ++++++------------- docs_src/guides/propagate-state/001.py | 20 +++++ src/protean/core/entity.py | 2 - src/protean/core/event.py | 4 +- src/protean/domain/__init__.py | 29 +++++++ .../model/elasticsearch_model/conftest.py | 1 + .../sqlalchemy_model/postgresql/conftest.py | 1 + .../model/sqlalchemy_model/sqlite/conftest.py | 1 + .../repository/elasticsearch_repo/conftest.py | 1 + .../sqlalchemy_repo/postgresql/conftest.py | 1 + .../postgresql/test_associations.py | 2 + .../postgresql/test_persistence.py | 1 + .../test_persisting_list_of_value_objects.py | 1 + .../postgresql/test_provider.py | 1 + .../postgresql/test_transactions.py | 1 + .../sqlalchemy_repo/sqlite/conftest.py | 1 + tests/aggregate/elements.py | 11 ++- .../test_aggregate_association_dao.py | 3 + tests/aggregate/test_aggregate_events.py | 1 + .../test_aggregate_reference_field.py | 5 +- tests/aggregate/test_as_dict.py | 1 + tests/entity/elements.py | 8 -- .../fields/test_list_of_value_objects.py | 1 + tests/entity/test_entity.py | 15 ---- ...> test_entity_aggregate_cluster_option.py} | 0 tests/entity/test_entity_meta.py | 11 --- tests/entity/test_entity_provider_option.py | 56 +++++++++++++ tests/repository/test_child_persistence.py | 1 + 29 files changed, 231 insertions(+), 104 deletions(-) rename tests/entity/{test_entity_aggregate_cluster_property.py => test_entity_aggregate_cluster_option.py} (100%) create mode 100644 tests/entity/test_entity_provider_option.py diff --git a/docs/guides/domain-definition/aggregates.md b/docs/guides/domain-definition/aggregates.md index ca83daf3..33dc4a8e 100644 --- a/docs/guides/domain-definition/aggregates.md +++ b/docs/guides/domain-definition/aggregates.md @@ -181,3 +181,74 @@ the aggregate. used only when the configured database is active. Refer to the section on `Customizing Persistence schemas` for more information. + +## Associations + +Protean provides multiple options for Aggregates to weave object graphs with +enclosed Entities. + +### `HasOne` + +A HasOne field establishes a has-one relation with the entity. In the example +below, `Post` has exactly one `Statistic` record associated with it. + +```python hl_lines="18 22-26" +{! docs_src/guides/domain-definition/008.py !} +``` + +```shell +>>> post = Post(title='Foo') +>>> post.stats = Statistic(likes=10, dislikes=1) +>>> current_domain.repository_for(Post).add(post) +``` + +### `HasMany` + +```python hl_lines="19 29-33" +{! docs_src/guides/domain-definition/008.py !} +``` + +Below is an example of adding multiple comments to the domain defined above: + +```shell +❯ protean shell --domain docs_src/guides/domain-definition/008.py +... +In [1]: from protean.globals import current_domain + +In [2]: post = Post(title='Foo') + +In [3]: comment1 = Comment(content='bar') + +In [4]: comment2 = Comment(content='baz') + +In [5]: post.add_comments([comment1, comment2]) + +In [6]: current_domain.repository_for(Post).add(post) +Out[6]: + +In [7]: post.to_dict() +Out[7]: +{'title': 'Foo', + 'created_on': '2024-05-06 14:29:22.946329+00:00', + 'comments': [{'content': 'bar', 'id': 'af238f7b-5225-41fc-ae37-36cd4cface66'}, + {'content': 'baz', 'id': '5b7fa5ad-7b64-4194-ade7-fb7a4b3a8a15'}], + 'id': '19031285-6e27-4b7e-8b06-47ba6766208a'} +``` + +### `Reference` + +A `Reference` field establishes the opposite relationship with the parent at +the data level. Entities that are connected by `HasMany` and `HasOne` +relationships are connected to the owning aggregate with a `Reference` field +acting as the foreign key. + +```shell +In [8]: post = current_domain.repository_for(Post).get(post.id) + +In [9]: post.comments[0].post +Out[9]: + +In [10]: post.comments[0].post_id +Out[10]: 'e288ee30-e1d5-4fb3-94d8-d8083a6dc9db' +``` + diff --git a/docs/guides/domain-definition/entities.md b/docs/guides/domain-definition/entities.md index 6d895d30..43dd7e89 100644 --- a/docs/guides/domain-definition/entities.md +++ b/docs/guides/domain-definition/entities.md @@ -17,7 +17,7 @@ over time. additional responsibility of managing the lifecycle of one or more related entities. -# Definition +## Definition An Entity is defined with the `Domain.entity` decorator: @@ -49,72 +49,42 @@ IncorrectUsageError: {'_entity': ['Entity `Comment` needs to be associated with -## Associations - -Protean provides multiple options for Aggregates to weave object graphs with -enclosed Entities. - -### `HasOne` - -A HasOne field establishes a has-one relation with the entity. In the example -below, `Post` has exactly one `Statistic` record associated with it. - -```python hl_lines="18 22-26" -{! docs_src/guides/domain-definition/008.py !} -``` - -```shell ->>> post = Post(title='Foo') ->>> post.stats = Statistic(likes=10, dislikes=1) ->>> current_domain.repository_for(Post).add(post) -``` +## Configuration -### `HasMany` +Similar to an aggregate, an entity's behavior can be customized with by passing +additional options to its decorator, or with a `Meta` class as we saw earlier. -```python hl_lines="19 29-33" -{! docs_src/guides/domain-definition/008.py !} -``` - -Below is an example of adding multiple comments to the domain defined above: +Available options are: -```shell -❯ protean shell --domain docs_src/guides/domain-definition/008.py -... -In [1]: from protean.globals import current_domain +### `abstract` -In [2]: post = Post(title='Foo') +Marks an Entity as abstract if `True`. If abstract, the entity cannot be +instantiated and needs to be subclassed. -In [3]: comment1 = Comment(content='bar') +### `auto_add_id_field` -In [4]: comment2 = Comment(content='baz') +If `True`, Protean will not add an identifier field (acting as primary key) +by default to the entity. This option is usually combined with `abstract` to +create entities that are meant to be subclassed by other aggregates. -In [5]: post.add_comments([comment1, comment2]) +### `schema_name` -In [6]: current_domain.repository_for(Post).add(post) -Out[6]: - -In [7]: post.to_dict() -Out[7]: -{'title': 'Foo', - 'created_on': '2024-05-06 14:29:22.946329+00:00', - 'comments': [{'content': 'bar', 'id': 'af238f7b-5225-41fc-ae37-36cd4cface66'}, - {'content': 'baz', 'id': '5b7fa5ad-7b64-4194-ade7-fb7a4b3a8a15'}], - 'id': '19031285-6e27-4b7e-8b06-47ba6766208a'} -``` +The name to store and retrieve the entity from the persistence store. By +default, `schema_name` is the snake case version of the Entity's name. -### `Reference` +### `model` -A `Reference` field establishes the opposite relationship with the parent at -the data level. Entities that are connected by `HasMany` and `HasOne` -relationships can reference their owning object. +Similar to an aggregate, Protean automatically constructs a representation +of the entity that is compatible with the configured database. While the +generated model suits most use cases, you can also explicitly construct a model +and associate it with the entity, just like in an aggregate. -```shell -In [8]: post = current_domain.repository_for(Post).get(post.id) +!!!note + An Entity is always persisted in the same persistence store as the + its Aggregate. -In [9]: post.comments[0].post -Out[9]: +## Associations -In [10]: post.comments[0].post_id -Out[10]: 'e288ee30-e1d5-4fb3-94d8-d8083a6dc9db' -``` - +Entities can be further enclose other entities within them, with the `HasOne` +and `HasMany` relationships, just like in an aggregate. Refer to the Aggregate's +[Association documentation](./aggregates.md#associations) for more details. diff --git a/docs_src/guides/propagate-state/001.py b/docs_src/guides/propagate-state/001.py index a9d9ccdc..f1943ec2 100644 --- a/docs_src/guides/propagate-state/001.py +++ b/docs_src/guides/propagate-state/001.py @@ -49,3 +49,23 @@ def reduce_stock_level(self, event: OrderShipped): inventory.in_stock -= event.quantity # (2) repo.add(inventory) + + +domain.init() +with domain.domain_context(): + # Persist Order + order = Order(book_id=1, quantity=10, total_amount=100) + domain.repository_for(Order).add(order) + + # Persist Inventory + inventory = Inventory(book_id=1, in_stock=100) + domain.repository_for(Inventory).add(inventory) + + # Ship Order + order.ship_order() + domain.repository_for(Order).add(order) + + # Verify that Inventory Level has been reduced + stock = domain.repository_for(Inventory).get(inventory.id) + print(stock.to_dict()) + assert stock.in_stock == 90 diff --git a/src/protean/core/entity.py b/src/protean/core/entity.py index b963ec0b..4cbff199 100644 --- a/src/protean/core/entity.py +++ b/src/protean/core/entity.py @@ -1,6 +1,5 @@ """Entity Functionality and Classes""" -import copy import functools import inspect import logging @@ -119,7 +118,6 @@ def __init_subclass__(subclass) -> None: def _default_options(cls): return [ ("auto_add_id_field", True), - ("provider", "default"), ("model", None), ("part_of", None), ("aggregate_cluster", None), diff --git a/src/protean/core/event.py b/src/protean/core/event.py index 18712e13..39ccf358 100644 --- a/src/protean/core/event.py +++ b/src/protean/core/event.py @@ -48,8 +48,8 @@ def _default_options(cls): ) # This method is called during class import, so we cannot use part_of if it - # is still a string. We ignore it for now, and resolve `stream_name` in - # the factory after the domain has resolved references. + # is still a string. We ignore it for now, and resolve `stream_name` later + # when the domain has resolved references. # FIXME A better mechanism would be to not set stream_name here, unless explicitly # specified, and resolve it during `domain.init()` part_of = None if isinstance(part_of, str) else part_of diff --git a/src/protean/domain/__init__.py b/src/protean/domain/__init__.py index 036e5a01..54172a1f 100644 --- a/src/protean/domain/__init__.py +++ b/src/protean/domain/__init__.py @@ -205,6 +205,9 @@ def init(self, traverse=True): # noqa: C901 # Assign Aggregate Clusters self._assign_aggregate_clusters() + # Set Aggregate Cluster Options + self._set_aggregate_cluster_options() + # Run Validations self._validate_domain() @@ -725,6 +728,21 @@ def _validate_domain(self): } ) + # Check that entities have the same provider as the aggregate + for _, entity in self.registry._elements[DomainObjects.ENTITY.value].items(): + if ( + entity.cls.meta_.aggregate_cluster.meta_.provider + != entity.cls.meta_.provider + ): + raise IncorrectUsageError( + { + "element": ( + f"Entity `{entity.cls.__name__}` has a different provider " + f"than its aggregate `{entity.cls.meta_.aggregate_cluster.__name__}`" + ) + } + ) + def _assign_aggregate_clusters(self): """Assign Aggregate Clusters to all relevant elements""" from protean.core.aggregate import BaseAggregate @@ -747,6 +765,7 @@ def _assign_aggregate_clusters(self): for _, element in self.registry._elements[element_type.value].items(): part_of = element.cls.meta_.part_of if part_of: + # Traverse up the graph tree to find the root aggregate while not issubclass( part_of, (BaseAggregate, BaseEventSourcedAggregate) ): @@ -754,6 +773,16 @@ def _assign_aggregate_clusters(self): element.cls.meta_.aggregate_cluster = part_of + def _set_aggregate_cluster_options(self): + for element_type in [DomainObjects.ENTITY]: + for _, element in self.registry._elements[element_type.value].items(): + if not hasattr(element.cls.meta_, "provider"): + setattr( + element.cls.meta_, + "provider", + element.cls.meta_.aggregate_cluster.meta_.provider, + ) + ###################### # Element Decorators # ###################### diff --git a/tests/adapters/model/elasticsearch_model/conftest.py b/tests/adapters/model/elasticsearch_model/conftest.py index f51d7c04..7c0b4a91 100644 --- a/tests/adapters/model/elasticsearch_model/conftest.py +++ b/tests/adapters/model/elasticsearch_model/conftest.py @@ -33,6 +33,7 @@ def setup_db(): domain.register_model( ProviderCustomModel, entity_cls=Provider, schema_name="providers" ) + domain.init(traverse=False) domain.providers["default"]._create_database_artifacts() diff --git a/tests/adapters/model/sqlalchemy_model/postgresql/conftest.py b/tests/adapters/model/sqlalchemy_model/postgresql/conftest.py index 8f81a146..1357b2ea 100644 --- a/tests/adapters/model/sqlalchemy_model/postgresql/conftest.py +++ b/tests/adapters/model/sqlalchemy_model/postgresql/conftest.py @@ -43,6 +43,7 @@ def setup_db(): domain.register_model( ProviderCustomModel, entity_cls=Provider, schema_name="adults" ) + domain.init(traverse=False) domain.repository_for(ArrayUser)._dao domain.repository_for(GenericPostgres)._dao diff --git a/tests/adapters/model/sqlalchemy_model/sqlite/conftest.py b/tests/adapters/model/sqlalchemy_model/sqlite/conftest.py index 87c7a80f..1912412f 100644 --- a/tests/adapters/model/sqlalchemy_model/sqlite/conftest.py +++ b/tests/adapters/model/sqlalchemy_model/sqlite/conftest.py @@ -30,6 +30,7 @@ def setup_db(): domain.register_model( ProviderCustomModel, entity_cls=Provider, schema_name="adults" ) + domain.init(traverse=False) domain.repository_for(ArrayUser)._dao domain.repository_for(ComplexUser)._dao diff --git a/tests/adapters/repository/elasticsearch_repo/conftest.py b/tests/adapters/repository/elasticsearch_repo/conftest.py index 825e82df..8f06e64d 100644 --- a/tests/adapters/repository/elasticsearch_repo/conftest.py +++ b/tests/adapters/repository/elasticsearch_repo/conftest.py @@ -23,6 +23,7 @@ def setup_db(): domain.register(Alien) domain.register(User) domain.register(ComplexUser) + domain.init(traverse=False) domain.providers["default"]._create_database_artifacts() diff --git a/tests/adapters/repository/sqlalchemy_repo/postgresql/conftest.py b/tests/adapters/repository/sqlalchemy_repo/postgresql/conftest.py index 8ab47513..43fe602b 100644 --- a/tests/adapters/repository/sqlalchemy_repo/postgresql/conftest.py +++ b/tests/adapters/repository/sqlalchemy_repo/postgresql/conftest.py @@ -31,6 +31,7 @@ def setup_db(): domain.register(Audit) domain.register(Order) domain.register(Customer, part_of=Order) + domain.init(traverse=False) domain.repository_for(Alien)._dao domain.repository_for(ComplexUser)._dao diff --git a/tests/adapters/repository/sqlalchemy_repo/postgresql/test_associations.py b/tests/adapters/repository/sqlalchemy_repo/postgresql/test_associations.py index 85d455bd..50abeadb 100644 --- a/tests/adapters/repository/sqlalchemy_repo/postgresql/test_associations.py +++ b/tests/adapters/repository/sqlalchemy_repo/postgresql/test_associations.py @@ -30,6 +30,7 @@ class Audit(BaseAggregate): def test_updating_a_has_many_association(test_domain): test_domain.register(Post) test_domain.register(Comment, part_of=Post) + test_domain.init(traverse=False) post_repo = test_domain.repository_for(Post) post = Post(content="bar") @@ -51,6 +52,7 @@ def test_updating_a_has_many_association(test_domain): @pytest.mark.postgresql def test_embedded_dict_field_in_value_object(test_domain): test_domain.register(Audit) + test_domain.init(traverse=False) audit_repo = test_domain.repository_for(Audit) audit = Audit(permission=Permission(dict_object={"foo": "bar"})) diff --git a/tests/adapters/repository/sqlalchemy_repo/postgresql/test_persistence.py b/tests/adapters/repository/sqlalchemy_repo/postgresql/test_persistence.py index 1e2df56c..bbee05da 100644 --- a/tests/adapters/repository/sqlalchemy_repo/postgresql/test_persistence.py +++ b/tests/adapters/repository/sqlalchemy_repo/postgresql/test_persistence.py @@ -15,6 +15,7 @@ class Event(BaseAggregate): @pytest.mark.postgresql def test_persistence_and_retrieval(test_domain): test_domain.register(Event) + test_domain.init(traverse=False) repo = test_domain.repository_for(Event) event = Event( diff --git a/tests/adapters/repository/sqlalchemy_repo/postgresql/test_persisting_list_of_value_objects.py b/tests/adapters/repository/sqlalchemy_repo/postgresql/test_persisting_list_of_value_objects.py index 04313165..ecaa9c94 100644 --- a/tests/adapters/repository/sqlalchemy_repo/postgresql/test_persisting_list_of_value_objects.py +++ b/tests/adapters/repository/sqlalchemy_repo/postgresql/test_persisting_list_of_value_objects.py @@ -27,6 +27,7 @@ def test_persisting_and_retrieving_list_of_value_objects(test_domain): test_domain.register(Order) test_domain.register(Customer, part_of=Order) test_domain.register(Address) + test_domain.init(traverse=False) order = Order( customer=Customer( diff --git a/tests/adapters/repository/sqlalchemy_repo/postgresql/test_provider.py b/tests/adapters/repository/sqlalchemy_repo/postgresql/test_provider.py index fef81866..c399e589 100644 --- a/tests/adapters/repository/sqlalchemy_repo/postgresql/test_provider.py +++ b/tests/adapters/repository/sqlalchemy_repo/postgresql/test_provider.py @@ -18,6 +18,7 @@ class TestProviders: def register_elements(self, test_domain): test_domain.register(Person) test_domain.register(Alien) + test_domain.init(traverse=False) def test_initialization_of_providers_on_first_call(self, test_domain): """Test that ``providers`` object is available""" diff --git a/tests/adapters/repository/sqlalchemy_repo/postgresql/test_transactions.py b/tests/adapters/repository/sqlalchemy_repo/postgresql/test_transactions.py index ae2d8396..f6b9f8d7 100644 --- a/tests/adapters/repository/sqlalchemy_repo/postgresql/test_transactions.py +++ b/tests/adapters/repository/sqlalchemy_repo/postgresql/test_transactions.py @@ -14,6 +14,7 @@ class TestTransactions: def register_elements(self, test_domain): test_domain.register(Person) test_domain.register(PersonRepository, part_of=Person) + test_domain.init(traverse=False) def random_name(self): return "".join(random.choices(string.ascii_uppercase + string.digits, k=15)) diff --git a/tests/adapters/repository/sqlalchemy_repo/sqlite/conftest.py b/tests/adapters/repository/sqlalchemy_repo/sqlite/conftest.py index ddad6b28..67852bbc 100644 --- a/tests/adapters/repository/sqlalchemy_repo/sqlite/conftest.py +++ b/tests/adapters/repository/sqlalchemy_repo/sqlite/conftest.py @@ -22,6 +22,7 @@ def setup_db(): domain.register(Alien) domain.register(User) domain.register(ComplexUser) + domain.init(traverse=False) domain.repository_for(Person)._dao domain.repository_for(Alien)._dao diff --git a/tests/aggregate/elements.py b/tests/aggregate/elements.py index 348e6870..ccb5c1d7 100644 --- a/tests/aggregate/elements.py +++ b/tests/aggregate/elements.py @@ -87,7 +87,11 @@ class Account(BaseAggregate): class Author(BaseEntity): first_name = String(required=True, max_length=25) last_name = String(max_length=25) - posts = HasMany("tests.aggregate.elements.Post") + account = Reference("tests.aggregate.elements.Account") + + +class Profile(BaseEntity): + about_me = Text() account = Reference("tests.aggregate.elements.Account") @@ -98,11 +102,6 @@ class AccountWithId(BaseAggregate): author = HasOne("tests.aggregate.elements.Author") -class Profile(BaseEntity): - about_me = Text() - account = Reference("tests.aggregate.elements.Account") - - class ProfileWithAccountId(BaseEntity): about_me = Text() account = Reference("tests.aggregate.elements.AccountWithId") diff --git a/tests/aggregate/test_aggregate_association_dao.py b/tests/aggregate/test_aggregate_association_dao.py index 1ac5f115..82c8f507 100644 --- a/tests/aggregate/test_aggregate_association_dao.py +++ b/tests/aggregate/test_aggregate_association_dao.py @@ -22,7 +22,9 @@ def register_elements(self, test_domain): test_domain.register(Account) test_domain.register(Author, part_of=Account) test_domain.register(Post) + test_domain.register(Comment, part_of=Post) test_domain.register(Profile, part_of=Account) + test_domain.init(traverse=False) def test_successful_initialization_of_entity_with_has_one_association( self, test_domain @@ -46,6 +48,7 @@ class TestHasMany: def register_elements(self, test_domain): test_domain.register(Post) test_domain.register(Comment, part_of=Post) + test_domain.init(traverse=False) @pytest.fixture def persisted_post(self, test_domain): diff --git a/tests/aggregate/test_aggregate_events.py b/tests/aggregate/test_aggregate_events.py index 630f0827..3bc7cd7e 100644 --- a/tests/aggregate/test_aggregate_events.py +++ b/tests/aggregate/test_aggregate_events.py @@ -56,6 +56,7 @@ def register_elements(test_domain): test_domain.register(UserActivated, part_of=User) test_domain.register(UserRenamed, part_of=User) test_domain.register(PasswordChanged, part_of=User) + test_domain.init(traverse=False) def test_that_aggregate_has_events_list(): diff --git a/tests/aggregate/test_aggregate_reference_field.py b/tests/aggregate/test_aggregate_reference_field.py index 9baa2306..e127142e 100644 --- a/tests/aggregate/test_aggregate_reference_field.py +++ b/tests/aggregate/test_aggregate_reference_field.py @@ -3,7 +3,7 @@ from protean.exceptions import ValidationError from protean.reflection import attributes -from .elements import Account, Author, Post, Profile +from .elements import Account, Author class TestReferenceFieldAssociation: @@ -11,8 +11,7 @@ class TestReferenceFieldAssociation: def register_elements(self, test_domain): test_domain.register(Account) test_domain.register(Author, part_of=Account) - test_domain.register(Post) - test_domain.register(Profile, part_of=Account) + test_domain.init(traverse=False) def test_initialization_of_an_entity_containing_reference_field(self, test_domain): account = Account(email="john.doe@gmail.com", password="a1b2c3") diff --git a/tests/aggregate/test_as_dict.py b/tests/aggregate/test_as_dict.py index ebe6aa68..6e9be42e 100644 --- a/tests/aggregate/test_as_dict.py +++ b/tests/aggregate/test_as_dict.py @@ -68,6 +68,7 @@ class Post(BaseAggregate): test_domain.register(Post) test_domain.register(Comment, part_of=Post) + test_domain.init(traverse=False) post = Post(title="Test Post", slug="test-post", content="Do Re Mi Fa") comment1 = Comment(content="first comment") diff --git a/tests/entity/elements.py b/tests/entity/elements.py index fd96a2bc..305ad994 100644 --- a/tests/entity/elements.py +++ b/tests/entity/elements.py @@ -66,14 +66,6 @@ class SqlPerson(Person): pass -class DifferentDbPerson(Person): - pass - - -class SqlDifferentDbPerson(Person): - pass - - class OrderedPerson(BaseEntity): first_name = String(max_length=50, required=True) last_name = String(max_length=50) diff --git a/tests/entity/fields/test_list_of_value_objects.py b/tests/entity/fields/test_list_of_value_objects.py index 7255b7cc..07a3c2e4 100644 --- a/tests/entity/fields/test_list_of_value_objects.py +++ b/tests/entity/fields/test_list_of_value_objects.py @@ -27,6 +27,7 @@ def register_elements(test_domain): test_domain.register(Order) test_domain.register(Customer, part_of=Order) test_domain.register(Address) + test_domain.init(traverse=False) def test_that_list_of_value_objects_can_be_assigned_during_initialization(test_domain): diff --git a/tests/entity/test_entity.py b/tests/entity/test_entity.py index c7cf767c..4d7a8215 100644 --- a/tests/entity/test_entity.py +++ b/tests/entity/test_entity.py @@ -10,11 +10,9 @@ Adult, ConcretePerson, DbPerson, - DifferentDbPerson, Person, PersonAutoSSN, Relative, - SqlDifferentDbPerson, SqlPerson, ) @@ -27,12 +25,8 @@ def register_elements(test_domain): test_domain.register(Person, part_of=Account) test_domain.register(PersonAutoSSN, part_of=Account) test_domain.register(Relative, part_of=Account) - test_domain.register( - SqlDifferentDbPerson, part_of=Account, provider="non-default-sql" - ) test_domain.register(SqlPerson, part_of=Account, schema_name="people") test_domain.register(DbPerson, part_of=Account, schema_name="pepes") - test_domain.register(DifferentDbPerson, part_of=Account, provider="non-default") test_domain.register(Adult, part_of=Account, schema_name="adults") test_domain.init(traverse=False) @@ -83,15 +77,6 @@ def test_schema_name_can_be_overridden_in_entity_subclass(self): assert hasattr(SqlPerson.meta_, "schema_name") assert getattr(SqlPerson.meta_, "schema_name") == "people" - def test_default_and_overridden_provider_in_meta(self): - assert getattr(Person.meta_, "provider") == "default" - assert getattr(DifferentDbPerson.meta_, "provider") == "non-default" - - def test_provider_can_be_overridden_in_entity_subclass(self): - """Test that `provider` can be overridden""" - assert hasattr(SqlDifferentDbPerson.meta_, "provider") - assert getattr(SqlDifferentDbPerson.meta_, "provider") == "non-default-sql" - def test_that_schema_is_not_inherited(self): assert Person.meta_.schema_name != Adult.meta_.schema_name diff --git a/tests/entity/test_entity_aggregate_cluster_property.py b/tests/entity/test_entity_aggregate_cluster_option.py similarity index 100% rename from tests/entity/test_entity_aggregate_cluster_property.py rename to tests/entity/test_entity_aggregate_cluster_option.py diff --git a/tests/entity/test_entity_meta.py b/tests/entity/test_entity_meta.py index 23b34c54..09d67784 100644 --- a/tests/entity/test_entity_meta.py +++ b/tests/entity/test_entity_meta.py @@ -7,11 +7,9 @@ Adult, ConcretePerson, DbPerson, - DifferentDbPerson, Person, PersonAutoSSN, Relative, - SqlDifferentDbPerson, SqlPerson, ) @@ -62,15 +60,6 @@ def test_schema_name_can_be_overridden_in_entity_subclass(self): assert hasattr(SqlPerson.meta_, "schema_name") assert getattr(SqlPerson.meta_, "schema_name") == "people" - def test_default_and_overridden_provider_in_meta(self): - assert getattr(Person.meta_, "provider") == "default" - assert getattr(DifferentDbPerson.meta_, "provider") == "non-default" - - def test_provider_can_be_overridden_in_entity_subclass(self): - """Test that `provider` can be overridden""" - assert hasattr(SqlDifferentDbPerson.meta_, "provider") - assert getattr(SqlDifferentDbPerson.meta_, "provider") == "non-default-sql" - def test_that_schema_is_not_inherited(self): assert Person.meta_.schema_name != Adult.meta_.schema_name diff --git a/tests/entity/test_entity_provider_option.py b/tests/entity/test_entity_provider_option.py new file mode 100644 index 00000000..f76eb81e --- /dev/null +++ b/tests/entity/test_entity_provider_option.py @@ -0,0 +1,56 @@ +import pytest + +from protean import BaseAggregate, BaseEntity +from protean.exceptions import IncorrectUsageError +from protean.fields import HasOne, Integer, String + + +class Department(BaseAggregate): + name = String(max_length=50) + dean = HasOne("Dean") + + +class Dean(BaseEntity): + name = String(max_length=50) + age = Integer(min_value=21) + + +class TestAggregateAndEntityDefaultProvider: + @pytest.fixture(autouse=True) + def register_elements(self, test_domain): + test_domain.register(Department) + test_domain.register(Dean, part_of=Department) + + def test_default_provider_is_none(self, test_domain): + test_domain.init(traverse=False) + + assert Department.meta_.provider == "default" + assert Dean.meta_.provider == "default" + + +class TestWhenEntityHasSameProviderAsAggregate: + @pytest.fixture(autouse=True) + def register_elements(self, test_domain): + test_domain.register(Department, provider="primary") + test_domain.register(Dean, part_of=Department, provider="primary") + + def test_entity_provider_is_same_as_aggregate_provider(self, test_domain): + test_domain.init(traverse=False) + + assert Department.meta_.provider == "primary" + assert Dean.meta_.provider == "primary" + + +class TestWhenEntityDoesNotHaveSameProviderAsAggregate: + @pytest.fixture(autouse=True) + def register_elements(self, test_domain): + test_domain.register(Department, provider="primary") + test_domain.register(Dean, part_of=Department, provider="secondary") + + def test_entity_provider_is_same_as_aggregate_provider(self, test_domain): + with pytest.raises(IncorrectUsageError) as exc: + test_domain.init(traverse=False) + + assert exc.value.messages == { + "element": "Entity `Dean` has a different provider than its aggregate `Department`" + } diff --git a/tests/repository/test_child_persistence.py b/tests/repository/test_child_persistence.py index e753f247..c10a0fc8 100644 --- a/tests/repository/test_child_persistence.py +++ b/tests/repository/test_child_persistence.py @@ -85,6 +85,7 @@ def register_elements(self, test_domain): test_domain.register(Post) test_domain.register(PostMeta, part_of=Post) test_domain.register(Comment, part_of=Post) + test_domain.init(traverse=False) @pytest.fixture def persisted_post(self, test_domain):