def repository_factory(element_cls, **opts): element_cls = derive_element_class(element_cls, BaseRepository, **opts) if not element_cls.meta_.aggregate_cls: raise IncorrectUsageError({ "_entity": [ f"Repository `{element_cls.__name__}` should be associated with an Aggregate" ] }) # FIXME Uncomment # if not issubclass(element_cls.meta_.aggregate_cls, BaseAggregate): # raise IncorrectUsageError( # {"_entity": [f"Repository `{element_cls.__name__}` can only be associated with an Aggregate"]} # ) # Ensure the value of `database` is among known databases if element_cls.meta_.database != "ALL" and element_cls.meta_.database not in [ database.value for database in Database ]: raise IncorrectUsageError({ "_entity": [ f"Repository `{element_cls.__name__}` should be associated with a valid Database" ] }) return element_cls
def to_command_message(cls, command: BaseCommand) -> Message: # FIXME Should one of `aggregate_cls` or `stream_name` be mandatory? if not (command.meta_.aggregate_cls or command.meta_.stream_name): raise IncorrectUsageError({ "_entity": [ f"Command `{command.__class__.__name__}` needs to be associated with an aggregate or a stream" ] }) # Use the value of an identifier field if specified, or generate a new uuid if has_id_field(command): identifier = getattr(command, id_field(command).field_name) else: identifier = str(uuid4()) # Use explicit stream name if provided, or fallback on Aggregate's stream name stream_name = (command.meta_.stream_name or command.meta_.aggregate_cls.meta_.stream_name) return cls( stream_name=f"{stream_name}:command-{identifier}", type=fully_qualified_name(command.__class__), data=command.to_dict(), metadata=MessageMetadata( kind=MessageType.COMMAND.value, owner=current_domain.domain_name, **cls.derived_metadata(MessageType.COMMAND.value), ) # schema_version=command.meta_.version, # FIXME Maintain version for command )
def to_event_message(cls, event: BaseEvent) -> Message: # FIXME Should one of `aggregate_cls` or `stream_name` be mandatory? if not (event.meta_.aggregate_cls or event.meta_.stream_name): raise IncorrectUsageError({ "_entity": [ f"Event `{event.__class__.__name__}` needs to be associated with an aggregate or a stream" ] }) if has_id_field(event): identifier = getattr(event, id_field(event).field_name) else: identifier = str(uuid4()) # Use explicit stream name if provided, or fallback on Aggregate's stream name stream_name = (event.meta_.stream_name or event.meta_.aggregate_cls.meta_.stream_name) return cls( stream_name=f"{stream_name}-{identifier}", type=fully_qualified_name(event.__class__), data=event.to_dict(), metadata=MessageMetadata( kind=MessageType.EVENT.value, owner=current_domain.domain_name, **cls.derived_metadata(MessageType.EVENT.value), ) # schema_version=command.meta_.version, # FIXME Maintain version for event )
def __set_id_field(new_class): """Lookup the id field for this entity and assign""" # FIXME What does it mean when there are no declared fields? # Does it translate to an abstract entity? try: id_field = next( field for _, field in declared_fields(new_class).items() if isinstance(field, (Field, Reference)) and field.identifier) setattr(new_class, _ID_FIELD_NAME, id_field.field_name) # If the aggregate/entity has been marked abstract, # and contains an identifier field, raise exception if new_class.meta_.abstract and id_field: raise IncorrectUsageError({ "_entity": [ f"Abstract Aggregate `{new_class.__name__}` marked as abstract cannot have" " identity fields" ] }) except StopIteration: # If no id field is declared then create one # If the aggregate/entity is marked abstract, # avoid creating an identifier field. if not new_class.meta_.abstract: new_class.__create_id_field()
def event_sourced_aggregate_factory(element_cls, **opts): element_cls = derive_element_class(element_cls, BaseEventSourcedAggregate, **opts) # Iterate through methods marked as `@apply` and construct a projections map methods = inspect.getmembers(element_cls, predicate=inspect.isroutine) for method_name, method in methods: if not (method_name.startswith("__") and method_name.endswith("__")) and hasattr( method, "_event_cls"): element_cls._projections[fully_qualified_name( method._event_cls)].add(method) element_cls._events_cls_map[fully_qualified_name( method._event_cls)] = method._event_cls # Associate Event with the aggregate class if inspect.isclass(method._event_cls) and issubclass( method._event_cls, BaseEvent): # An Event can only be associated with one aggregate class, but multiple event handlers # can consume it. if (method._event_cls.meta_.aggregate_cls and method._event_cls.meta_.aggregate_cls != element_cls): raise IncorrectUsageError({ "_entity": [ f"{method._event_cls.__name__} Event cannot be associated with" f" {element_cls.__name__} because it is already associated with" f" {method._event_cls.meta_.aggregate_cls.__name__}" ] }) method._event_cls.meta_.aggregate_cls = element_cls return element_cls
def model_factory(element_cls, **kwargs): element_cls.element_type = DomainObjects.MODEL if hasattr(element_cls, "Meta"): element_cls.meta_ = ModelMeta(element_cls.Meta) else: element_cls.meta_ = ModelMeta() if not (hasattr(element_cls.meta_, "entity_cls") and element_cls.meta_.entity_cls): element_cls.meta_.entity_cls = kwargs.pop("entity_cls", None) if not (hasattr(element_cls.meta_, "schema") and element_cls.meta_.schema): element_cls.meta_.schema = kwargs.pop("schema", None) if not (hasattr(element_cls.meta_, "database") and element_cls.meta_.database): element_cls.meta_.database = kwargs.pop("database", None) if not element_cls.meta_.entity_cls: raise IncorrectUsageError({ "_entity": [ f"Model `{element_cls.__name__}` should be associated with an Entity or Aggregate" ] }) return element_cls
def __validate_for_basic_field_types(subclass): for field_name, field_obj in fields(subclass).items(): if isinstance(field_obj, (Reference, Association, ValueObject)): raise IncorrectUsageError({ "_entity": [ f"Views can only contain basic field types. " f"Remove {field_name} ({field_obj.__class__.__name__}) from class {subclass.__name__}" ] })
def __init__(self, event_cls: "BaseEvent") -> None: # Will throw error if the `apply` method is defined without event class # E.g. # @apply # def mark_published(self, event: Published): # ... if not inspect.isclass(event_cls): raise IncorrectUsageError( {"_entity": ["Apply method is missing Event class argument"]}) self._event_cls = event_cls
def event_sourced_repository_factory(element_cls, **opts): element_cls = derive_element_class(element_cls, BaseEventSourcedRepository, **opts) if not element_cls.meta_.aggregate_cls: raise IncorrectUsageError({ "_entity": [ f"Repository `{element_cls.__name__}` should be associated with an Aggregate" ] }) if not issubclass(element_cls.meta_.aggregate_cls, BaseEventSourcedAggregate): raise IncorrectUsageError({ "_entity": [ f"Repository `{element_cls.__name__}` can only be associated with an Aggregate" ] }) return element_cls
def subscriber_factory(element_cls, **kwargs): element_cls = derive_element_class(element_cls, BaseSubscriber, **kwargs) if not element_cls.meta_.event: raise IncorrectUsageError( { "_entity": [ f"Subscriber `{element_cls.__name__}` needs to be associated with an Event" ] } ) if not element_cls.meta_.broker: raise IncorrectUsageError( { "_entity": [ f"Subscriber `{element_cls.__name__}` needs to be associated with a Broker" ] } ) return element_cls
def factory_for(self, domain_object_type): from protean.core.aggregate import aggregate_factory from protean.core.application_service import application_service_factory from protean.core.command import command_factory from protean.core.command_handler import command_handler_factory from protean.core.domain_service import domain_service_factory from protean.core.email import email_factory from protean.core.entity import entity_factory from protean.core.event import domain_event_factory from protean.core.event_handler import event_handler_factory from protean.core.event_sourced_aggregate import event_sourced_aggregate_factory from protean.core.model import model_factory from protean.core.repository import repository_factory from protean.core.serializer import serializer_factory from protean.core.subscriber import subscriber_factory from protean.core.value_object import value_object_factory from protean.core.view import view_factory factories = { DomainObjects.AGGREGATE.value: aggregate_factory, DomainObjects.APPLICATION_SERVICE.value: application_service_factory, DomainObjects.COMMAND.value: command_factory, DomainObjects.COMMAND_HANDLER.value: command_handler_factory, DomainObjects.EVENT.value: domain_event_factory, DomainObjects.EVENT_HANDLER.value: event_handler_factory, DomainObjects.EVENT_SOURCED_AGGREGATE.value: event_sourced_aggregate_factory, DomainObjects.DOMAIN_SERVICE.value: domain_service_factory, DomainObjects.EMAIL.value: email_factory, DomainObjects.ENTITY.value: entity_factory, DomainObjects.MODEL.value: model_factory, DomainObjects.REPOSITORY.value: repository_factory, DomainObjects.SUBSCRIBER.value: subscriber_factory, DomainObjects.SERIALIZER.value: serializer_factory, DomainObjects.VALUE_OBJECT.value: value_object_factory, DomainObjects.VIEW.value: view_factory, } if domain_object_type.value not in factories: raise IncorrectUsageError({ "_entity": [f"Unknown Element Type `{domain_object_type.value}`"] }) return factories[domain_object_type.value]
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 entity_factory(element_cls, **kwargs): element_cls = derive_element_class(element_cls, BaseEntity, **kwargs) if element_cls.meta_.abstract is True: raise NotSupportedError({ "_entity": [ f"`{element_cls.__name__}` class has been marked abstract" f" and cannot be instantiated" ] }) if not element_cls.meta_.aggregate_cls: raise IncorrectUsageError({ "_entity": [ f"Entity `{element_cls.__name__}` needs to be associated with an Aggregate" ] }) return element_cls
def command_handler_factory(element_cls, **kwargs): element_cls = derive_element_class(element_cls, BaseCommandHandler, **kwargs) if not element_cls.meta_.aggregate_cls: raise IncorrectUsageError({ "_entity": [ f"Command Handler `{element_cls.__name__}` needs to be associated with an Aggregate" ] }) # Iterate through methods marked as `@handle` and construct a handler map if not element_cls._handlers: # Protect against re-registration methods = inspect.getmembers(element_cls, predicate=inspect.isroutine) for method_name, method in methods: if not (method_name.startswith("__") and method_name.endswith("__")) and hasattr( method, "_target_cls"): # Do not allow multiple handlers per command if (fully_qualified_name( method._target_cls) in element_cls._handlers and len(element_cls._handlers[fully_qualified_name( method._target_cls)]) != 0): raise NotSupportedError( f"Command {method._target_cls.__name__} cannot be handled by multiple handlers" ) # `_handlers` maps the command to its handler method element_cls._handlers[fully_qualified_name( method._target_cls)].add(method) # Associate Command with the handler's stream if inspect.isclass(method._target_cls) and issubclass( method._target_cls, BaseCommand): # Order of preference: # 1. Stream name defined in command # 2. Stream name derived from aggregate associated with command handler method._target_cls.meta_.stream_name = ( method._target_cls.meta_.stream_name or element_cls.meta_.aggregate_cls.meta_.stream_name) return element_cls
def event_handler_factory(element_cls, **opts): element_cls = derive_element_class(element_cls, BaseEventHandler, **opts) if not (element_cls.meta_.aggregate_cls or element_cls.meta_.stream_name): raise IncorrectUsageError({ "_entity": [ f"Event Handler `{element_cls.__name__}` needs to be associated with an aggregate or a stream" ] }) # Iterate through methods marked as `@handle` and construct a handler map # # Also, if `_target_cls` is an event, associate it with the event handler's # aggregate or stream methods = inspect.getmembers(element_cls, predicate=inspect.isroutine) for method_name, method in methods: if not (method_name.startswith("__") and method_name.endswith("__")) and hasattr( method, "_target_cls"): # `_handlers` is a dictionary mapping the event to the handler method. if method._target_cls == "$any": # This replaces any existing `$any` handler, by design. An Event Handler # can have only one `$any` handler method. element_cls._handlers["$any"] = {method} else: element_cls._handlers[fully_qualified_name( method._target_cls)].add(method) # Associate Event with the handler's stream if inspect.isclass(method._target_cls) and issubclass( method._target_cls, BaseEvent): # Order of preference: # 1. Stream name defined in event # 2. Stream name defined for the event handler # 3. Stream name derived from aggregate stream_name = element_cls.meta_.stream_name or ( element_cls.meta_.aggregate_cls.meta_.stream_name if element_cls.meta_.aggregate_cls else None) method._target_cls.meta_.stream_name = ( method._target_cls.meta_.stream_name or stream_name) return element_cls