class DbPerson(BaseEntity): first_name = String(max_length=50, required=True) last_name = String(max_length=50) age = Integer(default=21) class Meta: schema_name = "pepes"
class PersonSQLite(BaseAggregate): first_name = String(max_length=50, required=True) last_name = String(max_length=50, required=True) age = Integer(default=21) class Meta: provider = "sqlite"
class OrderedPerson(BaseEntity): first_name = String(max_length=50, required=True) last_name = String(max_length=50) age = Integer(default=21) class Meta: order_by = "first_name"
class Person(BaseAggregate): first_name = String(max_length=50, required=True) last_name = String(max_length=50, required=True) age = Integer(default=21) @classmethod def add_newcomer(cls, person_dict): """Factory method to add a new Person to the system""" newcomer = Person( first_name=person_dict["first_name"], last_name=person_dict["last_name"], age=person_dict["age"], ) # Publish Event via the domain current_domain.publish( PersonAdded( id=newcomer.id, first_name=newcomer.first_name, last_name=newcomer.last_name, age=newcomer.age, ) ) return newcomer
class PostMeta(BaseEntity): likes = Integer(default=0) post = Reference(Post) class Meta: aggregate_cls = Post
class PersonAdded(BaseEvent): id = Auto(identifier=True) first_name = String(max_length=50, required=True) last_name = String(max_length=50, required=True) age = Integer(default=21) class Meta: aggregate_cls = Person
class DbPerson(BaseView): person_id = Identifier(identifier=True) first_name = String(max_length=50, required=True) last_name = String(max_length=50) age = Integer(default=21) class Meta: schema_name = "peoples"
class OrderedPerson(BaseView): person_id = Identifier(identifier=True) first_name = String(max_length=50, required=True) last_name = String(max_length=50) age = Integer(default=21) class Meta: order_by = "first_name"
def test_choice(self): """Test choices validations for the Integer field""" class StatusChoices(enum.Enum): """Set of choices for the status""" PENDING = (0, "Pending") SUCCESS = (1, "Success") ERROR = (2, "Error") status = Integer(choices=StatusChoices) assert status is not None # Test loading of values to the status field assert status._load(0) == 0 with pytest.raises(ValidationError) as e_info: status._load(4) assert e_info.value.messages == { "unlinked": ["Value `4` is not a valid choice. " "Must be among [0, 1, 2]"] }
class BaseAggregate(EventedMixin, BaseEntity): """The Base class for Protean-Compliant Domain Aggregates. Provides helper methods to custom define aggregate attributes, and query attribute names during runtime. Basic Usage:: @domain.aggregate class Dog: id = field.Integer(identifier=True) name = field.String(required=True, max_length=50) age = field.Integer(default=5) owner = field.String(required=True, max_length=15) (or) class Dog(BaseAggregate): id = field.Integer(identifier=True) name = field.String(required=True, max_length=50) age = field.Integer(default=5) owner = field.String(required=True, max_length=15) domain.register_element(Dog) During persistence, the model associated with this entity is retrieved dynamically from the repository factory. Model is usually initialized with a live DB connection. """ element_type = DomainObjects.AGGREGATE def __new__(cls, *args, **kwargs): if cls is BaseAggregate: raise TypeError("BaseAggregate cannot be instantiated") return super().__new__(cls) # Track current version of Aggregate _version = Integer(default=-1) class Meta: abstract = True def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @classmethod def _default_options(cls): return [ ("provider", "default"), ("model", None), ("stream_name", inflection.underscore(cls.__name__)), ("schema_name", inflection.underscore(cls.__name__)), ]
class Building(BaseValueObject): name = String(max_length=50) floors = Integer() status = String(choices=BuildingStatus) def defaults(self): if not self.status: if self.floors == 4: self.status = BuildingStatus.DONE.value else: self.status = BuildingStatus.WIP.value def clean(self): errors = defaultdict(list) if self.floors >= 4 and self.status != BuildingStatus.DONE.value: errors["status"].append("should be DONE") return errors
def test_min_value(self): """Test minimum value validation for the integer field""" with pytest.raises(ValidationError): age = Integer(min_value=5) age._load(3)
class PostMeta(BaseEntity): likes = Integer(default=0) post = Reference(Post)
class NotAPerson(BaseView): identifier = Identifier(identifier=True) first_name = String(max_length=50, required=True) last_name = String(max_length=50) age = Integer(default=21)
class PersonExplicitID(BaseView): ssn = String(max_length=36, identifier=True) first_name = String(max_length=50, required=True) last_name = String(max_length=50) age = Integer(default=21)
class PersonAutoSSN(BaseView): ssn = Auto(identifier=True) first_name = String(max_length=50, required=True) last_name = String(max_length=50) age = Integer(default=21)
class Receiver(BaseAggregate): name = String() age = Integer()
class PersonAdded(BaseEvent): id = Identifier(required=True) email = String(max_length=255, required=True) first_name = String(max_length=50, required=True) last_name = String(max_length=50, required=True) age = Integer(default=21)
class NotAPerson(BaseEntity): first_name = String(max_length=50, required=True) last_name = String(max_length=50) age = Integer(default=21)
class PersonCommand(BaseCommand): first_name = String(max_length=50, required=True) last_name = String(max_length=50, required=True) age = Integer(default=21)
class Relative(BaseEntity): first_name = String(max_length=50, required=True) last_name = String(max_length=50) age = Integer(default=21) relative_of = HasOne(Person)
class Person: name = String() age = Integer()
class BaseEventSourcedAggregate(IdentityMixin, EventedMixin, OptionsMixin, BaseContainer): """Base Event Sourced Aggregate class that all EventSourced Aggregates should inherit from. The order of inheritance is important. We want BaseContainer to be initialised first followed by OptionsMixin (so that `meta_` is in place) before inheriting other mixins.""" element_type = DomainObjects.EVENT_SOURCED_AGGREGATE # Track current version of Aggregate _version = Integer(default=-1) class Meta: abstract = True @classmethod def _default_options(cls): return [ ("stream_name", inflection.underscore(cls.__name__)), ] def __init_subclass__(subclass) -> None: super().__init_subclass__() if not subclass.meta_.abstract: subclass.__validate_id_field() # Associate a `_projections` map with subclasses. # It needs to be initialized here because if it # were initialized in __init__, the same collection object # would be made available across all subclasses, # defeating its purpose. setattr(subclass, "_projections", defaultdict(set)) # Store associated events setattr(subclass, "_events_cls_map", {}) @classmethod 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" ] }) def __eq__(self, other): """Equivalence check to be based only on Identity""" # FIXME Enhanced Equality Checks # * Ensure IDs have values and both of them are not null # * Ensure that the ID is of the right type # * Ensure that Objects belong to the same `type` # * Check Reference equality # FIXME Check if `==` and `in` operator work with __eq__ if type(other) is type(self): self_id = getattr(self, id_field(self).field_name) other_id = getattr(other, id_field(other).field_name) return self_id == other_id return False def __hash__(self): """Overrides the default implementation and bases hashing on identity""" # FIXME Add Object Class Type to hash return hash(getattr(self, id_field(self).field_name)) def _apply(self, event_dict: Dict) -> None: """Apply the event onto the aggregate by calling the appropriate projection. Args: event (BaseEvent): Event object to apply """ # FIXME Handle case of missing projection for fn in self._projections[event_dict["type"]]: # Reconstruct Event object event_cls = self._events_cls_map[event_dict["type"]] event = event_cls(**event_dict["data"]) # Call event handler method fn(self, event)
class Person(BaseAggregate): first_name = String(max_length=50, required=True) last_name = String(max_length=50, required=True) age = Integer(default=21) created_at = DateTime(default=datetime.now())
class Person(BaseAggregate): first_name = String(max_length=50, required=True) last_name = String(max_length=50, required=True) age = Integer(default=21)
class PersonSchema: name = String(required=True) age = Integer(required=True) class Meta: aggregate_cls = User
class PostMeta(BaseEntity): likes = Integer(default=0) class Meta: aggregate_cls = Post
class AbstractPerson(BaseView): age = Integer(default=5) class Meta: abstract = True
class User(BaseEventSourcedAggregate): name = String() age = Integer()
class Provider(BaseAggregate): name = String() age = Integer()