added integration tests and repository pattern for image, model and visual communication DTOs
Code Quality Pipeline / Check Code (pull_request) Failing after 3m21s
Code Quality Pipeline / Check Code (pull_request) Failing after 3m21s
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
from .image_repository import ImageRepository
|
||||
from .model_repository import ModelRepository
|
||||
from .visual_communication_repository import VisualCommunicationRepository
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user