Coverage for fastblocks/adapters/images/cloudinary.py: 67%
51 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-09 00:47 -0700
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-09 00:47 -0700
1"""Cloudinary image adapter implementation."""
3from contextlib import suppress
4from typing import Any
5from uuid import UUID
7from acb.config import Settings
8from acb.depends import depends
10from ._base import ImagesBase
13class CloudinarySettings(Settings): # type: ignore[misc]
14 """Cloudinary-specific settings."""
16 cloud_name: str
17 api_key: str
18 api_secret: str
19 secure: bool = True
20 upload_preset: str | None = None
23class CloudinaryAdapter(ImagesBase):
24 """Cloudinary image adapter implementation."""
26 # Required ACB 0.19.0+ metadata
27 MODULE_ID: UUID = UUID("01937d86-4f2a-7b3c-8d9e-f3b4d3c2b2a1") # Static UUID7
28 MODULE_STATUS = "stable"
30 def __init__(self) -> None:
31 """Initialize Cloudinary adapter."""
32 super().__init__()
33 self.settings = CloudinarySettings()
35 # Register with ACB dependency system
36 with suppress(Exception):
37 depends.set(self)
39 async def upload_image(self, file_data: bytes, filename: str) -> str:
40 """Upload image to Cloudinary and return public_id."""
41 # Basic implementation - would integrate with cloudinary library
42 # For now, return a mock public_id based on filename
43 public_id = filename.rsplit(".", 1)[0] # Remove extension
44 return f"uploads/{public_id}"
46 async def get_image_url(
47 self, image_id: str, transformations: dict[str, Any] | None = None
48 ) -> str:
49 """Generate Cloudinary URL with optional transformations."""
50 base_url = f"https://res.cloudinary.com/{self.settings.cloud_name}/image/upload"
52 if transformations:
53 # Build transformation string
54 transform_parts = []
55 for key, value in transformations.items():
56 if key == "width":
57 transform_parts.append(f"w_{value}")
58 elif key == "height":
59 transform_parts.append(f"h_{value}")
60 elif key == "crop":
61 transform_parts.append(f"c_{value}")
62 elif key == "quality":
63 transform_parts.append(f"q_{value}")
64 elif key == "format":
65 transform_parts.append(f"f_{value}")
67 if transform_parts:
68 transform_str = ",".join(transform_parts)
69 return f"{base_url}/{transform_str}/{image_id}"
71 return f"{base_url}/{image_id}"
73 def get_img_tag(self, image_id: str, alt: str, **attributes: Any) -> str:
74 """Generate complete img tag with Cloudinary URL."""
75 url = self.get_image_url(image_id, attributes.pop("transformations", None))
77 # Build attributes string
78 attr_parts = [f'src="{url}"', f'alt="{alt}"']
80 for key, value in attributes.items():
81 if key in ("width", "height", "class", "id", "style"):
82 attr_parts.append(f'{key}="{value}"')
84 # Add lazy loading by default
85 if "loading" not in attributes:
86 attr_parts.append('loading="lazy"')
88 return f"<img {' '.join(attr_parts)}>"