Compare commits

..
47 Commits
Author SHA1 Message Date
brian 5faf3ef2a7 added use of larger model for refining prompt
Formatting Check / formatting-check (push) Successful in 13s
Python Code Quality / python-code-quality (push) Failing after 21s
Python Test / python-test (push) Successful in 45s
Sync Label Studio Annotations / sync_storage (push) Failing after 2m20s
2025-09-26 19:08:56 +00:00
Brian Bjarke Jensen 007161a927 code quality fixes
Formatting Check / formatting-check (push) Successful in 9s
Python Code Quality / python-code-quality (push) Successful in 18s
Python Test / python-test (push) Successful in 30s
Sync Label Studio Annotations / sync_storage (push) Successful in 57s
2025-09-26 00:02:02 +02:00
Brian Bjarke Jensen d804cc4a13 installed requests types 2025-09-26 00:01:54 +02:00
Brian Bjarke Jensen 6075848c91 added code to log to mlflow
Python Code Quality / python-code-quality (push) Failing after 19s
Python Test / python-test (push) Successful in 40s
Formatting Check / formatting-check (push) Successful in 11s
2025-09-25 23:57:57 +02:00
Brian Bjarke Jensen cdde0131a7 installed mlflow 2025-09-25 23:57:49 +02:00
Brian Bjarke Jensen 9a3694fc98 Merge pull request 'add-script-to-query-AI-model' (#17) from add-script-to-query-AI-model into main
Formatting Check / formatting-check (push) Successful in 49s
Python Test / python-test (push) Successful in 43s
Sync Label Studio Annotations / sync_storage (push) Successful in 21s
Python Code Quality / python-code-quality (push) Successful in 15s
Reviewed-on: #17
2025-09-21 19:48:53 +02:00
Brian Bjarke Jensen cbac5ded35 ruff format fixes
Formatting Check / formatting-check (pull_request) Successful in 53s
Python Code Quality / python-code-quality (pull_request) Successful in 15s
Python Test / python-test (pull_request) Successful in 45s
2025-09-21 19:40:01 +02:00
Brian Bjarke Jensen 7e9c4474ac ran a few query sessions
Formatting Check / formatting-check (pull_request) Successful in 51s
Python Code Quality / python-code-quality (pull_request) Failing after 16s
Python Test / python-test (pull_request) Successful in 41s
2025-09-20 10:29:59 +02:00
Brian Bjarke Jensen 3c8845aa6d added script to query ai model 2025-09-20 10:29:52 +02:00
Brian Bjarke Jensen 604b00c8ed fixed bug that didn't allow choice to be None 2025-09-20 10:26:57 +02:00
Brian Bjarke Jensen 26edf8d372 Merge pull request 'implemented CI template for code quality' (#14) from #13-implement-CI-templates into main
Python Code Quality / python-code-quality (push) Successful in 14s
Python Test / python-test (push) Successful in 39s
Formatting Check / formatting-check (push) Successful in 39s
Sync Label Studio Annotations / sync_storage (push) Successful in 1m5s
Reviewed-on: #14
2025-09-19 21:57:27 +02:00
Brian Bjarke Jensen b6f53c691b implemented formatting check pipeline template
Formatting Check / formatting-check (pull_request) Successful in 44s
Python Test / python-test (pull_request) Successful in 19s
Python Code Quality / python-code-quality (pull_request) Successful in 13s
2025-09-19 21:35:19 +02:00
Brian Bjarke Jensen 4a045125aa implemented pytest CI template
Python Code Quality / python-quality (pull_request) Successful in 13s
Python Test / test (pull_request) Successful in 24s
2025-09-19 21:30:37 +02:00
Brian Bjarke Jensen b01e1213de tried AI suggestion to fix CI template reference
Python Code Quality / python-quality (pull_request) Successful in 14s
Python Test / test (pull_request) Successful in 25s
2025-09-19 21:23:58 +02:00
Brian Bjarke Jensen 97ac547058 implemented CI template for code quality
Python Code Quality / python-quality (pull_request) Failing after 0s
Python Test / test (pull_request) Successful in 41s
2025-09-19 21:18:35 +02:00
Brian Bjarke Jensen b828347ca3 Merge pull request 'added CI step to trigger label studio sync to target storage' (#12) from readd-label-studio-sync-to-minio into main
Code Quality Pipeline / code-quality (push) Successful in 1m21s
Test Python Package / test (push) Successful in 25s
Sync Label Studio Annotations / sync_storage (push) Successful in 57s
Reviewed-on: #12
2025-09-19 00:07:31 +02:00
Brian Bjarke Jensen 51978810f4 added CI step to trigger label studio sync to target storage
Code Quality Pipeline / code-quality (pull_request) Successful in 36s
Test Python Package / test (pull_request) Successful in 26s
2025-09-19 00:04:09 +02:00
Brian Bjarke Jensen 5008c0970e Merge pull request 'add-script-to-convert-label-studio-output-to-annotation' (#9) from add-script-to-convert-label-studio-output-to-annotation into main
Code Quality Pipeline / code-quality (push) Successful in 37s
Test Python Package / test (push) Successful in 25s
Reviewed-on: #9
2025-09-18 23:57:18 +02:00
Brian Bjarke Jensen b6b35448b0 code quality fixes
Code Quality Pipeline / code-quality (pull_request) Successful in 37s
Test Python Package / test (pull_request) Successful in 31s
2025-09-18 23:50:36 +02:00
Brian Bjarke Jensen 28abd22571 updated to run python script to synchronize annotations from label studio to annotation repository
Code Quality Pipeline / code-quality (pull_request) Failing after 1m12s
Test Python Package / test (pull_request) Successful in 41s
2025-09-18 23:48:49 +02:00
Brian Bjarke Jensen 829cfd2e00 updated CI to use correct variable 2025-09-18 23:48:22 +02:00
Brian Bjarke Jensen f0a3bf89b0 added script to sync annotations from label studio 2025-09-18 23:48:07 +02:00
Brian Bjarke Jensen a124a72c96 allowed annotation to have None when no choice made 2025-09-18 23:47:56 +02:00
Brian Bjarke Jensen f858877bb5 changed cron schedule back
Code Quality Pipeline / code-quality (push) Successful in 27s
Test Python Package / test (push) Successful in 27s
2025-09-18 22:33:41 +02:00
Brian Bjarke Jensen 84c8133252 added step to ensure label studio output files have .json extension
Code Quality Pipeline / code-quality (push) Successful in 49s
Test Python Package / test (push) Successful in 28s
Sync Label Studio / sync_storage (push) Successful in 20s
2025-09-18 22:19:29 +02:00
Brian Bjarke Jensen bf098e16c8 fixed minio paths
Test Python Package / test (push) Successful in 29s
Code Quality Pipeline / code-quality (push) Successful in 46s
Sync Label Studio / sync_storage (push) Successful in 15s
2025-09-18 22:15:37 +02:00
Brian Bjarke Jensen 36523b750d removed unsupported flag
Code Quality Pipeline / code-quality (push) Successful in 43s
Test Python Package / test (push) Successful in 29s
Sync Label Studio / sync_storage (push) Successful in 13s
2025-09-18 22:06:15 +02:00
Brian Bjarke Jensen 3806cd7618 fixed mc command
Test Python Package / test (push) Successful in 28s
Sync Label Studio / sync_storage (push) Failing after 15s
Code Quality Pipeline / code-quality (push) Successful in 42s
2025-09-18 22:01:00 +02:00
Brian Bjarke Jensen 9985561130 slower cron schedule
Code Quality Pipeline / code-quality (push) Successful in 49s
Test Python Package / test (push) Successful in 53s
Sync Label Studio / sync_storage (push) Successful in 19s
2025-09-18 21:51:30 +02:00
Brian Bjarke Jensen 609800483b removed unsupported flag
Code Quality Pipeline / code-quality (push) Has been cancelled
Test Python Package / test (push) Has been cancelled
Sync Label Studio / sync_storage (push) Has started running
2025-09-18 21:50:08 +02:00
Brian Bjarke Jensen 051ce2c771 Merge pull request 'added step to install minio client' (#6) from fix-sync-label-studio-output-pipeline into main
Code Quality Pipeline / code-quality (push) Successful in 52s
Test Python Package / test (push) Successful in 33s
Sync Label Studio / sync_storage (push) Failing after 52s
Reviewed-on: #6
2025-09-18 21:40:34 +02:00
Brian Bjarke Jensen 5a04db5c11 added step to install minio client
Code Quality Pipeline / code-quality (pull_request) Successful in 57s
Test Python Package / test (pull_request) Successful in 43s
2025-09-18 21:29:51 +02:00
Brian Bjarke Jensen 18541c95de Merge pull request 'add-new-annotation-types' (#5) from add-new-annotation-types into main
Code Quality Pipeline / code-quality (push) Successful in 1m15s
Test Python Package / test (push) Successful in 43s
Sync Label Studio / sync_storage (push) Failing after 5s
Reviewed-on: #5
2025-09-18 21:19:05 +02:00
Brian Bjarke Jensen 8c061ce8d5 sped up cron schedule temporary
Code Quality Pipeline / code-quality (pull_request) Successful in 2m5s
Test Python Package / test (pull_request) Successful in 43s
2025-09-18 21:09:47 +02:00
Brian Bjarke Jensen 63ed329fd9 code quality fixes 2025-09-18 21:06:59 +02:00
Brian Bjarke Jensen 4a8e091a2d added new annotation types 2025-09-18 21:05:47 +02:00
Brian Bjarke Jensen 86935ab18e Merge pull request 'upload-images-to-minio' (#2) from upload-images-to-minio into main
Code Quality Pipeline / code-quality (push) Successful in 51s
Test Python Package / test (push) Successful in 37s
Reviewed-on: #2
2025-09-18 20:03:11 +02:00
Brian Bjarke Jensen fcbae0d87b updated CI step title
Code Quality Pipeline / code-quality (pull_request) Successful in 1m22s
Test Python Package / test (pull_request) Successful in 49s
2025-09-18 19:48:38 +02:00
Brian Bjarke Jensen 60c0993d59 added steps to correct content type in minio
Code Quality Pipeline / code-quality (pull_request) Failing after 39s
Test Python Package / test (pull_request) Successful in 30s
2025-09-18 19:45:17 +02:00
Brian Bjarke Jensen f1bfd02b7e sorted imports
Code Quality Pipeline / code-quality (pull_request) Successful in 39s
Test Python Package / test (pull_request) Successful in 31s
2025-09-18 19:20:25 +02:00
Brian Bjarke Jensen c3d60bbd44 added CI step to sync between label studio and output bucket in minio
Code Quality Pipeline / code-quality (pull_request) Successful in 1m20s
Test Python Package / test (pull_request) Successful in 49s
2025-09-18 19:19:30 +02:00
Brian Bjarke Jensen a8458c11f1 added script to upload images to minio 2025-09-18 19:19:07 +02:00
Brian Bjarke Jensen 3dd8307249 Merge pull request 'added infrastructure code' (#1) from add-infrastructure into main
Code Quality Pipeline / code-quality (push) Successful in 37s
Test Python Package / test (push) Successful in 29s
Reviewed-on: #1
2025-09-18 13:23:27 +02:00
Brian Bjarke Jensen f8e435bfe2 removed bound ports to avoid clashing with servers minio instance
Test Python Package / test (pull_request) Successful in 1m27s
Code Quality Pipeline / code-quality (pull_request) Successful in 39s
2025-09-18 12:40:57 +02:00
Brian Bjarke Jensen f96159829c fixed pytest in CI
Code Quality Pipeline / code-quality (pull_request) Successful in 39s
Test Python Package / test (pull_request) Failing after 29s
2025-09-18 12:24:56 +02:00
Brian Bjarke Jensen 25286c3eab fixed CI step
Code Quality Pipeline / code-quality (pull_request) Successful in 39s
Test Python Package / test (pull_request) Failing after 33s
2025-09-18 12:20:34 +02:00
Brian Bjarke Jensen 0ba860b892 added infrastructure code
Code Quality Pipeline / code-quality (pull_request) Failing after 1m19s
Test Python Package / test (pull_request) Failing after 50s
2025-09-18 12:13:39 +02:00
45 changed files with 6661 additions and 1 deletions
+11
View File
@@ -0,0 +1,11 @@
name: Formatting Check
on:
push:
branches:
- main
pull_request:
jobs:
formatting-check:
uses: brian/CI-templates/.gitea/workflows/web/[email protected]
+13
View File
@@ -0,0 +1,13 @@
name: Python Code Quality
on:
push:
branches:
- main
pull_request:
jobs:
python-code-quality:
uses: brian/CI-templates/.gitea/workflows/python/[email protected]
with:
python-version: ${{ vars.PYTHON_VERSION }}
+17
View File
@@ -0,0 +1,17 @@
name: Python Test
on:
push:
branches:
- main
pull_request:
jobs:
python-test:
uses: brian/CI-templates/.gitea/workflows/python/[email protected]
with:
python-version: ${{ vars.PYTHON_VERSION }}
coverage-package: "data_store"
api-url: ${{ vars.API_URL }}
secrets:
CI_RUNNER_TOKEN: ${{ secrets.CI_RUNNER_TOKEN }}
+43
View File
@@ -0,0 +1,43 @@
name: Sync Label Studio Annotations
on:
schedule:
# This job will run every day at 03:00 AM
- cron: "0 3 * * *"
jobs:
sync_storage:
runs-on: ubuntu-latest
steps:
- name: Sync Label Studio to target storage
env:
LABEL_STUDIO_API_TOKEN: ${{ secrets.LABEL_STUDIO_API_TOKEN }}
run: |
curl -X POST \
-H "Authorization: Token $LABEL_STUDIO_API_TOKEN" \
"https://label-studio.gt-proj.com/api/storages/export/s3/1/sync"
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ vars.PYTHON_VERSION }}
- name: Install uv
run: pip install uv
- name: Install dependencies
env:
UV_LINK_MODE: copy
run: uv sync --all-extras
- name: Sync target storage to annotations
env:
MINIO_ENDPOINT: ${{ vars.MINIO_ENDPOINT }}
MINIO_ACCESS_KEY: ${{ secrets.MINIO_ACCESS_KEY }}
MINIO_SECRET_KEY: ${{ secrets.MINIO_SECRET_KEY }}
MINIO_BUCKET: ${{ vars.MINIO_BUCKET }}
run: |
uv run python scripts/sync_label_studio_annotations.py
+40
View File
@@ -0,0 +1,40 @@
repos:
# General repository hygiene hooks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- id: debug-statements
- id: name-tests-test
- id: check-merge-conflict
# Python linting and formatting with Ruff
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.8
hooks:
- id: ruff-check
args: [--fix]
- id: ruff-format
# Static type checking with mypy
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.8.0
hooks:
- id: mypy
# Python syntax modernization with pyupgrade
- repo: https://github.com/asottile/pyupgrade
rev: v3.20.0
hooks:
- id: pyupgrade
args: ["--py311-plus"]
# Formatting for Markdown, JSON, and YAML with Prettier
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.1.0
hooks:
- id: prettier
files: "\\.(md|json|yaml|yml)$"
+1
View File
@@ -0,0 +1 @@
3.12
-1
View File
@@ -1,2 +1 @@
# visual-semiotic-ai-analysis # visual-semiotic-ai-analysis
View File
+26
View File
@@ -0,0 +1,26 @@
"""
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 .colour_enum import ColourEnum
from .contact_enum import ContactEnum
from .depth_enum import DepthEnum
from .distance_enum import DistanceEnum
from .job import Job
from .lighting_enum import LightingEnum
from .point_of_view_enum import PointOfViewEnum
__all__ = [
"AngleEnum",
"Annotation",
"ContactEnum",
"DistanceEnum",
"Job",
"PointOfViewEnum",
"ColourEnum",
"DepthEnum",
"LightingEnum",
]
+11
View File
@@ -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"
+29
View File
@@ -0,0 +1,29 @@
"""Definition of Annotation DTO."""
from .type_checking_base_model import TypeCheckingBaseModel
from .angle_enum import AngleEnum
from .colour_enum import ColourEnum
from .contact_enum import ContactEnum
from .depth_enum import DepthEnum
from .distance_enum import DistanceEnum
from .lighting_enum import LightingEnum
from .point_of_view_enum import PointOfViewEnum
class Annotation(TypeCheckingBaseModel):
"""Data Transfer Object representing an Annotation."""
angle: AngleEnum | None
angle_uncertainty: bool
colour: ColourEnum | None
colour_uncertainty: bool
contact: ContactEnum | None
contact_uncertainty: bool
depth: DepthEnum | None
depth_uncertainty: bool
distance: DistanceEnum | None
distance_uncertainty: bool
lighting: LightingEnum | None
lighting_uncertainty: bool
point_of_view: PointOfViewEnum | None
point_of_view_uncertainty: bool
+11
View File
@@ -0,0 +1,11 @@
"""Definition of ColourEnum DTO."""
from enum import StrEnum
class ColourEnum(StrEnum):
"""Enumeration for Colour values."""
HIGH = "high"
MEDIUM = "medium"
LOW = "low"
+10
View File
@@ -0,0 +1,10 @@
"""Definition of ContactEnum DTO."""
from enum import StrEnum
class ContactEnum(StrEnum):
"""Enumeration for Contact."""
OFFER = "offer"
DEMAND = "demand"
+11
View File
@@ -0,0 +1,11 @@
"""Definition of DepthEnum DTO."""
from enum import StrEnum
class DepthEnum(StrEnum):
"""Enumeration for Depth values."""
HIGH = "high"
MEDIUM = "medium"
LOW = "low"
+11
View File
@@ -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"
+21
View File
@@ -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
+11
View File
@@ -0,0 +1,11 @@
"""Definition of LightingEnum DTO."""
from enum import StrEnum
class LightingEnum(StrEnum):
"""Enumeration for Lighting values."""
HIGH = "high"
MEDIUM = "medium"
LOW = "low"
+10
View File
@@ -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
)
+14
View File
@@ -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()
+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,130 @@
"""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,
AngleEnum,
ColourEnum,
ContactEnum,
DepthEnum,
DistanceEnum,
LightingEnum,
PointOfViewEnum,
)
class AnnotationRepository(MinioAdapter, AnnotationRepositoryInterface):
"""AnnotationRepository implementation using MinIO as the backend."""
encoding: str = "utf-8"
data_format: str = "json"
content_type: str = "application/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(
angle=AngleEnum(data["angle"]) if data["angle"] is not None else None,
angle_uncertainty=bool(data["angle_uncertainty"]),
colour=ColourEnum(data["colour"])
if data["colour"] is not None
else None,
colour_uncertainty=bool(data["colour_uncertainty"]),
contact=ContactEnum(data["contact"])
if data["contact"] is not None
else None,
contact_uncertainty=bool(data["contact_uncertainty"]),
depth=DepthEnum(data["depth"]) if data["depth"] is not None else None,
depth_uncertainty=bool(data["depth_uncertainty"]),
distance=DistanceEnum(data["distance"])
if data["distance"] is not None
else None,
distance_uncertainty=bool(data["distance_uncertainty"]),
lighting=LightingEnum(data["lighting"])
if data["lighting"] is not None
else None,
lighting_uncertainty=bool(data["lighting_uncertainty"]),
point_of_view=PointOfViewEnum(data["point_of_view"])
if data["point_of_view"] is not None
else None,
point_of_view_uncertainty=bool(data["point_of_view_uncertainty"]),
)
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, self.content_type)
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,83 @@
"""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"
content_type: str = "image/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, self.content_type)
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
+93
View File
@@ -0,0 +1,93 @@
{
"angle": {
"In the context of visual angle in the Kress and van Leeuwen framework, analyze the image to determine the angle of the subject.": 0.425531914893617,
"\"Analyze the image to determine the visual angle of the subject in the Kress and van Leeuwen framework.\"": 0.46808510638297873,
"Refined Prompt: \"In the context of visual angle analysis in the Kress and van Leeuwen framework, please provide the angle of the subject depicted in the image.\"": 0.44680851063829785,
"\"Analyze the image using the Kress and van Leeuwen framework to determine the visual angle of the subject depicted.\"": 0.5106382978723404,
"\"In the context of visual angle analysis in the Kress and van Leeuwen framework, please provide the angle of the subject depicted in the image.\"": 0.46808510638297873,
"\"Refine the prompt by removing unnecessary words and focusing on the task of providing the visual angle of the subject in the image using the Kress and van Leeuwen framework.\"": 0.46808510638297873,
"Refined Prompt: \"Provide the visual angle of the subject depicted in the image using the Kress and van Leeuwen framework.\"": 0.425531914893617,
"\"Provide the visual angle of the subject depicted in the image using the Kress and van Leeuwen framework.\"": 0.3404255319148936,
"\"In the context of visual angle analysis in the Kress and van Leeuwen framework, please provide the exact angle measurement of the subject depicted in the image.\"": 0.5106382978723404,
"\"Refine the prompt by removing unnecessary words and focusing on providing the exact angle measurement of the subject in the image using the Kress and van Leeuwen framework.\"": 0.5106382978723404,
"\"Provide the exact visual angle measurement of the subject depicted in the image using the Kress and van Leeuwen framework.\"": 0.425531914893617,
"Refine the prompt by removing unnecessary words and focusing on providing the exact angle measurement of the subject in the image using the Kress and van Leeuwen framework.": 0.5106382978723404
},
"colour": {
"In the context of colour in the Kress and van Leeuwen framework, analyze the image to determine the colour saturation of the whole image.": 0.3220338983050847,
"\"Analyze the image for colour saturation using the Kress and van Leeuwen framework, considering the entire image.\"": 0.3220338983050847,
"To ensure accuracy in the analysis of colour saturation using the Kress and van Leeuwen framework for the entire image, please provide a prompt that clearly and succinctly requests this information.": 0.3050847457627119,
"\"Using the Kress and van Leeuwen framework, please provide an analysis of the colour saturation for the entire image.\"": 0.3220338983050847,
"\"Using the Kress and van Leeuwen framework, please provide an analysis of the color saturation for the entire image.\"": 0.288135593220339,
"Using the Kress and van Leeuwen framework, please provide an analysis of the color saturation for the entire image.": 0.3898305084745763,
"To ensure accuracy in the analysis of color saturation using the Kress and van Leeuwen framework for the entire image, please provide a prompt that clearly and succinctly requests this information.": 0.2711864406779661
},
"contact": {
"In the context of contact in the Kress and van Leeuwen framework, analyze the image to determine the contact between subject and viewer.": 0.6363636363636364,
"\"Analyze the image to determine the contact between the subject and the viewer in the context of the Kress and van Leeuwen framework.\"": 0.3181818181818182,
"\"Evaluate the contact between the subject and viewer in the context of the Kress and van Leeuwen framework by analyzing this image.\"": 0.5454545454545454,
"\"Analyze the image using the Kress and van Leeuwen framework to determine the contact between the subject and viewer.\"": 0.4090909090909091,
"\"Using the Kress and van Leeuwen framework, analyze this image to determine the contact between the subject and viewer.\"": 0.6818181818181818,
"\"Analyze this image using the Kress and van Leeuwen framework to determine the contact between the subject and viewer.\"": 0.7272727272727273,
"\"Analyze the image using the Kress and van Leeuwen framework to determine the level of contact between the subject and viewer.\"": 0.5909090909090909,
"\"Analyze this image using the Kress and van Leeuwen framework to determine the level of contact between the subject and viewer.\"": 0.45454545454545453,
"\"Using the Kress and van Leeuwen framework, analyze this image to determine the level of contact between the subject and viewer.\"": 0.5454545454545454,
"\"Analyze this image using the Kress and van Leeuwen framework by evaluating the contact between the subject and viewer.\"": 0.4090909090909091,
"Refined prompt: \"Analyze this image using the Kress and van Leeuwen framework by evaluating the level of contact between the subject and viewer.\"": 0.5,
"\"Analyze this image using the Kress and van Leeuwen framework by evaluating the level of contact between the subject and viewer.\"": 0.5,
"\"Evaluate the level of contact between the subject and viewer in the context of the Kress and van Leeuwen framework by analyzing this image.\"": 0.5
},
"depth": {
"In the context of depth in the Kress and van Leeuwen framework, analyze the image to determine the depth of the whole image.": 0.3793103448275862,
"\"Analyze the image to determine the depth of the scene in terms of foreground, midground, and background elements using the Kress and van Leeuwen framework.\"": 0.4482758620689655,
"\"Analyze the image using the Kress and van Leeuwen framework to determine the depth of the foreground, midground, and background elements.\"": 0.4482758620689655,
"\"Analyze the image using the Kress and van Leeuwen framework to determine the depth of each element in the scene, including foreground, midground, and background elements.\"": 0.4482758620689655,
"\"Using the Kress and van Leeuwen framework, analyze the image to determine the depth of each element in the scene, including foreground, midground, and background elements.\"": 0.39655172413793105,
"\"Using the Kress and van Leeuwen framework, analyze the depth of each element in the image, including foreground, midground, and background elements.\"": 0.39655172413793105
},
"distance": {
"In the context of spatial distance in the Kress and van Leeuwen framework, analyze the image to determine the spatial distance that the subject is portrayed at.": 0.4,
"\"Analyze the image using the Kress and van Leeuwen framework to determine the spatial distance portrayed for the subject.\"": 0.37142857142857144,
"\"Using the Kress and van Leeuwen framework, determine the spatial distance portrayed for the subject in this image.\"": 0.4,
"The refined prompt is: \"Analyze the image using the Kress and van Leeuwen framework to determine the spatial distance portrayed for the subject.\"": 0.4857142857142857,
"The refined prompt is: \"Determine the spatial distance portrayed for the subject in this image using the Kress and van Leeuwen framework.\"": 0.4857142857142857,
"The refined prompt is: \"Using the Kress and van Leeuwen framework, analyze the image to determine the spatial distance portrayed for the subject.\"": 0.6,
"\"Using the Kress and van Leeuwen framework, analyze this image to determine the spatial distance portrayed for the subject.\"": 0.5428571428571428,
"The refined prompt is: \"Analyze this image using the Kress and van Leeuwen framework to determine the spatial distance portrayed for the subject.\"": 0.4857142857142857,
"\"Analyze this image using the Kress and van Leeuwen framework to determine the spatial distance portrayed for the subject.\"": 0.42857142857142855,
"\"Determine the spatial distance portrayed for the subject in this image using the Kress and van Leeuwen framework.\"": 0.4857142857142857
},
"lighting": {
"In the context of lighting in the Kress and van Leeuwen framework, analyze the image to determine the lighting of the whole image.": 0.3333333333333333,
"Refined Prompt: \"Analyze the image for its overall lighting using the Kress and van Leeuwen framework, and provide an accuracy estimate of the analysis.\"": 0.4074074074074074,
"\"Analyze the image for its overall lighting using the Kress and van Leeuwen framework, and provide an accuracy estimate of the analysis.\"": 0.35185185185185186,
"\"In the context of the Kress and van Leeuwen framework, analyze the image for its overall lighting and provide an accuracy estimate of the analysis.\"": 0.4074074074074074,
"\"Analyze the image for its overall lighting using the Kress and van Leeuwen framework and provide an accuracy estimate of the analysis.\"": 0.24074074074074073,
"\"Refined Prompt: \\\"Analyze the image for its overall lighting using the Kress and van Leeuwen framework, providing a detailed description of the lighting conditions and an accuracy estimate of the analysis.\\\"\"": 0.35185185185185186,
"\"Analyze the image for its overall lighting using the Kress and van Leeuwen framework, providing a detailed description of the lighting conditions and an accuracy estimate of the analysis.\"": 0.3148148148148148,
"\"Refine the prompt for analyzing the image's overall lighting using the Kress and van Leeuwen framework by providing a detailed description of the lighting conditions and an accuracy estimate of the analysis.\"": 0.3148148148148148,
"Refine the prompt for analyzing the image's overall lighting using the Kress and van Leeuwen framework by providing a detailed description of the lighting conditions and an accuracy estimate of the analysis.": 0.18518518518518517,
"\"Analyze the image for its overall lighting conditions using the Kress and van Leeuwen framework, providing a detailed description of the light sources, their intensities, and the resulting illumination, along with an accuracy estimate of the analysis.\"": 0.2962962962962963,
"Refine the prompt for analyzing the image's overall lighting using the Kress and van Leeuwen framework by providing a detailed description of the light sources, their intensities, and the resulting illumination, along with an accuracy estimate of the analysis.": 0.18518518518518517,
"\"Analyze the image for its overall lighting using the Kress and van Leeuwen framework by providing a detailed description of the light sources, their intensities, and the resulting illumination, along with an accuracy estimate of the analysis.\"": 0.2222222222222222,
"\"Refine the prompt for analyzing the image's overall lighting using the Kress and van Leeuwen framework by providing a detailed description of the light sources, their intensities, and the resulting illumination, along with an accuracy estimate of the analysis.\"": 0.3888888888888889,
"\"Analyze the image for its overall lighting conditions using the Kress and van Leeuwen framework by providing a detailed description of the light sources, their intensities, and the resulting illumination, along with an accuracy estimate of the analysis.\"": 0.2037037037037037,
"To improve the accuracy of analyzing the image's overall lighting using the Kress and van Leeuwen framework, provide a detailed description of the light sources, their intensities, and the resulting illumination in the image, along with an estimated accuracy for the analysis.": 0.16666666666666666,
"\"Analyze the image for its overall lighting conditions using the Kress and van Leeuwen framework by providing a detailed description of the light sources, their intensities, and the resulting illumination in the image, along with an estimated accuracy of the analysis.\"": 0.35185185185185186,
"\"Provide a detailed description of the light sources, their intensities, and the resulting illumination in the image using the Kress and van Leeuwen framework, along with an estimated accuracy for the analysis.\"": 0.2037037037037037,
"\"Analyze the image for its overall lighting conditions using the Kress and van Leeuwen framework by providing a detailed description of the light sources, their intensities, and the resulting illumination in the image, along with an estimated accuracy for the analysis.\"": 0.37037037037037035,
"Refine the prompt for analyzing the image's overall lighting using the Kress and van Leeuwen framework by providing a detailed description of the light sources, their intensities, and the resulting illumination in the image, along with an estimated accuracy for the analysis.": 0.25925925925925924
},
"point_of_view": {
"In the context of point of view in the Kress and van Leeuwen framework, analyze the image to determine the orientation of the subject in relation to the viewer.": 0.64,
"Refined Prompt: \"Analyze the image using the Kress and van Leeuwen framework to determine the orientation of the subject in relation to the viewer's point of view.\"": 0.72,
"\"Using the Kress and van Leeuwen framework, analyze the image to determine the subject's orientation in relation to the viewer's point of view.\"": 0.6,
"Refined Prompt: \"Using the Kress and van Leeuwen framework, analyze the image to determine the subject's orientation in relation to the viewer's point of view.\"": 0.72,
"\"Analyze the image using the Kress and van Leeuwen framework to determine the subject's orientation in relation to the viewer's point of view.\"": 0.64,
"\"In the context of point of view in the Kress and van Leeuwen framework, provide an analysis of the image to determine the orientation of the subject in relation to the viewer's perspective.\"": 0.52,
"\"Analyze the image using the Kress and van Leeuwen framework to determine the orientation of the subject in relation to the viewer's point of view.\"": 0.56,
"\"Using the Kress and van Leeuwen framework, provide an analysis of the image to determine the orientation of the subject in relation to the viewer's point of view.\"": 0.68,
"\"Using the Kress and van Leeuwen framework, provide an analysis of the image to determine the subject's orientation in relation to the viewer's point of view.\"": 0.76,
"\"Using the Kress and van Leeuwen framework, provide a detailed analysis of the image to determine the orientation of the subject in relation to the viewer's point of view.\"": 0.68
}
}
+64
View File
@@ -0,0 +1,64 @@
{
"angle": {
"In the context of visual angle in the Kress and van Leeuwen framework, analyze the image to determine the angle of the subject.": 0.3333333333333333,
"\"Analyze the image to determine the visual angle of the subject using the Kress and van Leeuwen framework, providing an accuracy of at least 0.4.\"": 0.16666666666666666,
"\"Analyze the image provided within the context of the Kress and van Leeuwen framework to accurately determine the visual angle of the subject, with an accuracy target of at least 0.4.\"": 0.3333333333333333,
"\"Analyze the image provided using the Kress and van Leeuwen framework to accurately determine the visual angle of the subject with an accuracy target of at least 0.4.\"": 0.0,
"\"Analyze the image provided within the context of the Kress and van Leeuwen framework to accurately determine the visual angle of the subject with an accuracy target of at least 0.4.\"": 0.5,
"\"Analyze the image provided using the Kress and van Leeuwen framework to accurately determine the visual angle of the subject with a minimum accuracy target of 0.4.\"": 0.16666666666666666,
"Refined Prompt: \"Analyze the provided image using the Kress and van Leeuwen framework to accurately determine the visual angle of the subject with a minimum accuracy target of 0.4.\"": 0.16666666666666666,
"\"Analyze the provided image using the Kress and van Leeuwen framework to accurately determine the visual angle of the subject with a minimum accuracy target of 0.4.\"": 0.16666666666666666,
"\"Analyze the provided image using the Kress and van Leeuwen framework to accurately determine the visual angle of the subject, targeting an accuracy of at least 0.5.\"": 0.0,
"\"Analyze the provided image using the Kress and van Leeuwen framework to accurately determine the visual angle of the subject with a minimum accuracy target of 0.5.\"": 0.0,
"\"Analyze the provided image using the Kress and van Leeuwen framework to accurately determine the visual angle of the subject, targeting an accuracy of at least 0.4.\"": 0.6666666666666666
},
"colour": {
"In the context of colour in the Kress and van Leeuwen framework, analyze the image to determine the colour saturation of the whole image.": 0.2857142857142857,
"\"Analyze the image for color saturation using the Kress and van Leeuwen framework.\"": 0.42857142857142855,
"\"Analyze the image for color saturation using the Kress and van Leeuwen framework by considering all hues present in the image.\"": 0.2857142857142857,
"\"Using the Kress and van Leeuwen framework for color analysis, determine the overall saturation level of all hues present in the image.\"": 0.2857142857142857,
"\"Analyze the image for color saturation using the Kress and van Leeuwen framework by considering all hues present in the image and provide a numerical value representing the overall saturation level.\"": 0.5714285714285714,
"\"Using the Kress and van Leeuwen framework for color analysis, provide a numerical value representing the overall saturation level of all hues present in the image.\"": 0.2857142857142857,
"\"Using the Kress and van Leeuwen framework for color analysis, determine the overall saturation level of all hues present in the image by considering their respective saturation values.\"": 0.14285714285714285,
"\"Using the Kress and van Leeuwen framework for color analysis, provide a numerical value representing the overall saturation level of all hues present in the image by considering their respective saturation values.\"": 0.42857142857142855,
"\"Analyze the image using the Kress and van Leeuwen framework for color analysis by considering all hues present in the image to determine the overall saturation level as a numerical value.\"": 0.42857142857142855,
"\"Using the Kress and van Leeuwen framework for color analysis, determine the overall saturation level of all hues present in the image by considering their respective saturation values and provide a numerical value representing the overall saturation level.\"": 0.8571428571428571
},
"contact": {
"In the context of contact in the Kress and van Leeuwen framework, analyze the image to determine the contact between subject and viewer.": 0.5,
"\"Analyze the image in terms of the Kress and van Leeuwen framework by considering the contact between the subject and viewer. Please provide a detailed explanation of the contact points and their significance.\"": 0.0,
"\"Analyze the image using the Kress and van Leeuwen framework, focusing on the contact between the subject and viewer. Provide a detailed explanation of the contact points and their significance.\"": 1.0
},
"depth": {
"In the context of depth in the Kress and van Leeuwen framework, analyze the image to determine the depth of the whole image.": 0.7142857142857143,
"\"Analyze the image in the context of depth according to the Kress and van Leeuwen framework and provide an accurate assessment of the overall depth of the image.\"": 0.7142857142857143,
"\"Analyze the image for depth using the Kress and van Leeuwen framework and provide a precise measurement of the overall depth.\"": 0.5714285714285714,
"\"Analyze the image using the Kress and van Leeuwen framework to provide an accurate measurement of its overall depth.\"": 0.8571428571428571,
"To improve the accuracy of depth measurement using Kress and van Leeuwen framework, please provide a precise analysis of the image's overall depth based on this framework.": 0.5714285714285714,
"\"Analyze the image using the Kress and van Leeuwen framework and provide a precise measurement of its overall depth.\"": 0.8571428571428571,
"\"Analyze the image for depth using the Kress and van Leeuwen framework and provide a precise measurement of its overall depth.\"": 0.5714285714285714,
"\"Analyze the image using the Kress and van Leeuwen framework to provide a precise measurement of its overall depth.\"": 0.2857142857142857,
"To ensure accurate assessment of depth in the image using the Kress and van Leeuwen framework, please analyze the image for depth and provide a precise measurement based on this framework.": 0.42857142857142855,
"To provide an accurate measurement of the overall depth of the image using the Kress and van Leeuwen framework, please analyze the image for depth and provide a precise assessment based on this framework.": 0.2857142857142857
},
"distance": {
"In the context of spatial distance in the Kress and van Leeuwen framework, analyze the image to determine the spatial distance that the subject is portrayed at.": 0.5714285714285714,
"\"Analyze the image using the Kress and van Leeuwen framework to determine the spatial distance at which the subject is portrayed.\"": 1.0
},
"lighting": {
"In the context of lighting in the Kress and van Leeuwen framework, analyze the image to determine the lighting of the whole image.": 0.14285714285714285,
"\"Analyze the image to determine the lighting conditions present in the scene using the Kress and van Leeuwen framework.\"": 0.42857142857142855,
"Refined Prompt: \"Analyze the image using the Kress and van Leeuwen framework to determine the lighting conditions present in the scene.\"": 0.2857142857142857,
"\"Analyze the image using the Kress and van Leeuwen framework to determine the lighting conditions present in the scene.\"": 0.8571428571428571,
"\"Analyze the image using the Kress and van Leeuwen framework to determine the type of lighting present in the scene.\"": 0.2857142857142857,
"Refined Prompt: \"Analyze the image using the Kress and van Leeuwen framework to determine the type of lighting present in the scene.\"": 0.2857142857142857
},
"point_of_view": {
"In the context of point of view in the Kress and van Leeuwen framework, analyze the image to determine the orientation of the subject in relation to the viewer.": 0.3333333333333333,
"\"Analyze the image to determine the orientation of the subject in relation to the viewer using the Kress and van Leeuwen framework.\"": 0.6666666666666666,
"Refined Prompt: \"Using the Kress and van Leeuwen framework, determine the orientation of the subject in relation to the viewer by analyzing this image.\"": 0.0,
"\"Analyze the image using the Kress and van Leeuwen framework to determine the orientation of the subject in relation to the viewer.\"": 0.3333333333333333,
"\"Using the Kress and van Leeuwen framework, determine the orientation of the subject in relation to the viewer by analyzing this image.\"": 1.0,
"\"Analyze this image using the Kress and van Leeuwen framework to determine the orientation of the subject in relation to the viewer.\"": 0.6666666666666666
}
}
+87
View File
@@ -0,0 +1,87 @@
[project]
name = "visual-semiotic-ai-analysis"
version = "0.1.0"
description = "Add your description here"
authors = [
{ name = "Brian Bjarke Jensen", email = "[email protected]" }
]
readme = "README.md"
requires-python = ">=3.12"
license = { text = "MIT" }
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
dependencies = [
"mlflow>=3.4.0",
"pandas>=2.3.2",
"pillow>=11.3.0",
"pydantic>=2.11.9",
"python-repositories[minio]>=0.2.0",
"python-utils>=0.1.1",
]
[build-system]
requires = ["uv_build"]
build-backend = "uv_build"
[tool.uv.build-backend]
module-name = [
"data_store",
]
module-root = "."
[tool.uv.sources]
python-repositories = { index = "gitea" }
python-utils = { index = "gitea" }
[[tool.uv.index]]
name = "threadripper-proxpi-cache"
url = "http://10.0.0.2:5001/index/"
default = true
[[tool.uv.index]]
name = "gitea"
url = "https://gitea.gt-proj.com/api/packages/brian/pypi/simple/"
explicit = true
[tool.mypy]
python_version = "3.12"
warn_return_any = true # nudge to use stricter types
warn_unused_configs = true # nudge to remove unused configs
disallow_untyped_defs = true # disallow untyped function definitions
disallow_incomplete_defs = true # ensure all parts of a function has type annotation
check_untyped_defs = true # dont skip untyped functions
disallow_untyped_decorators = false # allow untyped decorators to keep code more readable
no_implicit_optional = true # disallow implicit optional types leading to None-mess
warn_redundant_casts = true # nudge to use explicit type casts
warn_unused_ignores = true # nudge to remove unused ignores
warn_no_return = true # catch missing return statements
warn_unreachable = true # catch unreachable code
show_error_codes = true # show error codes in output
explicit_package_bases = true # reduce risk of import confusion
namespace_packages = true # enable namespace packages
[[tool.mypy.overrides]]
module = "testcontainers.*"
ignore_missing_imports = true
[[tool.mypy.overrides]]
module = "minio.*"
ignore_missing_imports = true
[dependency-groups]
dev = [
"mypy>=1.18.1",
"pandas-stubs>=2.3.2.250827",
"pre-commit>=4.3.0",
"pytest>=8.4.2",
"pytest-cov>=7.0.0",
"python-dotenv>=1.1.1",
"pyupgrade>=3.20.0",
"ruff>=0.13.0",
"safety>=3.2.11",
"testcontainers>=4.13.0",
"types-requests>=2.32.4.20250913",
]
File diff suppressed because it is too large Load Diff
+336
View File
@@ -0,0 +1,336 @@
"""Script to query an AI model with a prompt and image and print the response."""
import os
import base64
import logging
import json
from io import BytesIO
import requests
import structlog
from dotenv import load_dotenv
from PIL import Image
from datetime import datetime
import mlflow
from python_utils import check_env
from data_store.repositories import JobRepository
from data_store.dto import (
AngleEnum,
ColourEnum,
ContactEnum,
DepthEnum,
DistanceEnum,
LightingEnum,
PointOfViewEnum,
)
RUN_NAME = f"query-ai-model_{datetime.now().strftime('%Y%m%d-%H%M%S')}"
IMAGE_MODEL = "llava:13b"
PROMPT_MODEL = "mistral-small:22b-instruct-2409-q4_K_M"
CATEGORY_PROMPT_MAP = {
"angle": (
"In the context of visual angle in the Kress and van Leeuwen framework, "
"analyze the image to determine the angle of the subject."
),
"colour": (
"In the context of colour in the Kress and van Leeuwen framework, "
"analyze the image to determine the colour saturation of the whole image."
),
"contact": (
"In the context of contact in the Kress and van Leeuwen framework, "
"analyze the image to determine the contact between subject and viewer."
),
"depth": (
"In the context of depth in the Kress and van Leeuwen framework, "
"analyze the image to determine the depth of the whole image."
),
"distance": (
"In the context of spatial distance in the Kress and van Leeuwen framework, "
"analyze the image to determine the spatial distance that the subject is portrayed at."
),
"lighting": (
"In the context of lighting in the Kress and van Leeuwen framework, "
"analyze the image to determine the lighting of the whole image."
),
"point_of_view": (
"In the context of point of view in the Kress and van Leeuwen framework, "
"analyze the image to determine the orientation of the subject in relation to the viewer."
),
}
def query_model_with_image(
prompt: str,
allowed_answers: list[str],
image: Image.Image,
model: str = "llava:13b",
) -> str:
"""
Query an Ollama instance with a prompt, image, and constrained answers.
Args:
prompt: The text prompt to send
allowed_answers: List of valid response options
image: PIL Image to analyze
model: The Ollama model to use (default: "llava:13b")
Returns:
The model's response text
"""
# Ensure required env vars are set
check_env("OLLAMA_ENDPOINT")
# Read environment variables
ollama_endpoint = str(os.getenv("OLLAMA_ENDPOINT"))
# Handle RGBA images by converting to RGB
if image.mode == "RGBA":
# Convert RGBA to RGB by compositing against white background
rgb_image = Image.new("RGB", image.size, (255, 255, 255))
rgb_image.paste(image, mask=image.split()[-1]) # Use alpha as mask
image = rgb_image
# Convert PIL Image to base64
buffer = BytesIO()
image.save(buffer, format="JPEG")
image_b64 = base64.b64encode(buffer.getvalue()).decode()
# Build prompt with allowed answers
prompt_with_options = (
f"{prompt}\n"
f"Please choose one of the following options: {', '.join(allowed_answers)}.\n"
"Respond with only the chosen option."
)
# Prepare request payload
payload = {
"model": model,
"prompt": prompt_with_options,
"images": [image_b64],
"stream": False,
}
# Make request to Ollama
# N.B. the generate endpoint does not retain context between calls
response = requests.post(
f"{ollama_endpoint}/api/generate", json=payload, timeout=60
)
response.raise_for_status()
result = response.json()
return str(result.get("response", "").strip())
def score_response(response: str, expected_answer: str) -> int:
"""
Score the model's response based on allowed answers.
Args:
response: The model's response text
expected_answer: The expected answer
Returns:
1 if the response is in allowed answers, else 0
"""
# Score response
return 1 if response.lower() == expected_answer.lower() else 0
def allowed_answers_for_category(
category: str,
) -> list[str]:
"""
Get allowed answers based on the category.
Args:
category: The category to determine allowed answers
Returns:
List of allowed answers
"""
match category.lower():
case "angle":
return [e.value for e in AngleEnum]
case "colour":
return [e.value for e in ColourEnum]
case "contact":
return [e.value for e in ContactEnum]
case "depth":
return [e.value for e in DepthEnum]
case "distance":
return [e.value for e in DistanceEnum]
case "lighting":
return [e.value for e in LightingEnum]
case "point_of_view":
return [e.value for e in PointOfViewEnum]
case _:
raise ValueError(f"Invalid category: {category}")
def calculate_model_prompt_accuracy(
model: str,
prompt: str,
category: str,
) -> float:
"""
Calculate the accuracy of the prompt against the expected answers for the given category.
"""
# Determine allowed answers based on category
allowed_answers = allowed_answers_for_category(category)
# Score each job in the repository)
jobs_skipped = 0
total_score = 0
with JobRepository() as job_repo:
job_ids = job_repo.list_all()
if not job_ids:
raise ValueError("No jobs found in JobRepository.")
total_jobs = len(job_ids)
# Get all responses for the prompt
for job_id in job_ids:
job = job_repo.get(job_id)
if not job:
jobs_skipped += 1
continue
# skip job if uncertainty flag is set for the category
if getattr(job.annotation, f"{category.lower()}_uncertainty"):
print(f"Job {job_id}: Skipping due to uncertainty flag.")
jobs_skipped += 1
continue
response = query_model_with_image(
model=model,
prompt=prompt,
allowed_answers=allowed_answers,
image=job.image,
)
# Get expected answer from job annotation
expected_answer = getattr(job.annotation, category.lower())
# Score the response
score = score_response(response, expected_answer)
print(
f"Job {job_id}: Response '{response}' - Expected answer '{expected_answer}' - Score: {score}"
)
total_score += score
# Calculate accuracy
jobs_evaluated = total_jobs - jobs_skipped
mlflow.log_metrics(
{
"total_jobs": total_jobs,
"jobs_skipped": jobs_skipped,
"jobs_evaluated": jobs_evaluated,
}
)
print(
f"Total Jobs: {total_jobs}, Jobs Skipped: {jobs_skipped}, Jobs Evaluated: {jobs_evaluated}"
)
if jobs_evaluated == 0:
raise ValueError("No jobs were evaluated. All jobs were skipped.")
accuracy = total_score / jobs_evaluated
return accuracy
def refine_prompt(
model: str,
prompt_accuracy_map: dict[str, float],
) -> str:
"""
Refine the prompt to improve accuracy.
Args:
model: The Ollama model to use
prompt_accuracy_map: A mapping of prompts to their accuracies
Returns:
The refined prompt text
"""
# Ensure required env vars are set
check_env("OLLAMA_ENDPOINT")
# Read environment variables
ollama_endpoint = str(os.getenv("OLLAMA_ENDPOINT"))
# Convert prompt accuracy map to json string for better readability
prompt_accuracy_json = json.dumps(prompt_accuracy_map, indent=2)
# Build refinement instruction
refinement_instruction = (
"Given the following prompts and accuracies:\n"
f"{prompt_accuracy_json}\n"
"Please refine the prompt to improve the accuracy.\n"
"The refined prompt should be clear, concise, and focused on obtaining accurate responses.\n"
"Respond with only the refined prompt."
)
# Prepare request payload
payload = {"model": model, "prompt": refinement_instruction, "stream": False}
# Make request to Ollama
# N.B. the generate endpoint does not retain context between calls
response = requests.post(
f"{ollama_endpoint}/api/generate", json=payload, timeout=60
)
response.raise_for_status()
result = response.json()
return str(result.get("response", "").strip())
if __name__ == "__main__":
load_dotenv()
# Ensure required env vars are set
check_env("OLLAMA_ENDPOINT")
# Configure structlog
structlog.configure(
processors=[
structlog.stdlib.filter_by_level,
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.JSONRenderer(),
],
logger_factory=structlog.stdlib.LoggerFactory(),
wrapper_class=structlog.stdlib.BoundLogger,
cache_logger_on_first_use=True,
)
# Set up basic logging configuration
logging.basicConfig(level=logging.ERROR)
# Connect to MLFlow
mlflow.set_tracking_uri("http://10.0.0.2:5000")
experiment = mlflow.get_experiment_by_name("visual-semiotic-ai-analysis")
experiment_id = experiment.experiment_id if experiment else None
# Calculate accuracy
for category, initial_prompt in CATEGORY_PROMPT_MAP.items():
step = 0
run = mlflow.start_run(
experiment_id=experiment_id,
run_name=RUN_NAME,
tags={
"image_model": IMAGE_MODEL,
"prompt_model": PROMPT_MODEL,
"category": category,
},
)
mlflow.log_param(f"step_{step}_prompt", initial_prompt)
print(f"Calculating accuracy for category: {category}")
accuracy = calculate_model_prompt_accuracy(IMAGE_MODEL, initial_prompt, category)
mlflow.log_metric("accuracy", accuracy, step=step)
# Log to MLFlow
prompt_accuracy_map = {initial_prompt: accuracy}
print(f"Prompt accuracy for category '{category}': {accuracy:.2%}")
# Loop to refine prompt based on accuracy
for i in range(99):
step += 1
try:
# Generate refined prompt
refined_prompt = refine_prompt(PROMPT_MODEL, prompt_accuracy_map)
mlflow.log_param(f"step_{step}_prompt", refined_prompt)
print(f"Refined prompt: {refined_prompt}")
except Exception as e:
print(f"Error refining prompt: {e}")
continue
try:
# Calculate accuracy for refined prompt
accuracy = calculate_model_prompt_accuracy(
IMAGE_MODEL, refined_prompt, category
)
mlflow.log_metric("accuracy", accuracy, step=step)
except Exception as e:
print(f"Error calculating accuracy: {e}")
continue
# Update prompt accuracy map
# prompt_accuracy_map[refined_prompt] = accuracy
print(f"Prompt accuracy for category '{category}': {accuracy:.2%}")
# Stop if accuracy is 100%
if accuracy == 1.0:
print(
f"Achieved 100% accuracy for category '{category}'. Stopping refinement."
)
break
mlflow.end_run()
+129
View File
@@ -0,0 +1,129 @@
"""Script to convert Label Studio output JSON files to an Annotation."""
import json
from uuid import UUID
from dotenv import load_dotenv
from python_repositories.adapters import MinioAdapter
from data_store.dto import Annotation
from data_store.repositories import AnnotationRepository
def list_label_studio_output_files(minio_adapter: MinioAdapter) -> list[str]:
"""List the Label Studio output files in MinIO."""
# Check input
if not isinstance(minio_adapter, MinioAdapter):
raise ValueError("minio_adapter must be an instance of MinioAdapter")
if not minio_adapter.is_connected:
raise ConnectionError("minio_adapter must be connected to MinIO")
# List objects with the Label Studio output prefix
prefix = "label-studio-output/"
return minio_adapter._list_objects(prefix)
def get_label_studio_output(minio_adapter: MinioAdapter, object_name: str) -> dict:
"""Get a Label Studio output file from MinIO."""
# Check input
if not isinstance(minio_adapter, MinioAdapter):
raise ValueError("minio_adapter must be an instance of MinioAdapter")
if not minio_adapter.is_connected:
raise ConnectionError("minio_adapter must be connected to MinIO")
if not isinstance(object_name, str) or len(object_name) == 0:
raise ValueError("object_name must be a non-empty string")
# Get object from MinIO
buffer = minio_adapter._get(object_name)
if buffer is None:
raise FileNotFoundError(f"File not found in MinIO: {object_name}")
# Convert buffer to JSON
data: dict = json.load(buffer)
return data
def convert_label_studio_output(label_studio_output: dict) -> tuple[Annotation, UUID]:
"""Convert Label Studio output to Annotation and corresponding job ID."""
# Check input
if not isinstance(label_studio_output, dict):
raise ValueError("label_studio_output must be a dictionary")
# Extract job id
image_path = label_studio_output["task"]["data"]["image"]
job_id_str = image_path.rsplit("/")[-1].split(".")[0]
job_id = UUID(job_id_str)
# Extract relevant information from the Label Studio output
annotation_data: dict[str, str | None | bool] = {}
for elem in label_studio_output["result"]:
# Extract key and value
key = elem["from_name"]
choices = elem["value"]["choices"]
# Check for presence of uncertainty flag
uncertainty_key = f"{key}_uncertainty"
uncertainty_value = bool("uncertain" in choices)
if uncertainty_value:
choices.remove("uncertain")
# If no choices left, set to None
value: str | None = choices[0] if choices else None
# Store in annotation data
annotation_data[key] = value
annotation_data[uncertainty_key] = uncertainty_value
# Create the Annotation object
annotation = Annotation(**annotation_data) # type: ignore[arg-type]
return annotation, job_id
def sync_label_studio_annotations(
minio_adapter: MinioAdapter,
annotation_repository: AnnotationRepository,
) -> None:
"""Sync Label Studio output files to AnnotationRepository."""
# Check inputs
if not isinstance(minio_adapter, MinioAdapter):
raise ValueError("minio_adapter must be an instance of MinioAdapter")
if not minio_adapter.is_connected:
raise ConnectionError("minio_adapter must be connected to MinIO")
if not isinstance(annotation_repository, AnnotationRepository):
raise ValueError(
"annotation_repository must be an instance of AnnotationRepository"
)
if not annotation_repository.is_connected:
raise ConnectionError("annotation_repository must be connected to MinIO")
# List annotations in repository
annotation_ids = annotation_repository.list_all()
print(f"Found {len(annotation_ids)} annotations in repository.")
# Count Label Studio output files
label_studio_files = list_label_studio_output_files(minio_adapter)
print(f"Found {len(label_studio_files)} Label Studio output files.")
# Stop early if equal number of annotations and Label Studio files
if len(annotation_ids) == len(label_studio_files):
print("No new Label Studio output files to process. Exiting.")
return
# Process each Label Studio output file
for file_name in label_studio_files:
print(f"Processing file: {file_name}")
# Get Label Studio output
try:
label_studio_output = get_label_studio_output(
minio_adapter,
file_name,
)
except Exception as exc:
print(f"Failed to get Label Studio output for {file_name}: {exc}")
continue
# Convert to Annotation
try:
annotation, job_id = convert_label_studio_output(label_studio_output)
except Exception as exc:
print(f"Failed to convert Label Studio output for {file_name}: {exc}")
continue
# Store Annotation in repository
try:
annotation_repository.put(annotation, job_id)
except Exception as exc:
print(f"Failed to store annotation for {file_name}: {exc}")
continue
print(f"Successfully processed and stored annotation for {file_name}.")
if __name__ == "__main__":
# Load environment variables from .env file
load_dotenv()
# Connect to data sources
with MinioAdapter() as minio_adapter, AnnotationRepository() as annotation_repo:
sync_label_studio_annotations(minio_adapter, annotation_repo)
+67
View File
@@ -0,0 +1,67 @@
"""Script to upload images to MinIO server."""
from pathlib import Path
from uuid import uuid4
import pandas as pd
from PIL import Image
from dotenv import load_dotenv
from data_store.repositories import ImageRepository
UPLOAD_DIR = Path("/Volumes/BW-PSSD/maersk_data/images")
def upload_images_to_minio(
image_repo: ImageRepository,
upload_dir: Path,
) -> None:
"""Upload images from the specified directory to MinIO server."""
# Check input
if not isinstance(image_repo, ImageRepository):
raise ValueError("image_repo must be an instance of ImageRepository.")
if not image_repo.is_connected:
raise ConnectionError("image_repo must be connected to MinIO server.")
if not isinstance(upload_dir, Path):
raise ValueError("upload_dir must be a pathlib.Path instance.")
if not upload_dir.is_dir():
raise NotADirectoryError(f"upload_dir must be a valid directory: {upload_dir}")
if not upload_dir.exists():
raise FileNotFoundError(f"upload_dir does not exist: {upload_dir}")
# Scan directory for images
images = list(upload_dir.glob("*.png"))
if not images:
raise FileNotFoundError(f"No PNG images found in directory: {upload_dir}")
print(f"Found {len(images)} images to upload.")
# Initialize list to store image name and id mapping
image_name_id_map: dict[str, str] = {}
# Begin uploading images
for image_path in images:
try:
# Load image
img = Image.open(image_path)
# Upload image to MinIO
img_id = uuid4()
image_repo.put(img, img_id)
print(f"Uploaded {image_path.name} with ID {img_id}.")
except Exception as e:
print(f"Failed to upload {image_path.name}: {e}")
continue
else:
# Store mapping
image_name_id_map[image_path.name] = str(img_id)
# Save mapping to CSV
df = pd.DataFrame(
list(image_name_id_map.items()), columns=["image_name", "image_id"]
)
csv_path = Path(__file__).parent / "image_name_id_map.csv"
df.to_csv(csv_path, index=False)
if __name__ == "__main__":
# Load environment variables from .env file
load_dotenv()
# Instantiate ImageRepository
with ImageRepository() as image_repo:
# Upload images from the specified directory
upload_images_to_minio(image_repo, UPLOAD_DIR)
print("Image upload process completed.")
@@ -0,0 +1,110 @@
"""Integration tests for AnnotationRepositoryInterface."""
from uuid import UUID
import pytest
from data_store.interfaces import AnnotationRepositoryInterface
from data_store.dto import Annotation
def test_get_raises_not_implemented_if_not_overwritten(
job_id_list: list[UUID],
) -> None:
"""Test that calling get on a subclass without get implemented raises NotImplementedError."""
class Incomplete(AnnotationRepositoryInterface):
"""A class missing the get method."""
def get(self, job_id: UUID) -> Annotation | None:
return super().get(job_id) # type: ignore
def put(self, annotation: Annotation, job_id: UUID) -> None:
return None
def delete(self, job_id: UUID) -> None:
return None
def list_all(self) -> list[UUID]:
return []
instance = Incomplete()
with pytest.raises(NotImplementedError):
instance.get(job_id_list[0])
def test_put_raises_not_implemented_if_not_overwritten(
annotation: Annotation,
job_id_list: list[UUID],
) -> None:
"""Test that calling put on a subclass without put implemented raises NotImplementedError."""
class Incomplete(AnnotationRepositoryInterface):
"""A class missing the put method."""
def get(self, job_id: UUID) -> Annotation | None:
return None
def put(self, annotation: Annotation, job_id: UUID) -> None:
return super().put(annotation, job_id) # type: ignore
def delete(self, job_id: UUID) -> None:
return None
def list_all(self) -> list[UUID]:
return []
instance = Incomplete()
with pytest.raises(NotImplementedError):
instance.put(annotation, job_id_list[0])
def test_delete_raises_not_implemented_if_not_overwritten(
job_id_list: list[UUID],
) -> None:
"""Test that calling delete on a subclass without delete implemented raises NotImplementedError."""
class Incomplete(AnnotationRepositoryInterface):
"""A class missing the delete method."""
def get(self, job_id: UUID) -> Annotation | None:
return None
def put(self, annotation: Annotation, job_id: UUID) -> None:
return None
def delete(self, job_id: UUID) -> None:
return super().delete(job_id) # type: ignore
def list_all(self) -> list[UUID]:
return []
instance = Incomplete()
with pytest.raises(NotImplementedError):
instance.delete(job_id_list[0])
def test_list_all_raises_not_implemented_if_not_overwritten() -> None:
"""Test that calling list_all on a subclass without list_all implemented raises NotImplementedError."""
class Incomplete(AnnotationRepositoryInterface):
"""A class missing the list_all method."""
def get(self, job_id: UUID) -> Annotation | None:
return None
def put(self, annotation: Annotation, job_id: UUID) -> None:
return None
def delete(self, job_id: UUID) -> None:
return None
def list_all(self) -> list[UUID]:
return super().list_all() # type: ignore
instance = Incomplete()
with pytest.raises(NotImplementedError):
instance.list_all()
# Allows local debugging by running file as script
if __name__ == "__main__":
pytest.main(["-s", "-v", __file__])
@@ -0,0 +1,86 @@
"""Integration tests for AnnotationRepository."""
from uuid import UUID
import pytest
from data_store.repositories.annotation_repository import AnnotationRepository
from data_store.dto import (
Annotation,
)
def test_get_annotation(
annotation_in_minio: tuple[UUID, Annotation],
annotation_repository: AnnotationRepository,
) -> None:
"""Test retrieving an annotation from AnnotationRepository."""
# Arrange
job_id, expected_annotation = annotation_in_minio
# Act
retrieved_annotation = annotation_repository.get(job_id)
# Assert
assert retrieved_annotation is not None
assert isinstance(retrieved_annotation, Annotation)
assert retrieved_annotation == expected_annotation
def test_put_annotation(
annotation_repository: AnnotationRepository,
annotation: Annotation,
job_id_list: list[UUID],
) -> None:
"""Test storing an annotation in AnnotationRepository."""
# Arrange
job_id = job_id_list[0]
assert annotation_repository.get(job_id) is None
# Act
annotation_repository.put(annotation, job_id)
# Assert
received_annotation = annotation_repository.get(job_id)
assert received_annotation is not None
assert isinstance(received_annotation, Annotation)
assert received_annotation == annotation
def test_delete_annotation(
annotation_in_minio: tuple[UUID, Annotation],
annotation_repository: AnnotationRepository,
) -> None:
"""Test deleting an annotation from AnnotationRepository."""
# Arrange
job_id, _ = annotation_in_minio
assert annotation_repository.get(job_id) is not None
# Act
annotation_repository.delete(job_id)
# Assert
assert annotation_repository.get(job_id) is None
def test_list_all_annotations(
annotation_in_minio: tuple[UUID, Annotation],
annotation_repository: AnnotationRepository,
) -> None:
"""Test listing all annotations in AnnotationRepository."""
# Arrange
job_id, _ = annotation_in_minio
# Act
annotation_ids = annotation_repository.list_all()
# Assert
assert isinstance(annotation_ids, list)
assert all(isinstance(i, UUID) for i in annotation_ids)
assert job_id in annotation_ids
def test_list_all_annotations_empty(
annotation_repository: AnnotationRepository,
) -> None:
"""Test listing all annotations in an empty AnnotationRepository."""
# Act
annotation_ids = annotation_repository.list_all()
# Assert
assert isinstance(annotation_ids, list)
assert len(annotation_ids) == 0
# Allows local debugging by running file as script
if __name__ == "__main__":
pytest.main(["-s", "-v", __file__])
+346
View File
@@ -0,0 +1,346 @@
"""Integration test configuration."""
import logging
import os
import json
from collections.abc import Generator
from uuid import UUID
from io import BytesIO
from PIL import Image
import pytest
import structlog
from minio import Minio, S3Error
from testcontainers.minio import MinioContainer
from python_repositories.adapters import MinioAdapter
from data_store.repositories import (
ImageRepository,
AnnotationRepository,
JobRepository,
)
from data_store.dto import (
Annotation,
AngleEnum,
ColourEnum,
ContactEnum,
DepthEnum,
DistanceEnum,
LightingEnum,
PointOfViewEnum,
Job,
)
# Last Docker image with a useful WebUI
MINIO_DOCKER_IMAGE = "minio/minio:RELEASE.2023-03-20T20-16-18Z"
@pytest.fixture(scope="session", autouse=True)
def configure_logging() -> None:
"""Configure logging for the test session."""
# Configure structlog
structlog.configure(
processors=[
structlog.stdlib.filter_by_level,
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.JSONRenderer(),
],
logger_factory=structlog.stdlib.LoggerFactory(),
wrapper_class=structlog.stdlib.BoundLogger,
cache_logger_on_first_use=True,
)
# Set up basic logging configuration
logging.basicConfig(level=logging.ERROR)
@pytest.fixture(scope="session")
def minio_container() -> Generator[dict[str, str]]:
"""Set up MinIO container and provide credentials for testing."""
# Set variables
access_key = "minioadmin"
secret_key = "minioadmin"
bucket_name = "test-bucket"
# Start container
container = MinioContainer(
image=MINIO_DOCKER_IMAGE,
access_key=access_key,
secret_key=secret_key,
)
container.start()
# Build environment variables dictionary
minio_host = container.get_container_host_ip()
minio_port = container.get_exposed_port(9000)
minio_endpoint = f"{minio_host}:{minio_port}"
env_vars = {
"MINIO_ENDPOINT": minio_endpoint,
"MINIO_ACCESS_KEY": access_key,
"MINIO_SECRET_KEY": secret_key,
"MINIO_BUCKET": bucket_name,
}
yield env_vars
# Stop container
container.stop()
@pytest.fixture(scope="session", autouse=True)
def set_environment_variables(
minio_container: dict[str, str],
) -> Generator[dict[str, str]]:
"""Set environment variables needed for tests."""
# Build environment variables dictionary
env_vars = {}
env_vars.update(minio_container)
# Set environment variables
for key, value in env_vars.items():
os.environ[key] = value
yield env_vars
# Cleanup
for key in env_vars:
_ = os.environ.pop(key, default=None)
@pytest.fixture(scope="session")
def job_id_list() -> Generator[list[UUID]]:
"""Provide a list of job IDs for testing."""
yield [
UUID("d358cbba-75be-4c38-becd-7abf338ca941"),
UUID("56d20d25-b733-4ed3-880c-732d7257dcaa"),
UUID("8019292f-8353-43d7-a189-076c6630de7b"),
]
# @pytest.fixture(scope="session")
# def csv_data_file(
# job_id_list: list[UUID],
# ) -> Generator[Path]:
# """Create a CSV data file for testing."""
# # Create csv content
# csv_data = pd.DataFrame(
# {
# "job_id": job_id_list,
# "distance": [
# "close",
# "long",
# "wrong",
# ],
# "angle": ["high", "low", "wrong"],
# "point_of_view": ["frontal", "oblique", "wrong"],
# "contact": ["offer", "demand", "wrong"],
# }
# )
# # Create data directory and file path
# data_dir = Path(tempfile.mkdtemp())
# data_path = data_dir / "annotations.csv"
# # Write CSV content to file
# csv_data.to_csv(data_path, header=True, index=False)
# yield data_path
# # Cleanup
# shutil.rmtree(data_dir)
@pytest.fixture(scope="session")
def raw_minio_client(
set_environment_variables: dict[str, str],
) -> Generator[Minio]:
"""Provide a MinIO client for testing."""
# Create MinIO client
client = Minio(
endpoint=set_environment_variables["MINIO_ENDPOINT"],
access_key=set_environment_variables["MINIO_ACCESS_KEY"],
secret_key=set_environment_variables["MINIO_SECRET_KEY"],
secure=False,
)
# Create bucket if it doesn't exist
bucket_name = set_environment_variables["MINIO_BUCKET"]
try:
if not client.bucket_exists(bucket_name):
client.make_bucket(bucket_name)
except S3Error as e:
raise RuntimeError(f"Failed to create or access bucket: {e}")
yield client
# No explicit cleanup needed for MinIO client
@pytest.fixture(scope="session")
def image_repository() -> Generator[ImageRepository]:
"""Provide an ImageRepository instance for testing."""
# Instantiate and connect repository
repo = ImageRepository()
repo.connect()
yield repo
# Cleanup
repo.disconnect()
@pytest.fixture(scope="session")
def image() -> Generator[Image.Image]:
"""Provide a sample image for testing."""
img = Image.new("RGB", (10, 10))
yield img
@pytest.fixture(scope="function")
def image_in_minio(
set_environment_variables: dict[str, str],
raw_minio_client: Minio,
image: Image.Image,
job_id_list: list[UUID],
) -> Generator[tuple[UUID, Image.Image]]:
"""Upload a sample image to MinIO and provide its ID for testing."""
# Prepare object name and buffer
bucket_name = set_environment_variables["MINIO_BUCKET"]
image_id = job_id_list[0]
object_name = ImageRepository._object_name(image_id)
buffer = BytesIO()
image.save(buffer, ImageRepository.image_format)
num_bytes = buffer.getbuffer().nbytes
buffer.seek(0)
# Upload image to MinIO
raw_minio_client.put_object(
bucket_name=bucket_name,
object_name=object_name,
data=buffer,
length=num_bytes,
part_size=MinioAdapter.chunk_size,
)
buffer.close()
yield image_id, image
# Cleanup: remove image from MinIO
try:
raw_minio_client.remove_object(
bucket_name=bucket_name,
object_name=object_name,
)
except S3Error as e:
raise RuntimeError(f"Failed to remove object from bucket: {e}")
@pytest.fixture(scope="session")
def annotation_repository() -> Generator[AnnotationRepository]:
"""Provide an AnnotationRepository instance for testing."""
# Instantiate and connect repository
repo = AnnotationRepository()
repo.connect()
yield repo
# Cleanup
repo.disconnect()
@pytest.fixture(scope="session")
def annotation() -> Generator[Annotation]:
"""Provide a sample annotation for testing."""
annotation = Annotation(
angle=AngleEnum.HIGH,
angle_uncertainty=True,
colour=ColourEnum.HIGH,
colour_uncertainty=False,
contact=ContactEnum.OFFER,
contact_uncertainty=True,
depth=DepthEnum.LOW,
depth_uncertainty=False,
distance=DistanceEnum.CLOSE,
distance_uncertainty=True,
lighting=LightingEnum.MEDIUM,
lighting_uncertainty=False,
point_of_view=PointOfViewEnum.FRONTAL,
point_of_view_uncertainty=True,
)
yield annotation
@pytest.fixture(scope="function")
def annotation_in_minio(
set_environment_variables: dict[str, str],
raw_minio_client: Minio,
annotation: Annotation,
job_id_list: list[UUID],
) -> Generator[tuple[UUID, Annotation]]:
"""Upload a sample annotation to MinIO and provide its ID for testing."""
# Prepare object name and buffer
bucket_name = set_environment_variables["MINIO_BUCKET"]
job_id = job_id_list[0]
object_name = AnnotationRepository._object_name(job_id)
data_json = annotation.model_dump(mode="json")
data_bytes = json.dumps(data_json).encode(AnnotationRepository.encoding)
buffer = BytesIO(data_bytes)
num_bytes = buffer.getbuffer().nbytes
buffer.seek(0)
# Upload annotation to MinIO
raw_minio_client.put_object(
bucket_name=bucket_name,
object_name=object_name,
data=buffer,
length=num_bytes,
part_size=MinioAdapter.chunk_size,
)
buffer.close()
yield job_id, annotation
# Cleanup: remove annotation from MinIO
try:
raw_minio_client.remove_object(
bucket_name=bucket_name,
object_name=object_name,
)
except S3Error as e:
raise RuntimeError(f"Failed to remove object from bucket: {e}")
@pytest.fixture(scope="session")
def job_repository() -> Generator[JobRepository]:
"""Provide a JobRepository instance for testing."""
# Instantiate and connect repository
repo = JobRepository()
repo.connect()
yield repo
# Cleanup
repo.disconnect()
@pytest.fixture(scope="session")
def job(
job_id_list: list[UUID],
image: Image.Image,
annotation: Annotation,
) -> Generator[Job]:
"""Provide a sample job for testing."""
job = Job(
job_id=job_id_list[0],
image=image,
annotation=annotation,
)
yield job
@pytest.fixture(scope="function")
def job_in_minio(
job: Job,
image_in_minio: tuple[UUID, Image.Image],
annotation_in_minio: tuple[UUID, Annotation],
) -> Generator[Job]:
"""Provide a sample job with its components already in MinIO for testing."""
# A job consists of an image and an annotation and both must be in MinIO
# before the job can be considered to be in MinIO.
# Ensure the job_id matches those of the image and annotation
image_id, _ = image_in_minio
annotation_id, _ = annotation_in_minio
assert job.job_id == annotation_id
assert job.job_id == image_id
yield job
@@ -0,0 +1,110 @@
"""Integration tests for ImageRepositoryInterface."""
from uuid import UUID
import pytest
from PIL import Image
from data_store.interfaces import ImageRepositoryInterface
def test_get_raises_not_implemented_if_not_overwritten(
job_id_list: list[UUID],
) -> None:
"""Test that calling get on a subclass without get implemented raises NotImplementedError."""
class Incomplete(ImageRepositoryInterface):
"""A class missing the get method."""
def get(self, image_id: UUID) -> Image.Image | None:
return super().get(image_id) # type: ignore
def put(self, image: Image.Image, image_id: UUID) -> None:
return None
def delete(self, image_id: UUID) -> None:
return None
def list_all(self) -> list[UUID]:
return []
instance = Incomplete()
with pytest.raises(NotImplementedError):
instance.get(job_id_list[0])
def test_put_raises_not_implemented_if_not_overwritten(
image: Image.Image,
job_id_list: list[UUID],
) -> None:
"""Test that calling put on a subclass without put implemented raises NotImplementedError."""
class Incomplete(ImageRepositoryInterface):
"""A class missing the put method."""
def get(self, image_id: UUID) -> Image.Image | None:
return None
def put(self, image: Image.Image, image_id: UUID) -> None:
return super().put(image, image_id) # type: ignore
def delete(self, image_id: UUID) -> None:
return None
def list_all(self) -> list[UUID]:
return []
instance = Incomplete()
with pytest.raises(NotImplementedError):
instance.put(image, job_id_list[0])
def test_delete_raises_not_implemented_if_not_overwritten(
job_id_list: list[UUID],
) -> None:
"""Test that calling delete on a subclass without delete implemented raises NotImplementedError."""
class Incomplete(ImageRepositoryInterface):
"""A class missing the delete method."""
def get(self, image_id: UUID) -> Image.Image | None:
return None
def put(self, image: Image.Image, image_id: UUID) -> None:
return None
def delete(self, image_id: UUID) -> None:
return super().delete(image_id) # type: ignore
def list_all(self) -> list[UUID]:
return []
instance = Incomplete()
with pytest.raises(NotImplementedError):
instance.delete(job_id_list[0])
def test_list_all_raises_not_implemented_if_not_overwritten() -> None:
"""Test that calling list_all on a subclass without list_all implemented raises NotImplementedError."""
class Incomplete(ImageRepositoryInterface):
"""A class missing the list_all method."""
def get(self, image_id: UUID) -> Image.Image | None:
return None
def put(self, image: Image.Image, image_id: UUID) -> None:
return None
def delete(self, image_id: UUID) -> None:
return None
def list_all(self) -> list[UUID]:
return super().list_all() # type: ignore
instance = Incomplete()
with pytest.raises(NotImplementedError):
instance.list_all()
# Allows local debugging by running file as script
if __name__ == "__main__":
pytest.main(["-s", "-v", __file__])
@@ -0,0 +1,95 @@
"""Integration tests for ImageRepository."""
from uuid import UUID
import pytest
from PIL import Image
from data_store.repositories.image_repository import ImageRepository
def same_image(img1: Image.Image, img2: Image.Image) -> bool:
"""Check if two images are the same based on their attributes."""
return (
img1.size == img2.size
and img1.mode == img2.mode
and list(img1.getdata()) == list(img2.getdata())
)
def test_get_image(
image_in_minio: tuple[UUID, Image.Image],
image_repository: ImageRepository,
) -> None:
"""Test retrieving an image from ImageRepository."""
# Arrange
image_id, expected_image = image_in_minio
# Act
retrieved_image = image_repository.get(image_id)
# Assert
assert retrieved_image is not None
assert isinstance(retrieved_image, Image.Image)
assert same_image(retrieved_image, expected_image)
def test_put_image(
image_repository: ImageRepository,
image: Image.Image,
job_id_list: list[UUID],
) -> None:
"""Test storing an image in ImageRepository."""
# Arrange
image_id = job_id_list[0]
assert image_repository.get(image_id) is None
# Act
image_repository.put(image, image_id)
# Assert
received_image = image_repository.get(image_id)
assert received_image is not None
assert isinstance(received_image, Image.Image)
assert received_image.size == image.size
assert received_image.mode == image.mode
assert list(received_image.getdata()) == list(image.getdata())
def test_delete_image(
image_in_minio: tuple[UUID, Image.Image],
image_repository: ImageRepository,
) -> None:
"""Test deleting an image from ImageRepository."""
# Arrange
image_id, _ = image_in_minio
assert image_repository.get(image_id) is not None
# Act
image_repository.delete(image_id)
# Assert
assert image_repository.get(image_id) is None
def test_list_all_images(
image_in_minio: tuple[UUID, Image.Image],
image_repository: ImageRepository,
) -> None:
"""Test listing all images in ImageRepository."""
# Arrange
image_id, _ = image_in_minio
# Act
image_ids = image_repository.list_all()
# Assert
assert isinstance(image_ids, list)
assert all(isinstance(i, UUID) for i in image_ids)
assert image_id in image_ids
def test_list_all_images_empty(
image_repository: ImageRepository,
) -> None:
"""Test listing all images in an empty ImageRepository."""
# Act
image_ids = image_repository.list_all()
# Assert
assert isinstance(image_ids, list)
assert len(image_ids) == 0
# Allows local debugging by running file as script
if __name__ == "__main__":
pytest.main(["-s", "-v", __file__])
@@ -0,0 +1,109 @@
"""Integration tests for the JobRepositoryInterface."""
from uuid import UUID
import pytest
from data_store.interfaces import JobRepositoryInterface
from data_store.dto import Job
def test_get_raises_not_implemented_if_not_overwritten(
job_id_list: list[UUID],
) -> None:
"""Test that calling get on a subclass without get implemented raises NotImplementedError."""
class Incomplete(JobRepositoryInterface):
"""A class missing the get method."""
def get(self, job_id: UUID) -> Job | None:
return super().get(job_id) # type: ignore
def put(self, job: Job) -> None:
return None
def delete(self, job_id: UUID) -> None:
return None
def list_all(self) -> list[UUID]:
return []
instance = Incomplete()
with pytest.raises(NotImplementedError):
instance.get(job_id_list[0])
def test_put_raises_not_implemented_if_not_overwritten(
job: Job,
) -> None:
"""Test that calling put on a subclass without put implemented raises NotImplementedError."""
class Incomplete(JobRepositoryInterface):
"""A class missing the put method."""
def get(self, job_id: UUID) -> Job | None:
return None
def put(self, job: Job) -> None:
return super().put(job) # type: ignore
def delete(self, job_id: UUID) -> None:
return None
def list_all(self) -> list[UUID]:
return []
instance = Incomplete()
with pytest.raises(NotImplementedError):
instance.put(job)
def test_delete_raises_not_implemented_if_not_overwritten(
job_id_list: list[UUID],
) -> None:
"""Test that calling delete on a subclass without delete implemented raises NotImplementedError."""
class Incomplete(JobRepositoryInterface):
"""A class missing the delete method."""
def get(self, job_id: UUID) -> Job | None:
return None
def put(self, job: Job) -> None:
return None
def delete(self, job_id: UUID) -> None:
return super().delete(job_id) # type: ignore
def list_all(self) -> list[UUID]:
return []
instance = Incomplete()
with pytest.raises(NotImplementedError):
instance.delete(job_id_list[0])
def test_list_all_raises_not_implemented_if_not_overwritten() -> None:
"""Test that calling list_all on a subclass without list_all implemented raises NotImplementedError."""
class Incomplete(JobRepositoryInterface):
"""A class missing the list_all method."""
def get(self, job_id: UUID) -> Job | None:
return None
def put(self, job: Job) -> None:
return None
def delete(self, job_id: UUID) -> None:
return None
def list_all(self) -> list[UUID]:
return super().list_all() # type: ignore
instance = Incomplete()
with pytest.raises(NotImplementedError):
instance.list_all()
# Allows local debugging by running file as script
if __name__ == "__main__":
pytest.main(["-s", "-v", __file__])
+116
View File
@@ -0,0 +1,116 @@
"""Integration tests for JobRepository."""
from uuid import UUID
import pytest
from PIL import Image
from data_store.repositories.job_repository import JobRepository
from data_store.dto import Job
def same_image(img1: Image.Image, img2: Image.Image) -> bool:
"""Check if two images are the same based on their attributes."""
assert img1.size == img2.size
assert img1.mode == img2.mode
assert list(img1.getdata()) == list(img2.getdata())
return True
def same_job(job1: Job, job2: Job) -> bool:
"""Check if two Job instances are the same based on their attributes."""
assert job1.job_id == job2.job_id
assert same_image(job1.image, job2.image)
assert job1.annotation == job2.annotation
return True
def test_get_job(
job_in_minio: Job,
job_repository: JobRepository,
) -> None:
"""Test retrieving a job from JobRepository."""
# Arrange
job_id = job_in_minio.job_id
# Act
retrieved_job = job_repository.get(job_id)
# Assert
assert retrieved_job is not None
assert isinstance(retrieved_job, Job)
assert same_job(retrieved_job, job_in_minio)
def test_get_job_without_image_returns_none(
annotation_in_minio: tuple[UUID, object],
job_repository: JobRepository,
) -> None:
"""Test retrieving a job without an image returns None."""
# Arrange
job_id, _ = annotation_in_minio
# Act
retrieved_job = job_repository.get(job_id)
# Assert
assert retrieved_job is None
def test_get_job_without_annotation_returns_none(
image_in_minio: tuple[UUID, object],
job_repository: JobRepository,
) -> None:
"""Test retrieving a job without an annotation returns None."""
# Arrange
job_id, _ = image_in_minio
# Act
retrieved_job = job_repository.get(job_id)
# Assert
assert retrieved_job is None
def test_put_job(
job_repository: JobRepository,
job: Job,
) -> None:
"""Test storing a job in JobRepository."""
# Arrange
job_id = job.job_id
assert job_repository.get(job_id) is None
# Act
job_repository.put(job)
# Assert
received_job = job_repository.get(job_id)
assert received_job is not None
assert isinstance(received_job, Job)
assert same_job(received_job, job)
def test_delete_job(
job_in_minio: Job,
job_repository: JobRepository,
) -> None:
"""Test deleting a job from JobRepository."""
# Arrange
job_id = job_in_minio.job_id
assert job_repository.get(job_id) is not None
# Act
job_repository.delete(job_id)
# Assert
assert job_repository.get(job_id) is None
def test_list_all_jobs(
job_in_minio: Job,
job_repository: JobRepository,
) -> None:
"""Test listing all job IDs from JobRepository."""
# Arrange
job_id = job_in_minio.job_id
# Act
job_ids = job_repository.list_all()
# Assert
assert isinstance(job_ids, list)
assert all(isinstance(jid, UUID) for jid in job_ids)
assert job_id in job_ids
# Allows local debugging by running file as script
if __name__ == "__main__":
pytest.main(["-s", "-v", __file__])
+141
View File
@@ -0,0 +1,141 @@
"""Unit tests for AnnotationRepository class."""
import unittest
from unittest.mock import patch, MagicMock, _patch_dict
from uuid import UUID
from data_store.repositories.annotation_repository import AnnotationRepository
from data_store.dto import (
Annotation,
AngleEnum,
ColourEnum,
ContactEnum,
DepthEnum,
DistanceEnum,
LightingEnum,
PointOfViewEnum,
)
class TestAnnotationRepository(unittest.TestCase):
"""Unit tests for AnnotationRepository class."""
patcher: _patch_dict
repo: AnnotationRepository
job_id: UUID
bad_job_id: str
annotation: Annotation
bad_annotation: dict
@classmethod
def setUpClass(cls) -> None:
"""Set up test case with a mocked MinioAdapter."""
# Patch environment variables for MinioAdapter
cls.patcher = patch.dict(
"os.environ",
{
"MINIO_ENDPOINT": "localhost:9000",
"MINIO_ACCESS_KEY": "minioadmin",
"MINIO_SECRET_KEY": "minioadmin",
"MINIO_BUCKET": "test-bucket",
},
)
cls.patcher.start()
# Prepare AnnotationRepository
cls.repo = AnnotationRepository()
cls.repo._get = MagicMock() # type: ignore[method-assign]
cls.repo._put = MagicMock() # type: ignore[method-assign]
cls.repo._delete = MagicMock() # type: ignore[method-assign]
# Prepare other test variables
cls.job_id = UUID("d87022f8-4169-4d69-8512-bbd65cc1cb23")
cls.bad_job_id = "not-a-uuid"
cls.annotation = Annotation(
angle=AngleEnum.HIGH,
angle_uncertainty=True,
colour=ColourEnum.HIGH,
colour_uncertainty=False,
contact=ContactEnum.OFFER,
contact_uncertainty=True,
depth=DepthEnum.LOW,
depth_uncertainty=False,
distance=DistanceEnum.CLOSE,
distance_uncertainty=True,
lighting=LightingEnum.MEDIUM,
lighting_uncertainty=False,
point_of_view=PointOfViewEnum.FRONTAL,
point_of_view_uncertainty=True,
)
cls.bad_annotation = {"not_an_annotation": "irrelevant value"}
@classmethod
def tearDownClass(cls) -> None:
# Stop patching
cls.patcher.stop()
def setUp(self) -> None:
"""Set up each test with a fresh AnnotationRepository instance."""
self.repo._get.reset_mock() # type: ignore[attr-defined]
self.repo._put.reset_mock() # type: ignore[attr-defined]
self.repo._delete.reset_mock() # type: ignore[attr-defined]
def test_should_adhere_to_interface(self) -> None:
"""Test that AnnotationRepository adheres to AnnotationRepositoryInterface."""
# Instantiation fails if interface not adhered to
_ = AnnotationRepository()
def test_should_have_encoding_set(self) -> None:
"""Test that AnnotationRepository has encoding when instantiated."""
self.assertTrue(hasattr(self.repo, "encoding"))
def test_should_have_data_format_set(self) -> None:
"""Test that AnnotationRepository has data_format when instantiated."""
self.assertTrue(hasattr(self.repo, "data_format"))
def test_should_have_folder_name_set(self) -> None:
"""Test that AnnotationRepository has folder_name when instantiated."""
self.assertTrue(hasattr(self.repo, "folder_name"))
def test_object_name_invalid_job_id_raises_value_error(self) -> None:
"""Test that _object_name() raises ValueError for invalid job_id."""
with self.assertRaises(ValueError):
_ = self.repo._object_name(self.bad_job_id) # type: ignore
def test_get_invalid_job_id_raises_value_error(self) -> None:
"""Test that get() raises ValueError for invalid job_id."""
with self.assertRaises(ValueError):
self.repo.get(self.bad_job_id) # type: ignore
def test_get_data_that_cannot_be_converted_returns_none(self) -> None:
"""Test that get() returns None if retrieved data cannot be converted to Annotation."""
# Arrange
bad_data = b'{"not":"valid annotation data"}'
mock_buffer = MagicMock()
mock_buffer.read.return_value = bad_data
self.repo._get.return_value = mock_buffer # type: ignore[attr-defined]
# Act
result = self.repo.get(self.job_id)
# Assert
self.assertIsNone(result)
self.repo._get.assert_called_once_with( # type: ignore[attr-defined]
f"{self.repo.folder_name}/{self.job_id}.{self.repo.data_format}"
)
def test_put_invalid_job_id_raises_value_error(self) -> None:
"""Test that put() raises ValueError for invalid job_id."""
with self.assertRaises(ValueError):
self.repo.put(self.annotation, self.bad_job_id) # type: ignore
def test_put_invalid_annotation_raises_value_error(self) -> None:
"""Test that put() raises ValueError for invalid annotation."""
with self.assertRaises(ValueError):
self.repo.put(self.bad_annotation, self.job_id) # type: ignore
def test_delete_invalid_job_id_raises_value_error(self) -> None:
"""Test that delete() raises ValueError for invalid job_id."""
with self.assertRaises(ValueError):
self.repo.delete(self.bad_job_id) # type: ignore
# Allows local debugging by running file as script
if __name__ == "__main__":
unittest.main()
+98
View File
@@ -0,0 +1,98 @@
"""Unit tests for ImageRepository class."""
import unittest
from unittest.mock import patch, MagicMock, _patch_dict
from uuid import UUID
from PIL import Image
from data_store.repositories.image_repository import ImageRepository
class TestImageRepository(unittest.TestCase):
"""Unit tests for ImageRepository class."""
patcher: _patch_dict
repo: ImageRepository
image: Image.Image
bad_image: str
image_id: UUID
bad_image_id: str
@classmethod
def setUpClass(cls) -> None:
"""Set up test case with a mocked MinioAdapter."""
# Patch environment variables for MinioAdapter
cls.patcher = patch.dict(
"os.environ",
{
"MINIO_ENDPOINT": "localhost:9000",
"MINIO_ACCESS_KEY": "minioadmin",
"MINIO_SECRET_KEY": "minioadmin",
"MINIO_BUCKET": "test-bucket",
},
)
cls.patcher.start()
# Prepare ImageRepository with mocked MinioAdapter
cls.repo = ImageRepository()
cls.repo._get = MagicMock(return_value=None) # type: ignore[method-assign]
cls.repo._put = MagicMock(return_value=None) # type: ignore[method-assign]
cls.repo._delete = MagicMock(return_value=None) # type: ignore[method-assign]
# Prepare other test variables
cls.image = Image.new("RGB", (10, 10))
cls.image_id = UUID("d87022f8-4169-4d69-8512-bbd65cc1cb23")
cls.bad_image = "not_an_image"
cls.bad_image_id = "not-a-uuid"
@classmethod
def tearDownClass(cls) -> None:
# Stop patching
cls.patcher.stop()
def setUp(self) -> None:
"""Set up each test with a fresh ImageRepository instance."""
self.repo._get.reset_mock() # type: ignore[attr-defined]
self.repo._put.reset_mock() # type: ignore[attr-defined]
self.repo._delete.reset_mock() # type: ignore[attr-defined]
def test_should_adhere_to_interface(self) -> None:
"""Test that ImageRepository adheres to ImageRepositoryInterface."""
# Instantiation fails if interface not adhered to
_ = ImageRepository()
def test_should_have_image_format_set(self) -> None:
"""Test that ImageRepository has image_format set."""
self.assertTrue(hasattr(self.repo, "image_format"))
def test_should_have_folder_name_set(self) -> None:
"""Test that ImageRepository has folder_name set."""
self.assertTrue(hasattr(self.repo, "folder_name"))
def test_object_name_invalid_image_id_raises_value_error(self) -> None:
"""Test that _object_name() raises ValueError for invalid image_id."""
with self.assertRaises(ValueError):
_ = self.repo._object_name(self.bad_image_id) # type: ignore
def test_get_invalid_image_id_raises_value_error(self) -> None:
"""Test getting an image with invalid image_id."""
with self.assertRaises(ValueError):
self.repo.get(self.bad_image_id) # type: ignore
def test_put_invalid_image_id_raises_value_error(self) -> None:
"""Test putting an image with invalid image_id."""
with self.assertRaises(ValueError):
self.repo.put(self.image, self.bad_image_id) # type: ignore
def test_put_invalid_image_type_raises_value_error(self) -> None:
"""Test putting an invalid image type."""
with self.assertRaises(ValueError):
self.repo.put(self.bad_image, self.bad_image_id) # type: ignore
def test_delete_invalid_image_id_raises_value_error(self) -> None:
"""Test deleting an image with invalid image_id."""
with self.assertRaises(ValueError):
self.repo.delete(self.bad_image_id) # type: ignore
# Allows local debugging by running file as script
if __name__ == "__main__":
unittest.main()
+85
View File
@@ -0,0 +1,85 @@
"""Unit tests for JobRepository class."""
import unittest
from unittest.mock import patch, MagicMock, _patch_dict
from uuid import UUID
from data_store.repositories.job_repository import JobRepository
class TestJobRepository(unittest.TestCase):
"""Unit tests for JobRepository class."""
patcher: _patch_dict
repo: JobRepository
job_id: UUID
bad_job_id: str
@classmethod
def setUpClass(cls) -> None:
"""Set up test case with a mocked MinioAdapter."""
# Patch environment variables for MinioAdapter
cls.patcher = patch.dict(
"os.environ",
{
"MINIO_ENDPOINT": "localhost:9000",
"MINIO_ACCESS_KEY": "minioadmin",
"MINIO_SECRET_KEY": "minioadmin",
"MINIO_BUCKET": "test-bucket",
},
)
cls.patcher.start()
# Prepare JobRepository
cls.repo = JobRepository()
cls.repo._get = MagicMock(return_value=None) # type: ignore[method-assign]
cls.repo._put = MagicMock(return_value=None) # type: ignore[method-assign]
cls.repo._delete = MagicMock(return_value=None) # type: ignore[method-assign]
# Prepare other test variables
cls.job_id = UUID("d87022f8-4169-4d69-8512-bbd65cc1cb23")
cls.bad_job_id = "not-a-uuid"
@classmethod
def tearDownClass(cls) -> None:
# Stop patching
cls.patcher.stop()
def setUp(self) -> None:
"""Set up each test with a fresh JobRepository instance."""
self.repo._get.reset_mock() # type: ignore[attr-defined]
self.repo._put.reset_mock() # type: ignore[attr-defined]
self.repo._delete.reset_mock() # type: ignore[attr-defined]
def test_should_adhere_to_interface(self) -> None:
"""Test that JobRepository adheres to JobRepositoryInterface."""
# Instantiation fails if interface not adhered to
_ = JobRepository()
def test_enter_calls_super_and_returns_self(self) -> None:
"""Test that __enter__ calls super().__enter__() and returns self."""
repo = JobRepository()
with patch(
"python_repositories.adapters.MinioAdapter.__enter__", return_value=None
) as mock_super_enter:
result = repo.__enter__()
mock_super_enter.assert_called_once()
self.assertIs(result, repo)
def test_get_invalid_job_id_raises_value_error(self) -> None:
"""Test that get with invalid job_id raises ValueError."""
with self.assertRaises(ValueError):
self.repo.get(self.bad_job_id) # type: ignore[arg-type]
def test_put_invalid_job_raises_value_error(self) -> None:
"""Test that put with invalid job raises ValueError."""
with self.assertRaises(ValueError):
self.repo.put("not_a_job") # type: ignore[arg-type]
def test_delete_invalid_job_id_raises_value_error(self) -> None:
"""Test that delete with invalid job_id raises ValueError."""
with self.assertRaises(ValueError):
self.repo.delete(self.bad_job_id) # type: ignore[arg-type]
# Allows local debugging by running file as script
if __name__ == "__main__":
unittest.main()
Generated
+2840
View File
File diff suppressed because it is too large Load Diff