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

1"""Cloudinary image adapter implementation.""" 

2 

3from contextlib import suppress 

4from typing import Any 

5from uuid import UUID 

6 

7from acb.config import Settings 

8from acb.depends import depends 

9 

10from ._base import ImagesBase 

11 

12 

13class CloudinarySettings(Settings): # type: ignore[misc] 

14 """Cloudinary-specific settings.""" 

15 

16 cloud_name: str 

17 api_key: str 

18 api_secret: str 

19 secure: bool = True 

20 upload_preset: str | None = None 

21 

22 

23class CloudinaryAdapter(ImagesBase): 

24 """Cloudinary image adapter implementation.""" 

25 

26 # Required ACB 0.19.0+ metadata 

27 MODULE_ID: UUID = UUID("01937d86-4f2a-7b3c-8d9e-f3b4d3c2b2a1") # Static UUID7 

28 MODULE_STATUS = "stable" 

29 

30 def __init__(self) -> None: 

31 """Initialize Cloudinary adapter.""" 

32 super().__init__() 

33 self.settings = CloudinarySettings() 

34 

35 # Register with ACB dependency system 

36 with suppress(Exception): 

37 depends.set(self) 

38 

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}" 

45 

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" 

51 

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}") 

66 

67 if transform_parts: 

68 transform_str = ",".join(transform_parts) 

69 return f"{base_url}/{transform_str}/{image_id}" 

70 

71 return f"{base_url}/{image_id}" 

72 

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)) 

76 

77 # Build attributes string 

78 attr_parts = [f'src="{url}"', f'alt="{alt}"'] 

79 

80 for key, value in attributes.items(): 

81 if key in ("width", "height", "class", "id", "style"): 

82 attr_parts.append(f'{key}="{value}"') 

83 

84 # Add lazy loading by default 

85 if "loading" not in attributes: 

86 attr_parts.append('loading="lazy"') 

87 

88 return f"<img {' '.join(attr_parts)}>"