added infrastructure code
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
"""
|
||||
DTO package for visual-semiotic-ai-analysis.
|
||||
Exposes core data transfer objects and enums for use throughout the project.
|
||||
"""
|
||||
|
||||
from .angle_enum import AngleEnum
|
||||
from .annotation import Annotation
|
||||
from .contact_enum import ContactEnum
|
||||
from .distance_enum import DistanceEnum
|
||||
from .job import Job
|
||||
from .point_of_view_enum import PointOfViewEnum
|
||||
|
||||
__all__ = [
|
||||
"AngleEnum",
|
||||
"Annotation",
|
||||
"ContactEnum",
|
||||
"DistanceEnum",
|
||||
"Job",
|
||||
"PointOfViewEnum",
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
"""Definition of AngleEnum DTO."""
|
||||
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
class AngleEnum(StrEnum):
|
||||
"""Enumeration for Angle values."""
|
||||
|
||||
HIGH = "high"
|
||||
EYE_LEVEL = "eye-level"
|
||||
LOW = "low"
|
||||
@@ -0,0 +1,16 @@
|
||||
"""Definition of Annotation DTO."""
|
||||
|
||||
from .type_checking_base_model import TypeCheckingBaseModel
|
||||
from .distance_enum import DistanceEnum
|
||||
from .angle_enum import AngleEnum
|
||||
from .point_of_view_enum import PointOfViewEnum
|
||||
from .contact_enum import ContactEnum
|
||||
|
||||
|
||||
class Annotation(TypeCheckingBaseModel):
|
||||
"""Data Transfer Object representing an Annotation."""
|
||||
|
||||
distance: DistanceEnum
|
||||
angle: AngleEnum
|
||||
point_of_view: PointOfViewEnum
|
||||
contact: ContactEnum
|
||||
@@ -0,0 +1,10 @@
|
||||
"""Definition of ContactEnum DTO."""
|
||||
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
class ContactEnum(StrEnum):
|
||||
"""Enumeration for Contact."""
|
||||
|
||||
OFFER = "offer"
|
||||
DEMAND = "demand"
|
||||
@@ -0,0 +1,11 @@
|
||||
"""Definition of DistanceEnum DTO."""
|
||||
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
class DistanceEnum(StrEnum):
|
||||
"""Enumeration for Distance values."""
|
||||
|
||||
CLOSE = "close"
|
||||
MEDIUM = "medium"
|
||||
LONG = "long"
|
||||
@@ -0,0 +1,21 @@
|
||||
"""Definition of Job DTO."""
|
||||
|
||||
from uuid import uuid4, UUID
|
||||
from pydantic import Field
|
||||
from PIL import Image
|
||||
|
||||
from .type_checking_base_model import TypeCheckingBaseModel
|
||||
from .annotation import Annotation
|
||||
|
||||
|
||||
class Job(TypeCheckingBaseModel):
|
||||
"""Data Transfer Object representing a Job."""
|
||||
|
||||
class Config:
|
||||
"""Pydantic configuration for Job DTO."""
|
||||
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
job_id: UUID = Field(default_factory=uuid4)
|
||||
image: Image.Image
|
||||
annotation: Annotation
|
||||
@@ -0,0 +1,10 @@
|
||||
"""Definition of PointOfViewEnum DTO."""
|
||||
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
class PointOfViewEnum(StrEnum):
|
||||
"""Enumeration for Point of View."""
|
||||
|
||||
FRONTAL = "frontal"
|
||||
OBLIQUE = "oblique"
|
||||
@@ -0,0 +1,12 @@
|
||||
"""Definition of class_base class."""
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
|
||||
class TypeCheckingBaseModel(BaseModel):
|
||||
"""BaseModel with added type checking on input types."""
|
||||
|
||||
model_config = ConfigDict(
|
||||
validate_assignment=True, # ensure that dynamic validations adhere to defined types
|
||||
frozen=True, # ensure data immutability
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
"""
|
||||
Interfaces package for visual-semiotic-ai-analysis.
|
||||
Exposes repository interfaces for use throughout the project.
|
||||
"""
|
||||
|
||||
from .annotation_repository_interface import AnnotationRepositoryInterface
|
||||
from .job_repository_interface import JobRepositoryInterface
|
||||
from .image_repository_interface import ImageRepositoryInterface
|
||||
|
||||
__all__ = [
|
||||
"JobRepositoryInterface",
|
||||
"ImageRepositoryInterface",
|
||||
"AnnotationRepositoryInterface",
|
||||
]
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Definition of AnnotationRepositoryInterface."""
|
||||
|
||||
from uuid import UUID
|
||||
from abc import ABC, abstractmethod
|
||||
from data_store.dto import Annotation
|
||||
|
||||
|
||||
class AnnotationRepositoryInterface(ABC):
|
||||
"""AnnotationRepository interface."""
|
||||
|
||||
@abstractmethod
|
||||
def get(self, job_id: UUID) -> Annotation | None:
|
||||
"""Retrieve an Annotation by its associated Job ID."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def put(self, annotation: Annotation, job_id: UUID) -> None:
|
||||
"""Update or create an Annotation."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def delete(self, job_id: UUID) -> None:
|
||||
"""Delete an Annotation by its associated Job ID."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def list_all(self) -> list[UUID]:
|
||||
"""List all Job IDs with associated Annotations in the repository."""
|
||||
raise NotImplementedError()
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Definition of ImageRepositoryInterface"""
|
||||
|
||||
from uuid import UUID
|
||||
from abc import ABC, abstractmethod
|
||||
from PIL import Image
|
||||
|
||||
|
||||
class ImageRepositoryInterface(ABC):
|
||||
"""ImageRepository interface."""
|
||||
|
||||
@abstractmethod
|
||||
def get(self, image_id: UUID) -> Image.Image | None:
|
||||
"""Retrieve an Image by its ID."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def put(self, image: Image.Image, image_id: UUID) -> None:
|
||||
"""Update or create an Image."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def delete(self, image_id: UUID) -> None:
|
||||
"""Delete an Image by its ID."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def list_all(self) -> list[UUID]:
|
||||
"""List all image IDs in the repository."""
|
||||
raise NotImplementedError()
|
||||
@@ -0,0 +1,30 @@
|
||||
"""Definition of JobRepositoryInterface."""
|
||||
|
||||
from uuid import UUID
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from data_store.dto import Job
|
||||
|
||||
|
||||
class JobRepositoryInterface(ABC):
|
||||
"""JobRepository interface."""
|
||||
|
||||
@abstractmethod
|
||||
def get(self, job_id: UUID) -> Job | None:
|
||||
"""Retrieve a Job by its ID."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def put(self, job: Job) -> None:
|
||||
"""Update or create a Job."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def delete(self, job_id: UUID) -> None:
|
||||
"""Delete a Job by its ID."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def list_all(self) -> list[UUID]:
|
||||
"""List all Job IDs in the repository."""
|
||||
raise NotImplementedError()
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user