Coverage for fastblocks / adapters / images / cloudinary.py: 44%
54 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-26 03:58 -0800
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-26 03:58 -0800
1"""Cloudinary image adapter implementation."""
3from contextlib import suppress
4from typing import Any
5from uuid import UUID
7from acb.depends import depends
9from ._base import ImagesBase, ImagesBaseSettings
12class CloudinaryImagesSettings(ImagesBaseSettings):
13 """Cloudinary-specific settings."""
15 cloud_name: str
16 api_key: str
17 api_secret: str
18 secure: bool = True
19 upload_preset: str | None = None
22class CloudinaryImages(ImagesBase):
23 """Cloudinary image adapter implementation."""
25 # Required ACB 0.19.0+ metadata
26 MODULE_ID: UUID = UUID("01937d86-4f2a-7b3c-8d9e-f3b4d3c2b2a1") # Static UUID7
27 MODULE_STATUS = "stable"
29 def __init__(self) -> None:
30 """Initialize Cloudinary adapter."""
31 super().__init__()
32 self.settings = CloudinaryImagesSettings()
34 # Register with ACB dependency system
35 with suppress(Exception):
36 depends.set(self)
38 async def upload_image(self, file_data: bytes, filename: str) -> str:
39 """Upload image to Cloudinary and return public_id."""
40 # Basic implementation - would integrate with cloudinary library
41 # For now, return a mock public_id based on filename
42 public_id = filename.rsplit(".", 1)[0] # Remove extension
43 return f"uploads/{public_id}"
45 async def get_image_url(
46 self, image_id: str, transformations: dict[str, Any] | None = None
47 ) -> str:
48 """Generate Cloudinary URL with optional transformations."""
49 base_url = f"https://res.cloudinary.com/{self.settings.cloud_name}/image/upload"
51 if transformations:
52 # Build transformation string
53 transform_parts = []
54 for key, value in transformations.items():
55 if key == "width":
56 transform_parts.append(f"w_{value}")
57 elif key == "height":
58 transform_parts.append(f"h_{value}")
59 elif key == "crop":
60 transform_parts.append(f"c_{value}")
61 elif key == "quality":
62 transform_parts.append(f"q_{value}")
63 elif key == "format":
64 transform_parts.append(f"f_{value}")
66 if transform_parts:
67 transform_str = ",".join(transform_parts)
68 return f"{base_url}/{transform_str}/{image_id}"
70 return f"{base_url}/{image_id}"
72 def get_img_tag(self, image_id: str, alt: str, **attributes: Any) -> str:
73 url = self.get_image_url(image_id, attributes.pop("transformations", None))
75 # Build attributes string
76 attr_parts = [f'src="{url}"', f'alt="{alt}"']
78 for key, value in attributes.items():
79 if key in ("width", "height", "class", "id", "style"):
80 attr_parts.append(f'{key}="{value}"')
82 # Add lazy loading by default
83 if "loading" not in attributes:
84 attr_parts.append('loading="lazy"')
86 return f"<img {' '.join(attr_parts)}>"
89ImagesSettings = CloudinaryImagesSettings
90Images = CloudinaryImages
92depends.set(Images, "cloudinary")
94__all__ = ["CloudinaryImages", "CloudinaryImagesSettings", "Images", "ImagesSettings"]