Skip to content

Commit

Permalink
On our path to a generic DDD repository
Browse files Browse the repository at this point in the history
  • Loading branch information
MrMatAP committed Dec 31, 2023
1 parent 96b7071 commit 7f759dd
Show file tree
Hide file tree
Showing 7 changed files with 158 additions and 0 deletions.
8 changes: 8 additions & 0 deletions src/kaso_mashin/common/ddd/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from .base_types import (
T, UniqueIdentifier,
Base,
Entity, EntityModel,
ValueObject,
AggregateRoot, Repository)
from .models import InstanceModel, ImageModel
from .repositories import InstanceRepository, ImageRepository
Empty file.
71 changes: 71 additions & 0 deletions src/kaso_mashin/common/ddd/base_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import abc
import typing

import sqlalchemy.orm


class Base(sqlalchemy.orm.DeclarativeBase): # pylint: disable=too-few-public-methods
"""
Base class for database persistence
"""
pass


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

def merge(self, other: 'EntityModel'):
for key, value in self.__dict__.items():
if key == id:
continue
setattr(self, key, getattr(other, key))


T = typing.TypeVar('T', bound=EntityModel, covariant=True)
UniqueIdentifier = typing.TypeVar('UniqueIdentifier', str, sqlalchemy.UUID(as_uuid=True))


class Entity(abc.ABC):
pass


class ValueObject(abc.ABC):
pass


class AggregateRoot(Entity):
pass


class Repository(typing.Generic[T]):

def __init__(self, model: typing.Type[T], session: sqlalchemy.orm.Session) -> None:
self._model = model
self._session = session

def get_by_id(self, uid: UniqueIdentifier) -> T:
return self._session.get(self._model, str(uid))

def list(self) -> typing.Iterable[T]:
return self._session.query(self._model).all()

def create(self, entity: T) -> T:
self._session.add(entity)
self._session.commit()
return entity

def modify(self, update: T) -> T:
current: T = self._session.get(self._model, update.id)
current.merge(update)
self._session.add(current)
self._session.commit()
return current

def remove(self, uid: UniqueIdentifier) -> None:
current = self._session.get(self._model, str(uid))
self._session.delete(current)
self._session.commit()



18 changes: 18 additions & 0 deletions src/kaso_mashin/common/ddd/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from sqlalchemy import String, Integer
from sqlalchemy.orm import Mapped, mapped_column

from .base_types import Base, EntityModel


class InstanceModel(EntityModel, Base):
__tablename__ = 'ddd_instances'

name: Mapped[str] = mapped_column(String(64), unique=True)
vcpu: Mapped[int] = mapped_column(Integer)
ram: Mapped[int] = mapped_column(Integer)


class ImageModel(EntityModel, Base):
__tablename__ = 'ddd_images'

name: Mapped[str] = mapped_column(String(64), unique=True)
10 changes: 10 additions & 0 deletions src/kaso_mashin/common/ddd/repositories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from .base_types import Repository
from .models import InstanceModel, ImageModel


class InstanceRepository(Repository[InstanceModel]):
pass


class ImageRepository(Repository[ImageModel]):
pass
14 changes: 14 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import shutil
import fastapi
import fastapi.testclient
import sqlalchemy
import sqlalchemy.orm

from kaso_mashin.common.config import Config
from kaso_mashin.server.run import create_server
Expand All @@ -14,6 +16,8 @@
from kaso_mashin.common.model import (
IdentityKind, IdentityModel)

from kaso_mashin.common.ddd import Base as DDDBase

KasoTestContext = collections.namedtuple('KasoTestContext', 'runtime client')
KasoIdentity = collections.namedtuple('KasoIdentity',
'name kind gecos homedir shell pubkey passwd')
Expand Down Expand Up @@ -105,3 +109,13 @@ def test_kaso_context_seeded(test_kaso_context_empty) -> KasoTestContext:
test_kaso_context_empty.runtime.db.session.commit()
logging.getLogger().info(f'Yielding seeded Kaso Mashin context')
yield test_kaso_context_empty


@pytest.fixture(scope='module')
def ddd_session() -> sqlalchemy.orm.Session:
db = pathlib.Path(__file__).parent.parent.joinpath('build/ddd.sqlite')
engine = sqlalchemy.create_engine('sqlite:///{}'.format(db), echo=False)
DDDBase.metadata.create_all(engine)
session = sqlalchemy.orm.Session(engine)
yield session
session.close()
37 changes: 37 additions & 0 deletions tests/test_ddd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import uuid
import sqlalchemy.orm

from kaso_mashin.common.ddd import InstanceModel, ImageModel, InstanceRepository


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 = InstanceRepository(model=InstanceModel, session=ddd_session)
instances = instance_repository.list()
assert instances is not None


def test_instance_model_roundtrip(ddd_session: sqlalchemy.orm):
test_id = str(uuid.uuid4())
instance = InstanceModel(id=test_id, name=f'instance-{test_id}', vcpu=16, ram=32)
repository = InstanceRepository(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)

0 comments on commit 7f759dd

Please sign in to comment.