added integration tests and repository pattern for image, model and visual communication DTOs
Code Quality Pipeline / Check Code (pull_request) Failing after 3m21s

This commit is contained in:
brian
2025-02-28 22:24:52 +00:00
parent ac6a305da8
commit 090a1cd8bb
45 changed files with 2630 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
from .image_repository import ImageRepository
from .model_repository import ModelRepository
from .visual_communication_repository import VisualCommunicationRepository
+7
View File
@@ -0,0 +1,7 @@
from .hexadecimal_string import HexadecimalString
from .image_data import ImageData
from .model_data import ModelData
from .visual_communication_data import (
VisualCommunicationData,
VisualCommunicationValues,
)
@@ -0,0 +1,37 @@
"""Definition of BytesIO Pydantic Annotation."""
from io import BytesIO
from typing import Any
from pydantic.json_schema import JsonSchemaValue
from pydantic_core import core_schema
class BytesIOPydanticAnnotation:
"""Pydantic annotation that defines input validation, as well as general
and json serialization."""
@classmethod
def validate_input(cls, v: Any, handler) -> BytesIO:
"""Pydantic-related function to validate input on instantiation."""
if isinstance(v, BytesIO):
return v
s = handler(v)
return BytesIO(s)
@classmethod
def __get_pydantic_core_schema__(
cls,
source_type,
_handler,
) -> core_schema.CoreSchema:
assert source_type is BytesIO
return core_schema.no_info_wrap_validator_function(
function=cls.validate_input,
schema=core_schema.str_schema(),
serialization=core_schema.to_string_ser_schema(),
)
@classmethod
def __get_pydantic_json_schema__(cls, _core_schema, handler) -> JsonSchemaValue:
return handler(core_schema.str_schema())
@@ -0,0 +1,24 @@
"""Definition of Checksum DTO."""
from __future__ import annotations
import re
class HexadecimalString(str):
"""Hexadecimal-string class."""
def __new__(cls, string):
# ensure proper input format
pattern = r'[0-9-a-fA-F]{32}'
match = re.match(pattern, string)
if match is None:
raise ValueError(f'format does not match a hexadecimal-string: {string}')
return super().__new__(cls, string)
def __repr__(self) -> str:
class_name = self.__class__.__name__
return f"{class_name}('{self}')"
def __reduce__(self):
return self.__class__, (self,)
@@ -0,0 +1,38 @@
"""Definition of HexadecimalString Pydantic Annotation."""
from typing import Any
from pydantic.json_schema import JsonSchemaValue
from pydantic_core import core_schema
from .hexadecimal_string import HexadecimalString
class HexadecimalStringPydanticAnnotation:
"""Pydantic annotation that defines input validation, as well as general
and json serialization."""
@classmethod
def validate_input(cls, v: Any, handler) -> HexadecimalString:
"""Pydantic-related function to validate input on instantiation."""
if isinstance(v, HexadecimalString):
return v
s = handler(v)
return HexadecimalString(s)
@classmethod
def __get_pydantic_core_schema__(
cls,
source_type,
_handler,
) -> core_schema.CoreSchema:
assert source_type is HexadecimalString
return core_schema.no_info_wrap_validator_function(
function=cls.validate_input,
schema=core_schema.str_schema(),
serialization=core_schema.to_string_ser_schema(),
)
@classmethod
def __get_pydantic_json_schema__(cls, _core_schema, handler) -> JsonSchemaValue:
return handler(core_schema.str_schema())
+16
View File
@@ -0,0 +1,16 @@
"""Definition of VisualData DTO."""
from typing import Annotated
from PIL import Image
from pydantic import Field
from .image_pydantic_annotation import ImagePydanticAnnotation
from .type_checking_base_model import TypeCheckingBaseModel
class ImageData(TypeCheckingBaseModel):
"""Visual data class."""
image: Annotated[Image.Image, ImagePydanticAnnotation]
name: str = Field(min_length=1)
@@ -0,0 +1,37 @@
"""Definition of BytesIO Pydantic Annotation."""
from typing import Any
from PIL import Image
from pydantic.json_schema import JsonSchemaValue
from pydantic_core import core_schema
class ImagePydanticAnnotation:
"""Pydantic annotation that defines input validation, as well as general
and json serialization."""
@classmethod
def validate_input(cls, v: Any, handler) -> Image.Image:
"""Pydantic-related function to validate input on instantiation."""
if isinstance(v, Image.Image):
return v
s = handler(v)
return Image.open(s)
@classmethod
def __get_pydantic_core_schema__(
cls,
source_type,
_handler,
) -> core_schema.CoreSchema:
assert source_type is Image.Image
return core_schema.no_info_wrap_validator_function(
function=cls.validate_input,
schema=core_schema.str_schema(),
serialization=core_schema.to_string_ser_schema(),
)
@classmethod
def __get_pydantic_json_schema__(cls, _core_schema, handler) -> JsonSchemaValue:
return handler(core_schema.str_schema())
+60
View File
@@ -0,0 +1,60 @@
"""Definition of ModelData DTO."""
from __future__ import annotations
from hashlib import md5
from io import BytesIO
from typing import Annotated
import torch
from pydantic import Field
from .bytes_io_pydantic_annotation import BytesIOPydanticAnnotation
from .hexadecimal_string import HexadecimalString
from .hexadecimal_string_pydantic_annotation import HexadecimalStringPydanticAnnotation
from .type_checking_base_model import TypeCheckingBaseModel
class ModelData(TypeCheckingBaseModel):
"""Model Data DTO."""
buffer: Annotated[BytesIO, BytesIOPydanticAnnotation]
buffer_checksum: Annotated[HexadecimalString, HexadecimalStringPydanticAnnotation]
class_name: str = Field(
min_length=1,
description='name of model class to generate data.',
)
@staticmethod
def calculate_checksum(buffer: BytesIO) -> HexadecimalString:
"""Calculate buffer checksum."""
checksum = md5(buffer.getbuffer()).hexdigest()
return HexadecimalString(checksum)
@staticmethod
def model_to_buffer(model: torch.nn.Module) -> BytesIO:
"""Save model to buffer."""
assert isinstance(model, torch.nn.Module)
buffer = BytesIO()
torch.save(model.state_dict(), buffer)
return buffer
@classmethod
def from_model(cls, model: torch.nn.Module) -> ModelData:
"""Instantiate from torch module."""
assert isinstance(model, torch.nn.Module)
# get model name
class_name = type(model).__name__
# save data to buffer
buffer = cls.model_to_buffer(model)
buffer = BytesIO()
torch.save(model.state_dict(), buffer)
# calculate checksum
buffer_checksum = cls.calculate_checksum(buffer)
# instantiate from buffer
data = cls(
buffer=buffer,
buffer_checksum=buffer_checksum,
class_name=class_name,
)
return data
@@ -0,0 +1,12 @@
"""Definition of TypeCheckingBaseModel class."""
from pydantic import BaseModel, ConfigDict
class TypeCheckingBaseModel(BaseModel):
"""BaseModel with added type checking on input types."""
model_config = ConfigDict(
validate_assignment=True, # argument type checking
frozen=True, # ensure data immutability
)
@@ -0,0 +1,2 @@
from .visual_communication_data import VisualCommunicationData
from .visual_communication_values import VisualCommunicationValues
@@ -0,0 +1,11 @@
"""Definition of AngleValues DTO."""
from .values_model import ValuesModel
class AngleValues(ValuesModel):
"""Angle values DTO."""
high: float
eye_level: float
low: float
@@ -0,0 +1,10 @@
"""Definition of ContactValues DTO."""
from .values_model import ValuesModel
class ContactValues(ValuesModel):
"""Contact values DTO."""
offer: float
demand: float
@@ -0,0 +1,11 @@
"""Definition of DistanceValues DTO."""
from .values_model import ValuesModel
class DistanceValues(ValuesModel):
"""Distance values DTO."""
long: float
medium: float
close: float
@@ -0,0 +1,12 @@
"""Definition of FramingValues DTO."""
from .values_model import ValuesModel
class FramingValues(ValuesModel):
"""Framing values DTO."""
frame_lines: float
empty_space: float
colour_contrast: float
form_contrast: float
@@ -0,0 +1,11 @@
"""Definition of InformationValueValues DTO."""
from .values_model import ValuesModel
class InformationValueValues(ValuesModel):
"""Information value values DTO."""
given_new: float
ideal_real: float
central_marginal: float
@@ -0,0 +1,11 @@
"""Definition of ModalityColorValues DTO."""
from .values_model import ValuesModel
class ModalityColorValues(ValuesModel):
"""Modality color values DTO."""
high: float
medium: float
low: float
@@ -0,0 +1,11 @@
"""Definition of ModalityDepthValues DTO."""
from .values_model import ValuesModel
class ModalityDepthValues(ValuesModel):
"""Modality depth values DTO."""
high: float
medium: float
low: float
@@ -0,0 +1,11 @@
"""Definition of ModalityLightingValues DTO."""
from .values_model import ValuesModel
class ModalityLightingValues(ValuesModel):
"""Modality lighting values DTO."""
high: float
medium: float
low: float
@@ -0,0 +1,10 @@
"""Definition of PointOfViewValues DTO."""
from .values_model import ValuesModel
class PointOfViewValues(ValuesModel):
"""Point-of-view values DTO."""
frontal: float
oblique: float
@@ -0,0 +1,13 @@
"""Definition of SalienceValues DTO."""
from .values_model import ValuesModel
class SalienceValues(ValuesModel):
"""Salience values DTO."""
size: float
colour: float
tone: float
form: float
positioning: float
@@ -0,0 +1,60 @@
"""Definition of ValuesModel base class."""
from __future__ import annotations
import random
from pydantic import BaseModel, ConfigDict
from torch import Tensor
class ValuesModel(BaseModel):
"""ValuesModel base class."""
model_config = ConfigDict(
validate_assignment=True, # argument type checking
frozen=True, # ensure data immutability
)
@classmethod
def list_fields(cls) -> list[str]:
"""List options that are stored as attributes."""
return list(cls.model_fields.keys())
@classmethod
def from_random(cls):
"""Instantiate with random numbers."""
kwargs = {field: random.random() for field in cls.list_fields()}
return cls(**kwargs)
@classmethod
def from_choice(cls, option: str) -> ValuesModel:
"""Instantiate from choice."""
assert isinstance(option, str)
assert len(option) > 0
allowed_options_list = cls.list_fields()
if option not in allowed_options_list:
raise ValueError(f'option {option} must be in {allowed_options_list}')
# generate field values
kwargs = {field: 0 for field in allowed_options_list}
# set chosen value to max probability
kwargs[option] = 1
return cls(**kwargs)
@classmethod
def from_tensor(cls, tensor: Tensor):
"""Instantiate from list of values."""
assert tensor.size(dim=0) == 1, f'tensor batch larger than 1: {tensor}'
data_list = [float(t.item()) for t in tensor[0]]
kwargs = dict(zip(cls.list_fields(), data_list))
return cls(**kwargs)
def highest_score_field(self) -> str:
"""Return name of field with highest score."""
model_dict = self.model_dump()
return max(model_dict, key=lambda k: model_dict[k])
def highest_score_value(self) -> float:
"""Return value of field with highest score."""
model_dict = self.model_dump()
return max(model_dict.values())
@@ -0,0 +1,14 @@
"""Definition of VisualCommunicationData DTO."""
from pydantic import Field
from ..type_checking_base_model import TypeCheckingBaseModel
from .visual_communication_values import VisualCommunicationValues
class VisualCommunicationData(TypeCheckingBaseModel):
"""Visual communication data class."""
name: str = Field(min_length=1)
annotation: VisualCommunicationValues | None = None
prediction: VisualCommunicationValues | None = None
@@ -0,0 +1,49 @@
"""Definition of VisualCommunicationValues DTO."""
from __future__ import annotations
from ..type_checking_base_model import TypeCheckingBaseModel
from .angle_values import AngleValues
from .contact_values import ContactValues
from .distance_values import DistanceValues
from .framing_values import FramingValues
from .information_value_values import InformationValueValues
from .modality_color_values import ModalityColorValues
from .modality_depth_values import ModalityDepthValues
from .modality_lighting_values import ModalityLightingValues
from .point_of_view_values import PointOfViewValues
from .salience_values import SalienceValues
from .visual_syntax_values import VisualSyntaxValues
class VisualCommunicationValues(TypeCheckingBaseModel):
"""Visual communication values class."""
visual_syntax: VisualSyntaxValues
contact: ContactValues
angle: AngleValues
point_of_view: PointOfViewValues
distance: DistanceValues
modality_lighting: ModalityLightingValues
modality_color: ModalityColorValues
modality_depth: ModalityDepthValues
information_value: InformationValueValues
framing: FramingValues
salience: SalienceValues
@classmethod
def from_random(cls) -> VisualCommunicationValues:
"""Create a random instance."""
return cls(
visual_syntax=VisualSyntaxValues.from_random(),
contact=ContactValues.from_random(),
angle=AngleValues.from_random(),
point_of_view=PointOfViewValues.from_random(),
distance=DistanceValues.from_random(),
modality_lighting=ModalityLightingValues.from_random(),
modality_color=ModalityColorValues.from_random(),
modality_depth=ModalityDepthValues.from_random(),
information_value=InformationValueValues.from_random(),
framing=FramingValues.from_random(),
salience=SalienceValues.from_random(),
)
@@ -0,0 +1,26 @@
"""Definition of VisualSyntaxValues DTO."""
from .values_model import ValuesModel
class VisualSyntaxValues(ValuesModel):
"""Visual syntax values DTO."""
non_transactional_action: float
non_transactional_reaction: float
unidirectional_transactional_action: float
unidirectional_transactional_reaction: float
bidirectional_transactional_action: float
bidirectional_transactional_reaction: float
conversion: float
speech_process: float
classification_overt_taxonomy: float
analytical_exhaustive: float
analytical_disarranged: float
analytical_temporal: float
analytical_distributed: float
analytical_topological: float
analytical_exploded: float
analytical_inclusive: float
symbolic_suggestive: float
symbolic_attributive: float
@@ -0,0 +1,74 @@
"""Definition of ImageRepository class."""
from __future__ import annotations
from io import BytesIO
from pathlib import Path
from PIL import Image
from .dto import ImageData
from .implementations import MinioImplementation
from .interfaces import ImageInterface
class ImageRepository(ImageInterface, MinioImplementation):
"""Image repository class that handles CRUD functionality for
VisualData."""
def __enter__(self) -> ImageRepository:
self.connect()
return self
@staticmethod
def _build_path(image_name: str) -> Path:
"""Build object path."""
assert isinstance(image_name, str)
path = Path('images') / image_name
return path
def get_data(self, image_name: str) -> ImageData | None:
"""Get Visual data."""
assert isinstance(image_name, str)
assert len(image_name) > 0
# build path
path = self._build_path(image_name)
# get object from bucket
buffer = self._get(path)
# handle if no data found
if not buffer:
return None
# convert data
image = Image.open(buffer)
data = ImageData(image=image, name=image_name)
return data
def put_data(self, data: ImageData) -> None:
"""Put visual data."""
assert isinstance(data, ImageData)
# build path
path = self._build_path(data.name)
# save image to buffer
buffer = BytesIO()
data.image.save(buffer, 'png')
# put object in bucket
self._put(path, buffer)
def remove_data(self, image_name: str) -> None:
"""Remove visual data."""
assert isinstance(image_name, str)
assert len(image_name) > 0
# build path
path = self._build_path(image_name)
# remove object
self._delete(path)
def list_names(self) -> list[str]:
"""List names of all images."""
# build path
path = self._build_path('')
# list object paths
obj_path_list = self._list_objects(path)
# strip prefix
name_list = [obj_path.split('/')[-1] for obj_path in obj_path_list]
return name_list
@@ -0,0 +1,2 @@
from .minio_implementation import MinioImplementation
from .mongo_implementation import MongoImplementation
@@ -0,0 +1,186 @@
"""MinIO implementation."""
from __future__ import annotations
import logging
import os
from io import BytesIO
from pathlib import Path
from traceback import format_exc
from minio import Minio
from shared.utils import check_env
from ..interfaces import DatabaseInterface
class MinioImplementation(DatabaseInterface):
"""MinIO basic CRUD implementation."""
def __init__(self):
# ensure necessary env vars available
var_list = {
'MINIO_ENDPOINT',
'MINIO_ACCESS_KEY',
'MINIO_SECRET_KEY',
'MINIO_BUCKET_NAME',
}
check_env(var_list)
# prepare internal variables
self._client: Minio | None = None
self._bucket_name: str | None = None
def connect(self):
"""Connect to MinIO server."""
# prepare arguments
minio_endpoint = str(os.getenv('MINIO_ENDPOINT'))
minio_access_key = str(os.getenv('MINIO_ACCESS_KEY'))
minio_secret_key = str(os.getenv('MINIO_SECRET_KEY'))
minio_bucket_name = str(os.getenv('MINIO_BUCKET_NAME'))
# connect client
client = Minio(
endpoint=minio_endpoint,
access_key=minio_access_key,
secret_key=minio_secret_key,
secure=False,
)
# ensure bucket exists
if not client.bucket_exists(bucket_name=minio_bucket_name):
logging.debug('creating bucket: %s', minio_bucket_name)
client.make_bucket(bucket_name=minio_bucket_name)
# persist state
self._client = client
self._bucket_name = minio_bucket_name
def close(self) -> None:
"""Close connection to MinIO server.
N.B. MinIO connection cannot be closed manually.
"""
self._client = None
self._bucket_name = None
def connected(self):
"""Check connection to Minio."""
res = isinstance(self._client, Minio)
logging.debug(res)
return res
def __enter__(self) -> MinioImplementation:
self.connect()
return self
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
if any(
(
exc_type is not None,
exc_val is not None,
exc_tb is not None,
),
):
logging.error('error while exiting context')
self.close()
def _put(
self,
path: Path,
buffer: BytesIO,
) -> None:
"""Save in-memory buffer as object in MinIO."""
assert isinstance(path, Path)
assert isinstance(buffer, BytesIO)
assert isinstance(self._client, Minio)
assert isinstance(self._bucket_name, str)
# prepare for saving
num_bytes = len(buffer.getvalue())
buffer.seek(0)
# send data to bucket
try:
self._client.put_object(
bucket_name=self._bucket_name,
object_name=path.as_posix(),
length=num_bytes,
data=buffer,
)
logging.debug('saved data to %s', path)
except Exception as exc:
logging.error('failed saving data to MinIO')
raise exc
def _get(
self,
path: Path,
) -> BytesIO | None:
"""Get object from MinIO as in-memory buffer."""
assert isinstance(path, Path)
assert isinstance(self._client, Minio)
assert isinstance(self._bucket_name, str)
try:
# make request
response = self._client.get_object(
bucket_name=self._bucket_name,
object_name=path.as_posix(),
)
assert response.status == 200
# get buffer
buffer = BytesIO()
chunk_size = 2**14
while chunk := response.read(chunk_size):
buffer.write(chunk)
buffer.seek(0)
logging.debug('got %s', path)
return buffer
except Exception:
logging.error('failed getting data from MinIO')
logging.debug(format_exc())
return None
finally:
# close connection if established
if 'response' in locals():
response.close()
response.release_conn()
def _delete(
self,
path: Path,
) -> None:
"""Delete object from MinIO."""
assert isinstance(path, Path)
assert isinstance(self._client, Minio)
assert isinstance(self._bucket_name, str)
# remove object
try:
self._client.remove_object(
bucket_name=self._bucket_name,
object_name=path.as_posix(),
)
logging.debug('deleted %s', path)
except Exception as exc:
logging.error('failed deleting %s', path)
logging.debug(format_exc())
raise exc
def _list_objects(
self,
path: Path,
) -> list[str]:
"""List objects in bucket under path."""
assert isinstance(path, Path)
assert isinstance(self._client, Minio)
assert isinstance(self._bucket_name, str)
try:
# list objects
obj_list = self._client.list_objects(
bucket_name=self._bucket_name,
prefix=path.as_posix(),
recursive=True,
)
# extract info
name_list = [obj.object_name for obj in obj_list]
logging.debug('got %s objects matching %s', len(name_list), path)
return name_list
except Exception as exc:
logging.error('failed listing objects under %s', path)
logging.debug(format_exc())
raise exc
@@ -0,0 +1,145 @@
"""Mongo implementation."""
from __future__ import annotations
import logging
import os
import traceback
from pymongo import MongoClient
from pymongo.collection import Collection
from pymongo.database import Database
from pymongo.errors import ServerSelectionTimeoutError
from shared.utils import check_env
from ..interfaces import DatabaseInterface
class MongoImplementation(DatabaseInterface):
"""MongoDB basic CRUD implementation."""
def __init__(self):
# ensure necessary env vars available
var_list = {
'MONGO_ENDPOINT',
'MONGO_DB',
'MONGO_COLLECTION',
}
check_env(var_list)
# prepare internal variables
self.client: MongoClient | None = None
self.db: Database | None = None
self.collection: Collection | None = None
def connect(self) -> None:
"""Connect to Mongo server."""
# prepare arguments
mongo_endpoint = str(os.getenv('MONGO_ENDPOINT'))
mongo_database = str(os.getenv('MONGO_DB'))
mongo_collection = str(os.getenv('MONGO_COLLECTION'))
# connect client
client = MongoClient(mongo_endpoint)
database = client[mongo_database]
collection = database[mongo_collection]
# set unique index on 'name'
collection.create_index(keys='name', unique=True)
# persist state
self._client = client
self._database = database
self._collection = collection
def close(self) -> None:
"""Close connection to Mongo server."""
self._client.close()
self._client = None
self._database = None
self._collection = None
def connected(self) -> bool:
"""Check connection to Mongo."""
if self._client is None:
return False
try:
# trigger fetch data
_ = self._client.server_info()
res = True
except ServerSelectionTimeoutError:
res = False
logging.debug(res)
return res
def __enter__(self) -> MongoImplementation:
self.connect()
return self
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
if any(
(
exc_type is not None,
exc_val is not None,
exc_tb is not None,
),
):
logging.error('error while exiting context')
traceback.print_exception(exc_type, exc_val, exc_tb)
self.close()
def _save(
self,
data: dict,
query: dict,
) -> None:
"""Save document in Mongo."""
assert isinstance(data, dict)
assert isinstance(query, dict)
assert 'name' in data
assert self.connected()
self._collection.update_one(
filter=query,
update={
'$set': data.copy(),
},
upsert=True,
)
logging.debug('Save %s', data)
def _get(
self,
query: dict,
) -> dict | None:
"""Get document from Mongo."""
assert isinstance(query, dict)
assert self.connected()
doc = self._collection.find_one(query, projection={'_id': False})
logging.debug('Found %s', doc)
return doc
def _delete(
self,
query: dict,
) -> None:
"""Remove document from Mongo."""
assert isinstance(query, dict)
assert self.connected()
doc = self._collection.delete_one(query)
logging.debug('Deleted %s', doc)
def _list_documents(self, key='name') -> list[str]:
"""List documents in Mongo."""
assert isinstance(key, str)
assert len(key) > 0
# build query
doc_list = list(
self._collection.find(
filter={},
projection={
'_id': False,
key: True,
},
),
)
# extract values
value_list = [doc[key] for doc in doc_list]
logging.debug('Got %s document(s)', len(doc_list))
return value_list
@@ -0,0 +1,4 @@
from .database_interface import DatabaseInterface
from .image_interface import ImageInterface
from .model_interface import ModelInterface
from .visual_communication_interface import VisualCommunicationInterface
@@ -0,0 +1,28 @@
"""Definition of DatabaseInterface class."""
from abc import ABC, abstractmethod
class DatabaseInterface(ABC):
"""Interface base class adding 'connect', 'close' and context
functionalities."""
@abstractmethod
def connect(self):
raise NotImplementedError()
@abstractmethod
def close(self):
raise NotImplementedError()
@abstractmethod
def connected(self) -> bool:
raise NotImplementedError()
@abstractmethod
def __enter__(self):
raise NotImplementedError()
@abstractmethod
def __exit__(self, exc_type, exc_val, exc_tb):
raise NotImplementedError()
@@ -0,0 +1,26 @@
"""Definition of ImageInterface."""
from abc import abstractmethod
from ..dto import ImageData
from .database_interface import DatabaseInterface
class ImageInterface(DatabaseInterface):
"""Image interface class."""
@abstractmethod
def get_data(self, image_name: str) -> ImageData | None:
raise NotImplementedError()
@abstractmethod
def put_data(self, data: ImageData) -> None:
raise NotImplementedError()
@abstractmethod
def remove_data(self, image_name: str) -> None:
raise NotImplementedError()
@abstractmethod
def list_names(self) -> list[str]:
raise NotImplementedError()
@@ -0,0 +1,26 @@
"""Definition of ModelInterface."""
from abc import abstractmethod
from ..dto import ModelData
from .database_interface import DatabaseInterface
class ModelInterface(DatabaseInterface):
"""Model interface class."""
@abstractmethod
def get_data(self, object_name: str) -> ModelData | None:
raise NotImplementedError()
@abstractmethod
def put_data(self, data: ModelData) -> None:
raise NotImplementedError()
@abstractmethod
def remove_data(self, object_name: str) -> None:
raise NotImplementedError()
@abstractmethod
def list_names(self) -> list[str]:
raise NotImplementedError()
@@ -0,0 +1,26 @@
"""Definition of VisualCommunicationInterface."""
from abc import abstractmethod
from ..dto import VisualCommunicationData
from .database_interface import DatabaseInterface
class VisualCommunicationInterface(DatabaseInterface):
"""Visual communication interface class."""
@abstractmethod
def get_data(self, name: str) -> VisualCommunicationData | None:
raise NotImplementedError()
@abstractmethod
def put_data(self, data: VisualCommunicationData) -> None:
raise NotImplementedError()
@abstractmethod
def remove_data(self, name: str) -> None:
raise NotImplementedError()
@abstractmethod
def list_names(self) -> list[str]:
raise NotImplementedError()
@@ -0,0 +1,77 @@
"""Definition of ModelRepository class."""
from __future__ import annotations
from pathlib import Path
from .dto import HexadecimalString, ModelData
from .implementations import MinioImplementation
from .interfaces import ModelInterface
class ModelRepository(ModelInterface, MinioImplementation):
"""Model repository class that handles CRUD functionality for ModelData."""
def __enter__(self) -> ModelRepository:
self.connect()
return self
@staticmethod
def _prefix() -> Path:
"""Object name prefix."""
return Path('models')
@classmethod
def _build_object_name(cls, data: ModelData) -> str:
"""Build object name from data."""
return f'{data.class_name}-{data.buffer_checksum}'
def get_data(self, object_name: str) -> ModelData | None:
"""Get model data."""
assert isinstance(object_name, str)
# build path
path = self._prefix() / object_name
# get object from bucket
buffer = self._get(path)
# handle if no data found
if not buffer:
return None
# extract info
class_name, buffer_checksum_str = object_name.split('-')
# convert data
buffer_checksum = HexadecimalString(buffer_checksum_str)
# instantiate data
data = ModelData(
buffer=buffer,
buffer_checksum=buffer_checksum,
class_name=class_name,
)
return data
def put_data(self, data: ModelData) -> None:
"""Put model data."""
assert isinstance(data, ModelData)
# build object name
object_name = self._build_object_name(data)
# build path
path = self._prefix() / object_name
# put object in bucket
self._put(path, data.buffer)
def remove_data(self, object_name: str) -> None:
"""Remove model data."""
assert isinstance(object_name, str)
# build path
path = self._prefix() / object_name
# remove object
self._delete(path)
def list_names(self) -> list[str]:
"""List names of all models."""
# build path
path = self._prefix()
# list object paths
obj_path_list = self._list_objects(path)
# strip prefix
name_list = [obj_path.split('/')[-1] for obj_path in obj_path_list]
return name_list
@@ -0,0 +1,56 @@
"""Definition of VisualCommunicationRepository class."""
from __future__ import annotations
from .dto import VisualCommunicationData
from .implementations import MongoImplementation
from .interfaces import VisualCommunicationInterface
class VisualCommunicationRepository(VisualCommunicationInterface, MongoImplementation):
"""Visual communication repository class that handles CRUD functionality
for VisualCommunicationData."""
def __enter__(self) -> VisualCommunicationRepository:
self.connect()
return self
def get_data(self, name: str) -> VisualCommunicationData | None:
"""Get visual communication data."""
assert isinstance(name, str)
assert len(name) > 0
# build query
query = {'name': name}
# get document from mongo
doc = self._get(query)
# handle if no data found
if doc is None:
return None
# instantiate object
data = VisualCommunicationData(**doc)
return data
def put_data(self, data: VisualCommunicationData) -> None:
"""Put visual communication data."""
assert isinstance(data, VisualCommunicationData)
# convert to dict
data = data.model_dump(mode='json')
# build query
query = {'name': data['name']}
# save to mongo
self._save(data, query)
def remove_data(self, name: str) -> None:
"""Remove visual communication data."""
assert isinstance(name, str)
assert len(name) > 0
# build query
query = {'name': name}
# remove document from mongo
self._delete(query)
def list_names(self) -> list[str]:
"""List names of all documents."""
# list documents
name_list = self._list_documents(key='name')
return name_list