import io import logging from typing import Optional import requests from sorl.thumbnail.default import engine from sorl.thumbnail.shortcuts import get_thumbnail from core_images.cache import ServiceCacheMixin from core_images.cache import ( create_image_thumbnails, delete_image_thumbnails, ) from core_images.cached_either import Right, Left from core_images.consts import ( NO_IMAGE_CACHE_VALUE, SMALL_RESOLUTION, MEDIUM_RESOLUTION, LARGE_RESOLUTION, ) from core_images.models import FallbackUrl, LabelImage from core_images.storage_backends import PublicMediaStorage logger = logging.getLogger(__name__) class InternalDataService(ServiceCacheMixin): FALLBACK_CACHE_KEY_PREFIX = "fallback_url" FALLBACK_MAP_CACHE_KEY = "fallback_map" NO_IMAGE_MSG = "No image available" RESOLUTION_MAPPING = { SMALL_RESOLUTION: "100x100", MEDIUM_RESOLUTION: "300x300", LARGE_RESOLUTION: "600x600", } def __init__(self, storage): self.storage = storage def get_fallback_url(self, gras_id: str): clean_gras_id = gras_id.replace("GRAS_", "") cache_key = [self.FALLBACK_CACHE_KEY_PREFIX, clean_gras_id] url = self.cache_get(cache_key) if url == NO_IMAGE_CACHE_VALUE: return Left(self.NO_IMAGE_MSG) elif url: return Right(url, True) fallback = FallbackUrl.objects.filter(gras_id=clean_gras_id).first() if not fallback: self.cache_set(cache_key, NO_IMAGE_CACHE_VALUE) return Left(self.NO_IMAGE_MSG) self.cache_set(cache_key, fallback.url) return Right(fallback.url) def get_fallback_map(self): cache_key = [ self.FALLBACK_CACHE_KEY_PREFIX, self.FALLBACK_MAP_CACHE_KEY, ] fallbacks = self.cache_get(cache_key) if isinstance(fallbacks, dict): return Right(fallbacks) fallbacks = { k: v for k, v in FallbackUrl.objects.values_list("gras_id", "url") } self.cache_set(cache_key, fallbacks) return Right(fallbacks) def safe_delete_fallback(self, gras_id: str, resolution: Optional[str]): clean_gras_id = gras_id.replace("GRAS_", "") cache_key = [self.FALLBACK_CACHE_KEY_PREFIX, clean_gras_id, resolution] url = self.cache_get(cache_key) if url == NO_IMAGE_CACHE_VALUE: return Left(self.NO_IMAGE_MSG, True) qty, _ = FallbackUrl.objects.filter(gras_id=clean_gras_id).delete() logger.info(f"Removing fallback url for : {gras_id}") self.cache_set(cache_key, NO_IMAGE_CACHE_VALUE) return Right(True) if qty else Left("Nothing to delete") def invalidate_fallback_cache(self, gras_id: str): clean_gras_id = gras_id.replace("GRAS_", "") logger.info(f"Invalidating fallback: {gras_id}") self.cache_delete([self.FALLBACK_CACHE_KEY_PREFIX, clean_gras_id]) self.cache_delete( [self.FALLBACK_CACHE_KEY_PREFIX, self.FALLBACK_MAP_CACHE_KEY] ) return Right(True) def label_image_url(self, label_id: str, resolution: Optional[str]): cache_key = ["label_image_url", label_id, resolution] url = self.cache_get(cache_key) if url == NO_IMAGE_CACHE_VALUE: return Left(self.NO_IMAGE_MSG) elif url: return Right(url, True) image_data = LabelImage.objects.filter(label_id=label_id).first() if not image_data: self.cache_set(cache_key, NO_IMAGE_CACHE_VALUE) return Left(self.NO_IMAGE_MSG) image = image_data.image geometry_string = self.RESOLUTION_MAPPING.get(resolution) if geometry_string: try: thumbnail = get_thumbnail( image, geometry_string, upscale=False, crop=False ) url = thumbnail.url except Exception as e: logger.exception(f"Unable to get thumbnail: {e}") url = image.url else: url = image.url self.cache_set(cache_key, url) return Right(url) def create_label_thumbnails(self, label_id: str): image_data = LabelImage.objects.filter(label_id=label_id).first() if not image_data: return Left(False) create_image_thumbnails( image_data.image, self.RESOLUTION_MAPPING.values() ) return Right(True) def delete_label_thumbnails(self, label_id: str): image_data = LabelImage.objects.filter(label_id=label_id).first() if not image_data: return Left(False) delete_image_thumbnails(image_data.image) return Right(True) def invalidate_labels_cache(self, label_id: str): cache_keys = [ ["label_image_url", label_id, SMALL_RESOLUTION], ["label_image_url", label_id, MEDIUM_RESOLUTION], ["label_image_url", label_id, LARGE_RESOLUTION], ] for cache_key in cache_keys: self.cache_delete(cache_key) return Right(True) def create_external_image_thumbnails( self, upload_to, image_id: str, url: str ): path = f"{upload_to}/{image_id}.jpg" resp = requests.get(url) if not engine.is_valid_image(resp.content): logger.error( "Invalid source image for thumbnail from vendor", {"image_id": image_id, "url": url} ) return Left("Invalid source image") file = io.BytesIO(resp.content) self.storage.save(path, file) file = self.storage.open(path) create_image_thumbnails(file, self.RESOLUTION_MAPPING.values()) return Right(True) def get_external_image_thumbnail( self, upload_to, image_id: str, resolution ): path = f"{upload_to}/{image_id}.jpg" image = self.storage.open(path, "wb") geometry_string = self.RESOLUTION_MAPPING.get(resolution) if geometry_string: try: thumbnail = get_thumbnail( image, geometry_string, upscale=False, crop=False ) url = thumbnail.url except Exception as e: logger.exception(f"Unable to get thumbnail: {e}") url = image.url else: url = image.url return url def get_internal_data_service(): return InternalDataService(storage=PublicMediaStorage())