added infrastructure code
Code Quality Pipeline / code-quality (pull_request) Failing after 1m19s
Test Python Package / test (pull_request) Failing after 50s

This commit is contained in:
Brian Bjarke Jensen
2025-09-18 12:13:39 +02:00
parent 9b4f0daf58
commit 0ba860b892
34 changed files with 3390 additions and 1 deletions
+14
View File
@@ -0,0 +1,14 @@
"""
Repositories package for visual-semiotic-ai-analysis.
Exposes repository implementations for use throughout the project.
"""
from .annotation_repository import AnnotationRepository
from .image_repository import ImageRepository
from .job_repository import JobRepository
__all__ = [
"AnnotationRepository",
"ImageRepository",
"JobRepository",
]
@@ -0,0 +1,106 @@
"""Definition of AnnotationRepository."""
from __future__ import annotations
from uuid import UUID
import json
from pathlib import Path
from io import BytesIO
from python_repositories.adapters import MinioAdapter
from data_store.interfaces import AnnotationRepositoryInterface
from data_store.dto import (
Annotation,
DistanceEnum,
AngleEnum,
PointOfViewEnum,
ContactEnum,
)
class AnnotationRepository(MinioAdapter, AnnotationRepositoryInterface):
"""AnnotationRepository implementation using MinIO as the backend."""
encoding: str = "utf-8"
data_format: str = "json"
folder_name: str = "annotations"
@classmethod
def _object_name(cls, job_id: UUID) -> str:
"""Generate the object name for a given job ID."""
# Check input
if not isinstance(job_id, UUID):
raise ValueError("job_id must be a valid UUID.")
# Return object name
return f"{cls.folder_name}/{job_id}.{cls.data_format}"
def __enter__(self) -> AnnotationRepository:
"""Enter the runtime context related to this object."""
super().__enter__()
return self
def get(self, job_id: UUID) -> Annotation | None:
"""Retrieve an Annotation by its job ID."""
# Check input
if not isinstance(job_id, UUID):
raise ValueError("job_id must be a valid UUID.")
# Get object
object_name = self._object_name(job_id)
buffer = self._get(object_name)
if buffer is None:
return None
# Convert buffer to Annotation
try:
data_str = buffer.read().decode(self.encoding)
data = json.loads(data_str)
annotation = Annotation(
distance=DistanceEnum(data["distance"]),
angle=AngleEnum(data["angle"]),
point_of_view=PointOfViewEnum(data["point_of_view"]),
contact=ContactEnum(data["contact"]),
)
except (KeyError, ValueError) as e:
self.logger.error(
"Failed to parse Annotation from data",
object_name=object_name,
error=str(e),
)
return None
return annotation
def put(self, annotation: Annotation, job_id: UUID) -> None:
"""Update or create an Annotation."""
# Check inputs
if not isinstance(annotation, Annotation):
raise ValueError("annotation must be an Annotation instance.")
if not isinstance(job_id, UUID):
raise ValueError("job_id must be a valid UUID.")
# Convert Annotation to json
data_json = annotation.model_dump(mode="json")
data_bytes = json.dumps(data_json).encode(self.encoding)
buffer = BytesIO(data_bytes)
# Put object
object_name = self._object_name(job_id)
self._put(object_name, buffer)
def delete(self, job_id: UUID) -> None:
"""Delete an Annotation by its job ID."""
# Check input
if not isinstance(job_id, UUID):
raise ValueError("job_id must be a valid UUID.")
# Delete object
object_name = self._object_name(job_id)
self._delete(object_name)
def list_all(self) -> list[UUID]:
"""List all job IDs in the repository."""
# List objects in the bucket
objects = self._list_objects(prefix=self.folder_name + "/")
# Stop early if no objects
if len(objects) == 0:
return []
# Remove folder prefix and file extension
object_stems = [Path(obj).stem for obj in objects]
# Convert to list of UUIDs
job_ids = [UUID(name) for name in object_stems]
return job_ids
@@ -0,0 +1,82 @@
"""Definition of ImageRepository class."""
from __future__ import annotations
from pathlib import Path
from io import BytesIO
from uuid import UUID
from PIL import Image
from python_repositories.adapters import MinioAdapter
from data_store.interfaces import ImageRepositoryInterface
class ImageRepository(MinioAdapter, ImageRepositoryInterface):
"""ImageRepository implementation using MinIO as the backend."""
image_format: str = "PNG"
folder_name: str = "images"
@classmethod
def _object_name(cls, image_id: UUID) -> str:
"""Generate the object name for a given image ID."""
# Check input
if not isinstance(image_id, UUID):
raise ValueError("image_id must be a valid UUID.")
# Return object name
return f"{cls.folder_name}/{image_id}.{cls.image_format.lower()}"
def __enter__(self) -> ImageRepository:
"""Enter the runtime context related to this object."""
super().__enter__()
return self
def get(self, image_id: UUID) -> Image.Image | None:
"""Retrieve an Image by its ID."""
# Check input
if not isinstance(image_id, UUID):
raise ValueError("image_id must be a valid UUID.")
# Get object
object_name = self._object_name(image_id)
buffer = self._get(object_name)
if buffer is None:
return None
# Convert bytes back to PIL Image
image: Image.Image = Image.open(buffer, formats=[self.image_format])
return image
def put(self, image: Image.Image, image_id: UUID) -> None:
"""Update or create an Image."""
# Check inputs
if not isinstance(image, Image.Image):
raise ValueError("image must be a PIL Image instance.")
if not isinstance(image_id, UUID):
raise ValueError("image_id must be a valid UUID.")
# Convert image to bytes
buffer = BytesIO()
image.save(buffer, self.image_format)
# Put object
object_name = self._object_name(image_id)
self._put(object_name, buffer)
def delete(self, image_id: UUID) -> None:
"""Delete an Image by its ID."""
# Check input
if not isinstance(image_id, UUID):
raise ValueError("image_id must be a valid UUID.")
# Delete object
object_name = self._object_name(image_id)
self._delete(object_name)
def list_all(self) -> list[UUID]:
"""List all image IDs in the repository."""
# List objects in the bucket
objects = self._list_objects(prefix=self.folder_name + "/")
# Stop early if no objects
if len(objects) == 0:
return []
# Remove folder prefix and file extension
object_stems = [Path(obj).stem for obj in objects]
# Convert to list of UUIDs
image_ids = [UUID(name) for name in object_stems]
return image_ids
+78
View File
@@ -0,0 +1,78 @@
"""Definition of JobRepository class."""
from __future__ import annotations
from uuid import UUID
from python_repositories.adapters import MinioAdapter
from data_store.interfaces import JobRepositoryInterface
from data_store.repositories import AnnotationRepository, ImageRepository
from data_store.dto import Job
class JobRepository(MinioAdapter, JobRepositoryInterface):
"""JobRepository implementation using MinIO as the backend."""
def __enter__(self) -> JobRepository:
"""Enter the runtime context related to this object."""
super().__enter__()
return self
def get(self, job_id: UUID) -> Job | None:
"""Retrieve a Job by its ID."""
# Check input
if not isinstance(job_id, UUID):
raise ValueError("job_id must be a valid UUID.")
# Get image
with ImageRepository() as image_repo:
image = image_repo.get(job_id)
if image is None:
self.logger.warning(f"Image for job ID {job_id} not found.")
return None
# Get annotation
with AnnotationRepository() as annotation_repo:
annotation = annotation_repo.get(job_id)
if annotation is None:
self.logger.warning(f"Annotation for job ID {job_id} not found.")
return None
# Create Job DTO
job = Job(
job_id=job_id,
image=image,
annotation=annotation,
)
return job
def put(self, job: Job) -> None:
"""Update or create a Job."""
# Check input
if not isinstance(job, Job):
raise ValueError("job must be a Job instance.")
# Store image
with ImageRepository() as image_repo:
image_repo.put(job.image, job.job_id)
# Store annotation
with AnnotationRepository() as annotation_repo:
annotation_repo.put(job.annotation, job.job_id)
def delete(self, job_id: UUID) -> None:
"""Delete a Job by its ID."""
# Check input
if not isinstance(job_id, UUID):
raise ValueError("job_id must be a valid UUID.")
# Delete image
with ImageRepository() as image_repo:
image_repo.delete(job_id)
# Delete annotation
with AnnotationRepository() as annotation_repo:
annotation_repo.delete(job_id)
def list_all(self) -> list[UUID]:
"""List all Job IDs."""
# List image IDs
with ImageRepository() as image_repo:
image_ids = set(image_repo.list_all())
# List annotation IDs
with AnnotationRepository() as annotation_repo:
annotation_ids = set(annotation_repo.list_all())
# Find intersection of IDs
job_ids = list(image_ids.intersection(annotation_ids))
return job_ids