Skip to content

Commit

Permalink
More work on aggregates and repository
Browse files Browse the repository at this point in the history
  • Loading branch information
MrMatAP committed Jan 1, 2024
1 parent b85badb commit 743421c
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 79 deletions.
2 changes: 1 addition & 1 deletion src/kaso_mashin/common/ddd/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
ValueObject,
AggregateRoot, Repository)
from .models import InstanceModel, ImageModel
from .aggregates import ImageEntity
from .aggregates import InstanceEntity, ImageEntity
21 changes: 6 additions & 15 deletions src/kaso_mashin/common/ddd/aggregates.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import uuid

from .base_types import UniqueIdentifier, BinaryScale, AggregateRoot, ValueObject
from .models import ImageModel


@dataclasses.dataclass
Expand All @@ -19,18 +18,10 @@ class ImageEntity(AggregateRoot):
min_ram: SizedValue = dataclasses.field(default_factory=lambda: SizedValue(value=0, scale=BinaryScale.GB))
min_disk: SizedValue = dataclasses.field(default_factory=lambda: SizedValue(value=0, scale=BinaryScale.GB))

@staticmethod
def from_model(model: ImageModel) -> 'ImageEntity':
return ImageEntity(id=model.id,
name=model.name,
min_vcpu=model.min_vcpu,
min_ram=SizedValue(value=model.min_ram, scale=BinaryScale.GB),
min_disk=SizedValue(value=model.min_disk, scale=BinaryScale.GB))

def as_model(self) -> ImageModel:
return ImageModel(id=self.id,
name=self.name,
min_vcpu=self.min_vcpu,
min_ram=self.min_ram.value,
min_disk=self.min_disk.value)

@dataclasses.dataclass
class InstanceEntity(AggregateRoot):
name: str
id: UniqueIdentifier = dataclasses.field(default_factory=lambda: str(uuid.uuid4()))
vcpu: int = dataclasses.field(default=1)
ram: SizedValue = dataclasses.field(default_factory=lambda: SizedValue(value=2, scale=BinaryScale.GB))
54 changes: 43 additions & 11 deletions src/kaso_mashin/common/ddd/base_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,16 @@ class Base(sqlalchemy.orm.DeclarativeBase): # pylint: disable=too-few-public-me


class EntityModel:
id: sqlalchemy.orm.Mapped[str] = sqlalchemy.orm.mapped_column(sqlalchemy.String(32),
primary_key=True)
id: sqlalchemy.orm.Mapped[str] = sqlalchemy.orm.mapped_column(sqlalchemy.String(32), primary_key=True)

@staticmethod
@abc.abstractmethod
def from_aggregateroot(entity):
raise NotImplementedError()

@abc.abstractmethod
def as_entity(self):
raise NotImplementedError()

def merge(self, other: 'EntityModel'):
for key, value in self.__dict__.items():
Expand All @@ -27,16 +35,19 @@ def merge(self, other: 'EntityModel'):
T = typing.TypeVar('T', bound=EntityModel, covariant=True)
UniqueIdentifier = typing.TypeVar('UniqueIdentifier', str, sqlalchemy.UUID(as_uuid=True))


class BinaryScale(enum.StrEnum):
KB = 'Kilobytes'
MB = 'Megabytes'
GB = 'Gigabytes'
TB = 'Terabytes'


@dataclasses.dataclass
class Entity(abc.ABC):
pass


class ValueObject(abc.ABC):
pass

Expand All @@ -54,10 +65,10 @@ def __init__(self, model: typing.Type[T], session: sqlalchemy.orm.Session) -> No

def get_by_id(self, uid: UniqueIdentifier) -> T | None:
"""
Get an entity by its unique identifier. Entities are cached in an identity map to minimise (potentially costly)
Return a model instance by its unique identifier. Models are cached in an identity map to minimise (potentially costly)
lookups into the datastore.
Args:
uid: The unique identifier of the entity
uid: The unique identifier of the model instance
Returns:
Expand All @@ -69,20 +80,36 @@ def get_by_id(self, uid: UniqueIdentifier) -> T | None:

def list(self) -> typing.Iterable[T]:
"""
List all known entities
List all currently known model instances
Returns:
An iterable containing all known entities
An iterable containing all known model instances
"""
self._identity_map.update({e.id:e for e in self._session.query(self._model).all()})
self._identity_map.update({e.id: e for e in self._session.query(self._model).all()})
return self._identity_map.values()

def create(self, entity: T) -> T:
self._session.add(entity)
def create(self, instance: T) -> T:
"""
Create a new model instance
Args:
instance: The model instance to create
Returns:
The persisted model instance
"""
self._session.add(instance)
self._session.commit()
self._identity_map[entity.id] = entity
return entity
self._identity_map[instance.id] = instance
return instance

def modify(self, update: T) -> T:
"""
Update an existing model instance
Args:
update: The updated model instance
Returns:
The updated model instance
"""
current: T = self._session.get(self._model, update.id)
current.merge(update)
self._session.add(current)
Expand All @@ -91,6 +118,11 @@ def modify(self, update: T) -> T:
return current

def remove(self, uid: UniqueIdentifier) -> None:
"""
Remove an existing model instance
Args:
uid: The unique identifier of the model instance to be removed
"""
current = self._session.get(self._model, str(uid))
self._session.delete(current)
self._session.commit()
Expand Down
34 changes: 34 additions & 0 deletions src/kaso_mashin/common/ddd/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from sqlalchemy.orm import Mapped, mapped_column

from .base_types import Base, EntityModel, BinaryScale
from .aggregates import InstanceEntity, ImageEntity, SizedValue


class InstanceModel(EntityModel, Base):
Expand All @@ -10,6 +11,21 @@ class InstanceModel(EntityModel, Base):
name: Mapped[str] = mapped_column(String(64), unique=True)
vcpu: Mapped[int] = mapped_column(Integer)
ram: Mapped[int] = mapped_column(Integer)
ram_scale: Mapped[str] = mapped_column(Enum(BinaryScale))

@staticmethod
def from_aggregateroot(entity: InstanceEntity) -> 'InstanceModel':
return InstanceModel(id=entity.id,
name=entity.name,
vcpu=entity.vcpu,
ram=entity.ram.value,
ram_scale=entity.ram.scale)

def as_entity(self):
return InstanceModel(id=self.id,
name=self.name,
vcpu=self.vcpu,
ram=SizedValue(value=self.ram, scale=self.ram_scale))


class ImageModel(EntityModel, Base):
Expand All @@ -21,3 +37,21 @@ class ImageModel(EntityModel, Base):
min_ram_scale: Mapped[str] = mapped_column(Enum(BinaryScale), default=BinaryScale.GB)
min_disk: Mapped[int] = mapped_column(Integer, default=0)
min_disk_scale: Mapped[str] = mapped_column(Enum(BinaryScale), default=BinaryScale.GB)

@staticmethod
def from_aggregateroot(entity: ImageEntity) -> 'ImageModel':
return ImageModel(id=entity.id,
name=entity.name,
min_vcpu=entity.min_vcpu,
min_ram=entity.min_ram.value,
min_ram_scale=entity.min_ram.scale,
min_disk=entity.min_disk.value,
min_disk_scale=entity.min_disk.scale)

def as_entity(self) -> ImageEntity:
return ImageEntity(id=self.id,
name=self.name,
min_vcpu=self.min_vcpu,
min_ram=SizedValue(value=self.min_ram, scale=self.min_ram_scale),
min_disk=SizedValue(value=self.min_disk, scale=self.min_disk_scale))

73 changes: 21 additions & 52 deletions tests/test_ddd.py
Original file line number Diff line number Diff line change
@@ -1,59 +1,28 @@
import uuid
import sqlalchemy.orm

from kaso_mashin.common.ddd import Repository, InstanceModel, ImageModel, ImageEntity
from kaso_mashin.common.ddd import Repository, ImageModel, ImageEntity, InstanceModel, InstanceEntity


def test_model(ddd_session: sqlalchemy.orm.Session):
assert ddd_session is not None
for i in range(0, 16):
instance_identifier = str(uuid.uuid4())
ddd_session.add(InstanceModel(id=instance_identifier,
name=f"instance-{instance_identifier}",
vcpu=i,
ram=i))
image_identifier = str(uuid.uuid4())
ddd_session.add(ImageModel(id=image_identifier,
name=f"image-{image_identifier}"))
ddd_session.commit()


def test_repositories(ddd_session: sqlalchemy.orm.Session):
instance_repository = Repository(model=InstanceModel, session=ddd_session)
instances = instance_repository.list()
assert instances is not None


def test_instance_model_roundtrip(ddd_session):
test_id = str(uuid.uuid4())
instance = InstanceModel(id=test_id, name=f'instance-{test_id}', vcpu=16, ram=32)
repository = Repository(model=InstanceModel, session=ddd_session)
assert instance == repository.create(instance)
assert instance == repository.get_by_id(test_id)
instance.name = f'instance-{test_id}-updated'
instance.vcpu = 8
instance.ram = 16
assert instance == repository.modify(instance)
repository.remove(test_id)
assert repository.get_by_id(test_id) is None


def test_image_model_roundtrip(ddd_session):
test_id = str(uuid.uuid4())
image = ImageModel(id=test_id, name=f'image-{test_id}')
repository = Repository(model=ImageModel, session=ddd_session)
assert image == repository.create(image)
assert image == repository.get_by_id(test_id)
image.name = f'image-{test_id}-updated'
assert image == repository.modify(image)
repository.remove(test_id)
assert repository.get_by_id(test_id) is None
def test_instance_entity(ddd_session):
repo = Repository(model=InstanceModel, session=ddd_session)
instance = InstanceEntity(name='Test Instance')
try:
repo.create(InstanceModel.from_aggregateroot(instance))
model = repo.get_by_id(instance.id)
loaded = model.as_entity()
assert loaded == instance
finally:
repo.remove(instance.id)
assert repo.get_by_id(instance.id) is None


def test_image_entity(ddd_session):
repository = Repository(model=ImageModel, session=ddd_session)
repo = Repository(model=ImageModel, session=ddd_session)
image = ImageEntity(name='Test Entity')
repository.create(image.as_model())
model = repository.get_by_id(image.id)
loaded = ImageEntity.from_model(model)
assert loaded == image
try:
repo.create(ImageModel.from_aggregateroot(image))
model = repo.get_by_id(image.id)
loaded_image = model.as_entity()
assert loaded_image == image
finally:
repo.remove(image.id)
assert repo.get_by_id(image.id) is None

0 comments on commit 743421c

Please sign in to comment.